Skip to content

Commit a5a2cd8

Browse files
authored
Rename Transforms (#11)
The following Transforms have been renamed. Transforms that inherit from Source: LITERAL_FROM → COPY REPLACE_FROM → REPLACE PLURALS_FROM → PLURALS Transforms which take text as input: REPLACE → REPLACE_IN_TEXT LITERAL → (removed) PLURALS → (removed) The following helpers have been added. LITERAL EXTERNAL_ARGUMENT MESSAGE_REFERENCE They take a string argument and immediately return a corresponding FTL AST node. (As opposed to Transforms which return AST nodes only when they are evaluated by a MergeContext.)
1 parent 01998d8 commit a5a2cd8

14 files changed

+375
-296
lines changed

fluent/migrate/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from .context import MergeContext # noqa: F401
44
from .transforms import ( # noqa: F401
5-
CONCAT, EXTERNAL, LITERAL, LITERAL_FROM, PLURALS, PLURALS_FROM, REPLACE,
6-
REPLACE_FROM, SOURCE
5+
Source, COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT
6+
)
7+
from .helpers import ( # noqa: F401
8+
LITERAL, EXTERNAL_ARGUMENT, MESSAGE_REFERENCE
79
)
810
from .changesets import convert_blame_to_changesets # noqa: F401

fluent/migrate/context.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def getParser(path):
1616
raise RuntimeError('compare-locales required')
1717

1818
from .cldr import get_plural_categories
19-
from .transforms import SOURCE
19+
from .transforms import Source
2020
from .merge import merge_resource
2121
from .util import get_message
2222

@@ -39,8 +39,10 @@ class MergeContext(object):
3939
into FTL and merged into the existing FTL files for this language.
4040
4141
- A list of `FTL.Message` objects some of whose nodes are special
42-
operation nodes: CONCAT, EXTERNAL, LITERAL, LITERAL_FROM, PLURALS,
43-
PLURALS_FROM, REPLACE, REPLACE_FROM, SOURCE.
42+
helper or transform nodes:
43+
44+
helpers: LITERAL, EXTERNAL_ARGUMENT, MESSAGE_REFERENCE
45+
transforms: COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT
4446
"""
4547

4648
def __init__(self, lang, reference_dir, localization_dir):
@@ -65,7 +67,7 @@ def __init__(self, lang, reference_dir, localization_dir):
6567
self.reference_resources = {}
6668
self.localization_resources = {}
6769

68-
# An iterable of `FTL.Entity` objects some of whose nodes can be the
70+
# An iterable of `FTL.Message` objects some of whose nodes can be the
6971
# transform operations.
7072
self.transforms = {}
7173

@@ -157,16 +159,16 @@ def add_transforms(self, path, transforms):
157159
`merge_changeset` is called, at which point they are evaluated to real
158160
FTL nodes with migrated translations.
159161
160-
Each transform is scanned for `SOURCE` nodes which will be used to
162+
Each transform is scanned for `Source` nodes which will be used to
161163
build the list of dependencies for the transformed message.
162164
"""
163165
def get_sources(acc, cur):
164-
if isinstance(cur, SOURCE):
166+
if isinstance(cur, Source):
165167
acc.add((cur.path, cur.key))
166168
return acc
167169

168170
for node in transforms:
169-
# Scan `node` for `SOURCE` nodes and collect the information they
171+
# Scan `node` for `Source` nodes and collect the information they
170172
# store into a set of dependencies.
171173
dependencies = fold(get_sources, node, set())
172174
# Set these sources as dependencies for the current transform.
@@ -178,7 +180,7 @@ def get_sources(acc, cur):
178180
def get_source(self, path, key):
179181
"""Get an entity value from the localized source.
180182
181-
Used by the `SOURCE` transform.
183+
Used by the `Source` transform.
182184
"""
183185
if path.endswith('.ftl'):
184186
resource = self.localization_resources[path]

fluent/migrate/helpers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding=utf8
2+
"""Fluent AST helpers.
3+
4+
The functions defined in this module offer a shorthand for defining common AST
5+
nodes.
6+
7+
They take a string argument and immediately return a corresponding AST node.
8+
(As opposed to Transforms which are AST nodes on their own and only return the
9+
migrated AST nodes when they are evaluated by a MergeContext.) """
10+
11+
from __future__ import unicode_literals
12+
13+
import fluent.syntax.ast as FTL
14+
15+
16+
def LITERAL(value):
17+
"""Create a Pattern with a single TextElement."""
18+
elements = [FTL.TextElement(value)]
19+
return FTL.Pattern(elements)
20+
21+
22+
def EXTERNAL_ARGUMENT(name):
23+
"""Create an ExternalArgument expression."""
24+
25+
return FTL.ExternalArgument(
26+
id=FTL.Identifier(name)
27+
)
28+
29+
30+
def MESSAGE_REFERENCE(name):
31+
"""Create a MessageReference expression."""
32+
33+
return FTL.MessageReference(
34+
id=FTL.Identifier(name)
35+
)

fluent/migrate/transforms.py

Lines changed: 100 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,72 @@
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+
266
from __future__ import unicode_literals
367

468
import fluent.syntax.ast as FTL
69+
from .helpers import LITERAL
570

671

772
def 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-
225246
class 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

Comments
 (0)