11# 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+
266from __future__ import unicode_literals
367
468import fluent .syntax .ast as FTL
69+ from .helpers import LITERAL
570
671
772def evaluate (ctx , node ):
@@ -19,10 +84,10 @@ def __call__(self, ctx):
1984 raise NotImplementedError
2085
2186
22- class SOURCE (Transform ):
87+ class Source (Transform ):
2388 """Declare the source translation to be migrated with other transforms.
2489
25- When evaluated `SOURCE ` returns a simple string value. All \\ uXXXX from
90+ When evaluated `Source ` returns a simple string value. All \\ uXXXX from
2691 the original translations are converted beforehand to the literal
2792 characters they encode.
2893
@@ -50,46 +115,15 @@ def __call__(self, ctx):
50115 return ctx .get_source (self .path , self .key )
51116
52117
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 ):
69119 """Create a Pattern with the translation value from the given source."""
70120
71121 def __call__ (self , ctx ):
72122 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 )
90124
91125
92- class REPLACE (Transform ):
126+ class REPLACE_IN_TEXT (Transform ):
93127 """Replace various placeables in the translation with FTL placeables.
94128
95129 The original placeables are defined as keys on the `replacements` dict.
@@ -105,7 +139,9 @@ def __call__(self, ctx):
105139
106140 # Only replace placeable which are present in the translation.
107141 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
109145 }
110146
111147 # Order the original placeables by their position in the translation.
@@ -150,11 +186,11 @@ def is_non_empty(elem):
150186 return FTL .Pattern (elements )
151187
152188
153- class REPLACE_FROM ( SOURCE ):
189+ class REPLACE ( Source ):
154190 """Create a Pattern with interpolations from given source.
155191
156192 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.
158194 """
159195
160196 def __init__ (self , path , key , replacements ):
@@ -163,24 +199,27 @@ def __init__(self, path, key, replacements):
163199
164200 def __call__ (self , ctx ):
165201 value = super (self .__class__ , self ).__call__ (ctx )
166- return REPLACE (value , self .replacements )(ctx )
202+ return REPLACE_IN_TEXT (value , self .replacements )(ctx )
167203
168204
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 .
171207
172208 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`.
175212 """
176213
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 )
179216 self .selector = selector
180217 self .foreach = foreach
181218
182219 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 (';' )
184223 keys = ctx .plural_categories
185224 last_index = min (len (variants ), len (keys )) - 1
186225
@@ -197,31 +236,13 @@ def createVariant(zipped_enum):
197236 )
198237
199238 select = FTL .SelectExpression (
200- expression = self . selector ,
239+ expression = selector ,
201240 variants = map (createVariant , enumerate (zip (keys , variants )))
202241 )
203242
204243 return FTL .Pattern ([select ])
205244
206245
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-
225246class CONCAT (Transform ):
226247 """Concatenate elements of many patterns."""
227248
@@ -230,9 +251,18 @@ def __init__(self, *patterns):
230251
231252 def __call__ (self , ctx ):
232253 # 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+ )
236266
237267 # Merge adjecent `FTL.TextElement` nodes.
238268 def merge_adjecent_text (acc , cur ):
@@ -246,6 +276,7 @@ def merge_adjecent_text(acc, cur):
246276 acc .append (cur )
247277 return acc
248278
279+ elements = reduce (concat_elements , self .patterns , [])
249280 elements = reduce (merge_adjecent_text , elements , [])
250281 return FTL .Pattern (elements )
251282
0 commit comments