11# frozen_string_literal: true
22
3+ # rubocop:disable Metrics/ClassLength
34module Maglev
45 # Translate a page into a given locale from a source locale.
56 # This is a fake service that only copies the page attributes.
@@ -16,7 +17,13 @@ class TranslatePage
1617 def call
1718 return nil unless page . persisted?
1819
20+ @translations = { }
21+
1922 translate_all!
23+
24+ persist_changes!
25+
26+ page
2027 end
2128
2229 protected
@@ -29,13 +36,19 @@ def theme
2936 @theme ||= fetch_theme . call
3037 end
3138
39+ # this is a third-step process because we need to group all the translations to process
40+ # in order to process them in parallel
3241 def translate_all!
33- translate_page_attributes
34- translate_all_sections
42+ # 1. replace all the text to translatewith placeholders
43+ prepare_translate_page_attributes
44+ prepare_translate_all_sections # sections from page and site
3545
36- persist_changes!
46+ # 2. Translate all the placeholders in parallel
47+ async_translate
3748
38- page
49+ # 3. replace the placeholders with the actual translations that has been processed in parallel
50+ replace_translations_in_page_attributes
51+ replace_translations_in_all_sections
3952 end
4053
4154 def persist_changes!
@@ -45,80 +58,123 @@ def persist_changes!
4558 end
4659 end
4760
48- def translate_page_attributes
61+ def async_translate
62+ tasks = @translations . map do |id , text |
63+ Concurrent ::Promises . future do
64+ { id => translate_text ( text ) }
65+ end
66+ end
67+ @translations = Concurrent ::Promises . zip ( *tasks ) . value! . reduce ( { } , :merge )
68+ end
69+
70+ def translate_text ( text )
71+ # @note: implement actual translation logic in a subclass
72+ # by default we just add the locale to the text
73+ text + " [#{ locale . upcase } ]"
74+ end
75+
76+ # ===== Apply translations =====
77+
78+ def replace_translations_in_page_attributes
4979 %w[ title seo_title meta_description og_title og_description ] . each do |attr |
50- translate_page_attribute ( attr )
80+ page . translations_for ( attr ) [ locale ] = replace_translated_text ( page . translations_for ( attr ) [ locale ] )
81+ end
82+ end
83+
84+ def replace_translations_in_all_sections
85+ [ page , site ] . each do |source |
86+ source . sections_translations = JSON . parse ( replace_translated_text ( source . sections_translations . to_json ) )
87+ end
88+ end
89+
90+ # ===== Prepare translations =====
91+
92+ def prepare_translate_page_attributes
93+ %w[ title seo_title meta_description og_title og_description ] . each do |attr |
94+ prepare_translate_page_attribute ( attr )
5195 end
5296 # og_image_url is a special case because it's a URL, not content
5397 page . translations_for ( :og_image_url ) [ locale ] = page . translations_for ( :og_image_url ) [ source_locale ]
5498 end
5599
56- def translate_page_attribute ( attr )
57- page . translations_for ( attr ) [ locale ] = translate_text ( page . translations_for ( attr ) [ source_locale ] )
100+ def prepare_translate_page_attribute ( attr )
101+ page . translations_for ( attr ) [ locale ] = prepare_translate_text ( page . translations_for ( attr ) [ source_locale ] )
58102 end
59103
60- def translate_all_sections
104+ def prepare_translate_all_sections
61105 [ page , site ] . each do |source |
62- translate_sections ( source )
106+ prepare_translate_sections ( source )
63107 end
64108 end
65109
66- def translate_sections ( source )
110+ def prepare_translate_sections ( source )
67111 # @note no need to translate if there are no sections in the source locale
68112 return if source . translations_for ( :sections , source_locale ) . blank?
69113
70114 # @note we don't want to overwrite existing translations
71115 return if source . translations_for ( :sections , locale ) . present?
72116
73117 source . sections_translations [ locale ] = clone_array ( source . sections_translations [ source_locale ] ) . tap do |sections |
74- sections . each { |section | translate_section ( section ) }
118+ sections . each { |section | prepare_translate_section ( section ) }
75119 end
76120 end
77121
78- def translate_section ( section )
122+ def prepare_translate_section ( section )
79123 definition = theme . sections . find ( section [ 'type' ] )
80- translate_settings ( section , definition )
81- translate_section_blocks ( section , definition )
124+ prepare_translate_settings ( section , definition )
125+ prepare_translate_section_blocks ( section , definition )
82126 end
83127
84- def translate_settings ( section_or_block , definition )
128+ def prepare_translate_settings ( section_or_block , definition )
85129 section_or_block [ 'settings' ] . each do |setting |
86130 type = definition . settings . find { |s | s . id == setting [ 'id' ] } &.type
87131 next if type . blank?
88132
89- setting [ 'value' ] = translate_setting_value ( setting [ 'value' ] , type )
133+ setting [ 'value' ] = prepare_translate_setting_value ( setting [ 'value' ] , type )
90134 end
91135 end
92136
93- def translate_section_blocks ( section , definition )
137+ def prepare_translate_section_blocks ( section , definition )
94138 section [ 'blocks' ] . each do |block |
95139 block_definition = definition . blocks . find { |b | b . type == block [ 'type' ] }
96- translate_settings ( block , block_definition )
140+ prepare_translate_settings ( block , block_definition )
97141 end
98142 end
99143
100- def translate_setting_value ( value , type )
144+ def prepare_translate_setting_value ( value , type )
101145 case type
102146 when 'text'
103- translate_text ( value )
147+ prepare_translate_text ( value )
104148 when 'link'
105- value . merge ( text : translate_text ( value [ 'text' ] ) ) if value . is_a? ( Hash )
149+ value . merge ( text : prepare_translate_text ( value [ 'text' ] ) ) if value . is_a? ( Hash )
106150 when 'image'
107- value . merge ( alt : translate_text ( value [ 'alt' ] ) ) if value . is_a? ( Hash )
151+ value . merge ( alt : prepare_translate_text ( value [ 'alt' ] ) ) if value . is_a? ( Hash )
108152 else
109153 value
110154 end
111155 end
112156
113157 # NOTE: this method is a placeholder for the actual translation logic.
114- def translate_text ( text )
115- return nil if text . blank?
158+ def prepare_translate_text ( text )
159+ return text if text . blank?
116160
117- text + " [#{ locale . upcase } ]"
161+ id = SecureRandom . uuid
162+ @translations [ id ] = text
163+
164+ "{#{ id } }"
165+ end
166+
167+ def replace_translated_text ( text )
168+ return text if text . blank?
169+
170+ text . gsub ( /\{ ([a-f0-9-]{36})\} / ) do |_match |
171+ @translations [ ::Regexp . last_match ( 1 ) ]
172+ end
118173 end
119174
120175 def clone_array ( array )
121176 Marshal . load ( Marshal . dump ( array || [ ] ) )
122177 end
123178 end
124179end
180+ # rubocop:enable Metrics/ClassLength
0 commit comments