1
1
# frozen_string_literal: true
2
2
3
+ # rubocop:disable Metrics/ClassLength
3
4
module Maglev
4
5
# Translate a page into a given locale from a source locale.
5
6
# This is a fake service that only copies the page attributes.
@@ -16,7 +17,13 @@ class TranslatePage
16
17
def call
17
18
return nil unless page . persisted?
18
19
20
+ @translations = { }
21
+
19
22
translate_all!
23
+
24
+ persist_changes!
25
+
26
+ page
20
27
end
21
28
22
29
protected
@@ -29,13 +36,19 @@ def theme
29
36
@theme ||= fetch_theme . call
30
37
end
31
38
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
32
41
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
35
45
36
- persist_changes!
46
+ # 2. Translate all the placeholders in parallel
47
+ async_translate
37
48
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
39
52
end
40
53
41
54
def persist_changes!
@@ -45,80 +58,123 @@ def persist_changes!
45
58
end
46
59
end
47
60
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
49
79
%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 )
51
95
end
52
96
# og_image_url is a special case because it's a URL, not content
53
97
page . translations_for ( :og_image_url ) [ locale ] = page . translations_for ( :og_image_url ) [ source_locale ]
54
98
end
55
99
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 ] )
58
102
end
59
103
60
- def translate_all_sections
104
+ def prepare_translate_all_sections
61
105
[ page , site ] . each do |source |
62
- translate_sections ( source )
106
+ prepare_translate_sections ( source )
63
107
end
64
108
end
65
109
66
- def translate_sections ( source )
110
+ def prepare_translate_sections ( source )
67
111
# @note no need to translate if there are no sections in the source locale
68
112
return if source . translations_for ( :sections , source_locale ) . blank?
69
113
70
114
# @note we don't want to overwrite existing translations
71
115
return if source . translations_for ( :sections , locale ) . present?
72
116
73
117
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 ) }
75
119
end
76
120
end
77
121
78
- def translate_section ( section )
122
+ def prepare_translate_section ( section )
79
123
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 )
82
126
end
83
127
84
- def translate_settings ( section_or_block , definition )
128
+ def prepare_translate_settings ( section_or_block , definition )
85
129
section_or_block [ 'settings' ] . each do |setting |
86
130
type = definition . settings . find { |s | s . id == setting [ 'id' ] } &.type
87
131
next if type . blank?
88
132
89
- setting [ 'value' ] = translate_setting_value ( setting [ 'value' ] , type )
133
+ setting [ 'value' ] = prepare_translate_setting_value ( setting [ 'value' ] , type )
90
134
end
91
135
end
92
136
93
- def translate_section_blocks ( section , definition )
137
+ def prepare_translate_section_blocks ( section , definition )
94
138
section [ 'blocks' ] . each do |block |
95
139
block_definition = definition . blocks . find { |b | b . type == block [ 'type' ] }
96
- translate_settings ( block , block_definition )
140
+ prepare_translate_settings ( block , block_definition )
97
141
end
98
142
end
99
143
100
- def translate_setting_value ( value , type )
144
+ def prepare_translate_setting_value ( value , type )
101
145
case type
102
146
when 'text'
103
- translate_text ( value )
147
+ prepare_translate_text ( value )
104
148
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 )
106
150
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 )
108
152
else
109
153
value
110
154
end
111
155
end
112
156
113
157
# 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?
116
160
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
118
173
end
119
174
120
175
def clone_array ( array )
121
176
Marshal . load ( Marshal . dump ( array || [ ] ) )
122
177
end
123
178
end
124
179
end
180
+ # rubocop:enable Metrics/ClassLength
0 commit comments