1
1
# coding=utf8
2
+ """Migration Transforms.
3
+
4
+ Transforms are AST nodes which describe how legacy translations should be
5
+ migrated. They are created inert and only return the migrated AST nodes when
6
+ they are evaluated by a MergeContext.
7
+
8
+ All Transforms evaluate to Fluent Patterns. This makes them suitable for
9
+ defining migrations of values of message, attributes and variants. The special
10
+ CONCAT Transform is capable of joining multiple Patterns returned by evaluating
11
+ other Transforms into a single Pattern. It can also concatenate Fluent
12
+ Expressions, like MessageReferences and ExternalArguments.
13
+
14
+ The COPY, REPLACE and PLURALS Transforms inherit from Source which is a special
15
+ AST Node defining the location (the file path and the id) of the legacy
16
+ translation. During the migration, the current MergeContext scans the
17
+ migration spec for Source nodes and extracts the information about all legacy
18
+ translations being migrated. Thus,
19
+
20
+ COPY('file.dtd', 'hello')
21
+
22
+ is equivalent to:
23
+
24
+ LITERAL(Source('file.dtd', 'hello'))
25
+
26
+ where LITERAL is a helper defined in the helpers.py module for creating Fluent
27
+ Patterns from the text passed as the argument.
28
+
29
+ The LITERAL helper and the special REPLACE_IN_TEXT Transforms are useful for
30
+ working with text rather than (path, key) source definitions. This is the case
31
+ when the migrated translation requires some hardcoded text, e.g. <a> and </a>
32
+ when multiple translations become a single one with a DOM overlay.
33
+
34
+ FTL.Message(
35
+ id=FTL.Identifier('update-failed'),
36
+ value=CONCAT(
37
+ COPY('aboutDialog.dtd', 'update.failed.start'),
38
+ LITERAL('<a>'),
39
+ COPY('aboutDialog.dtd', 'update.failed.linkText'),
40
+ LITERAL('</a>'),
41
+ COPY('aboutDialog.dtd', 'update.failed.end'),
42
+ )
43
+ )
44
+
45
+ The REPLACE_IN_TEXT Transform also takes text as input, making in possible to
46
+ pass it as the foreach function of the PLURALS Transform. In this case, each
47
+ slice of the plural string will be run through a REPLACE_IN_TEXT operation.
48
+ Those slices are strings, so a REPLACE(path, key, …) isn't suitable for them.
49
+
50
+ FTL.Message(
51
+ FTL.Identifier('delete-all'),
52
+ value=PLURALS(
53
+ 'aboutDownloads.dtd',
54
+ 'deleteAll',
55
+ EXTERNAL_ARGUMENT('num'),
56
+ lambda text: REPLACE_IN_TEXT(
57
+ text,
58
+ {
59
+ '#1': EXTERNAL_ARGUMENT('num')
60
+ }
61
+ )
62
+ )
63
+ )
64
+ """
65
+
2
66
from __future__ import unicode_literals
3
67
4
68
import fluent .syntax .ast as FTL
69
+ from .helpers import LITERAL
5
70
6
71
7
72
def evaluate (ctx , node ):
@@ -19,10 +84,10 @@ def __call__(self, ctx):
19
84
raise NotImplementedError
20
85
21
86
22
- class SOURCE (Transform ):
87
+ class Source (Transform ):
23
88
"""Declare the source translation to be migrated with other transforms.
24
89
25
- When evaluated `SOURCE ` returns a simple string value. All \\ uXXXX from
90
+ When evaluated `Source ` returns a simple string value. All \\ uXXXX from
26
91
the original translations are converted beforehand to the literal
27
92
characters they encode.
28
93
@@ -50,46 +115,15 @@ def __call__(self, ctx):
50
115
return ctx .get_source (self .path , self .key )
51
116
52
117
53
- class LITERAL (Transform ):
54
- """Create a Pattern with the literal text `value`.
55
-
56
- This transform is used by `LITERAL_FROM` and can be used on its own with
57
- `CONCAT`.
58
- """
59
-
60
- def __init__ (self , value ):
61
- self .value = value
62
-
63
- def __call__ (self , ctx ):
64
- elements = [FTL .TextElement (self .value )]
65
- return FTL .Pattern (elements )
66
-
67
-
68
- class LITERAL_FROM (SOURCE ):
118
+ class COPY (Source ):
69
119
"""Create a Pattern with the translation value from the given source."""
70
120
71
121
def __call__ (self , ctx ):
72
122
source = super (self .__class__ , self ).__call__ (ctx )
73
- return LITERAL (source )(ctx )
74
-
75
-
76
- class EXTERNAL (Transform ):
77
- """Create a Pattern with the external argument `name`
78
-
79
- This is a common use-case when joining translations with CONCAT.
80
- """
81
-
82
- def __init__ (self , name ):
83
- self .name = name
84
-
85
- def __call__ (self , ctx ):
86
- external = FTL .ExternalArgument (
87
- id = FTL .Identifier (self .name )
88
- )
89
- return FTL .Pattern ([external ])
123
+ return LITERAL (source )
90
124
91
125
92
- class REPLACE (Transform ):
126
+ class REPLACE_IN_TEXT (Transform ):
93
127
"""Replace various placeables in the translation with FTL placeables.
94
128
95
129
The original placeables are defined as keys on the `replacements` dict.
@@ -105,7 +139,9 @@ def __call__(self, ctx):
105
139
106
140
# Only replace placeable which are present in the translation.
107
141
replacements = {
108
- k : v for k , v in self .replacements .iteritems () if k in self .value
142
+ key : evaluate (ctx , repl )
143
+ for key , repl in self .replacements .iteritems ()
144
+ if key in self .value
109
145
}
110
146
111
147
# Order the original placeables by their position in the translation.
@@ -150,11 +186,11 @@ def is_non_empty(elem):
150
186
return FTL .Pattern (elements )
151
187
152
188
153
- class REPLACE_FROM ( SOURCE ):
189
+ class REPLACE ( Source ):
154
190
"""Create a Pattern with interpolations from given source.
155
191
156
192
Interpolations in the translation value from the given source will be
157
- replaced with FTL placeables using the `REPLACE ` transform.
193
+ replaced with FTL placeables using the `REPLACE_IN_TEXT ` transform.
158
194
"""
159
195
160
196
def __init__ (self , path , key , replacements ):
@@ -163,24 +199,27 @@ def __init__(self, path, key, replacements):
163
199
164
200
def __call__ (self , ctx ):
165
201
value = super (self .__class__ , self ).__call__ (ctx )
166
- return REPLACE (value , self .replacements )(ctx )
202
+ return REPLACE_IN_TEXT (value , self .replacements )(ctx )
167
203
168
204
169
- class PLURALS (Transform ):
170
- """Convert semicolon-separated variants into a select expression .
205
+ class PLURALS (Source ):
206
+ """Create a Pattern with plurals from given source .
171
207
172
208
Build an `FTL.SelectExpression` with the supplied `selector` and variants
173
- extracted from the source. Each variant will be run through the
174
- `foreach` function, which should return an `FTL.Node`.
209
+ extracted from the source. The source needs to be a semicolon-separated
210
+ list of variants. Each variant will be run through the `foreach` function,
211
+ which should return an `FTL.Node` or a `Transform`.
175
212
"""
176
213
177
- def __init__ (self , value , selector , foreach ):
178
- self .value = value
214
+ def __init__ (self , path , key , selector , foreach = LITERAL ):
215
+ super ( self .__class__ , self ). __init__ ( path , key )
179
216
self .selector = selector
180
217
self .foreach = foreach
181
218
182
219
def __call__ (self , ctx ):
183
- variants = self .value .split (';' )
220
+ value = super (self .__class__ , self ).__call__ (ctx )
221
+ selector = evaluate (ctx , self .selector )
222
+ variants = value .split (';' )
184
223
keys = ctx .plural_categories
185
224
last_index = min (len (variants ), len (keys )) - 1
186
225
@@ -197,31 +236,13 @@ def createVariant(zipped_enum):
197
236
)
198
237
199
238
select = FTL .SelectExpression (
200
- expression = self . selector ,
239
+ expression = selector ,
201
240
variants = map (createVariant , enumerate (zip (keys , variants )))
202
241
)
203
242
204
243
return FTL .Pattern ([select ])
205
244
206
245
207
- class PLURALS_FROM (SOURCE ):
208
- """Create a Pattern with plurals from given source.
209
-
210
- Semi-colon separated variants in the translation value from the given
211
- source will be replaced with an FTL select expression using the `PLURALS`
212
- transform.
213
- """
214
-
215
- def __init__ (self , path , key , selector , foreach ):
216
- super (self .__class__ , self ).__init__ (path , key )
217
- self .selector = selector
218
- self .foreach = foreach
219
-
220
- def __call__ (self , ctx ):
221
- value = super (self .__class__ , self ).__call__ (ctx )
222
- return PLURALS (value , self .selector , self .foreach )(ctx )
223
-
224
-
225
246
class CONCAT (Transform ):
226
247
"""Concatenate elements of many patterns."""
227
248
@@ -230,9 +251,18 @@ def __init__(self, *patterns):
230
251
231
252
def __call__ (self , ctx ):
232
253
# Flatten the list of patterns of which each has a list of elements.
233
- elements = [
234
- elems for pattern in self .patterns for elems in pattern .elements
235
- ]
254
+ def concat_elements (acc , cur ):
255
+ if isinstance (cur , FTL .Pattern ):
256
+ acc .extend (cur .elements )
257
+ return acc
258
+ elif (isinstance (cur , FTL .TextElement ) or
259
+ isinstance (cur , FTL .Expression )):
260
+ acc .append (cur )
261
+ return acc
262
+
263
+ raise RuntimeError (
264
+ 'CONCAT accepts FTL Patterns and Expressions.'
265
+ )
236
266
237
267
# Merge adjecent `FTL.TextElement` nodes.
238
268
def merge_adjecent_text (acc , cur ):
@@ -246,6 +276,7 @@ def merge_adjecent_text(acc, cur):
246
276
acc .append (cur )
247
277
return acc
248
278
279
+ elements = reduce (concat_elements , self .patterns , [])
249
280
elements = reduce (merge_adjecent_text , elements , [])
250
281
return FTL .Pattern (elements )
251
282
0 commit comments