Skip to content

Commit ca516c0

Browse files
committed
Bug 1321279 - Part 1 - Read target FTL files before migrations.
Read target Fluent localization files before migrating translations into them from legacy resources. This change also explicitly prohibits specifying Fluent resources as sources of migration. They could only be used with the COPY transform anyways.
1 parent e12bf03 commit ca516c0

20 files changed

+327
-102
lines changed

fluent/migrate/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# coding=utf8
22

33
from .context import MergeContext # noqa: F401
4+
from .errors import ( # noqa: F401
5+
MigrationError, NotSupportedError, UnreadableReferenceError
6+
)
47
from .transforms import ( # noqa: F401
58
Source, COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT
69
)

fluent/migrate/context.py

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def getParser(path):
1818
from .cldr import get_plural_categories
1919
from .transforms import Source
2020
from .merge import merge_resource
21-
from .util import get_message
21+
from .errors import NotSupportedError, UnreadableReferenceError
2222

2323

2424
class MergeContext(object):
@@ -79,6 +79,10 @@ def read_ftl_resource(self, path):
7979
f = codecs.open(path, 'r', 'utf8')
8080
try:
8181
contents = f.read()
82+
except UnicodeDecodeError as err:
83+
logger = logging.getLogger('migrate')
84+
logger.warn('Unable to read file {}: {}'.format(path, err))
85+
raise err
8286
finally:
8387
f.close()
8488

@@ -94,7 +98,7 @@ def read_ftl_resource(self, path):
9498
logger = logging.getLogger('migrate')
9599
for annot in annots:
96100
msg = annot.message
97-
logger.warn(u'Syntax error in {}: {}'.format(path, msg))
101+
logger.warn('Syntax error in {}: {}'.format(path, msg))
98102

99103
return ast
100104

@@ -111,43 +115,40 @@ def add_reference(self, path, realpath=None):
111115
try:
112116
ast = self.read_ftl_resource(fullpath)
113117
except IOError as err:
114-
logger = logging.getLogger('migrate')
115-
logger.error(u'Missing reference file: {}'.format(path))
116-
raise err
118+
error_message = 'Missing reference file: {}'.format(fullpath)
119+
logging.getLogger('migrate').error(error_message)
120+
raise UnreadableReferenceError(error_message)
117121
except UnicodeDecodeError as err:
118-
logger = logging.getLogger('migrate')
119-
logger.error(u'Error reading file {}: {}'.format(path, err))
120-
raise err
122+
error_message = 'Error reading file {}: {}'.format(fullpath, err)
123+
logging.getLogger('migrate').error(error_message)
124+
raise UnreadableReferenceError(error_message)
121125
else:
122126
self.reference_resources[path] = ast
123127

124-
def add_localization(self, path):
125-
"""Add an existing localization resource.
128+
def maybe_add_localization(self, path):
129+
"""Add a localization resource to migrate translations from.
126130
127-
If it's an FTL resource, add an FTL AST. Otherwise, it's a legacy
128-
resource. Use a compare-locales parser to create a dict of (key,
129-
string value) tuples.
131+
Only legacy resources can be added as migration sources. The resource
132+
may be missing on disk.
133+
134+
Uses a compare-locales parser to create a dict of (key, string value)
135+
tuples.
130136
"""
131-
fullpath = os.path.join(self.localization_dir, path)
132-
if fullpath.endswith('.ftl'):
133-
try:
134-
ast = self.read_ftl_resource(fullpath)
135-
except IOError:
136-
logger = logging.getLogger('migrate')
137-
logger.warn(u'Missing localization file: {}'.format(path))
138-
except UnicodeDecodeError as err:
139-
logger = logging.getLogger('migrate')
140-
logger.warn(u'Error reading file {}: {}'.format(path, err))
141-
else:
142-
self.localization_resources[path] = ast
137+
if path.endswith('.ftl'):
138+
error_message = (
139+
'Migrating translations from Fluent files is not supported '
140+
'({})'.format(path))
141+
logging.getLogger('migrate').error(error_message)
142+
raise NotSupportedError(error_message)
143+
144+
try:
145+
fullpath = os.path.join(self.localization_dir, path)
146+
collection = self.read_legacy_resource(fullpath)
147+
except IOError:
148+
logger = logging.getLogger('migrate')
149+
logger.warn('Missing localization file: {}'.format(path))
143150
else:
144-
try:
145-
collection = self.read_legacy_resource(fullpath)
146-
except IOError:
147-
logger = logging.getLogger('migrate')
148-
logger.warn(u'Missing localization file: {}'.format(path))
149-
else:
150-
self.localization_resources[path] = collection
151+
self.localization_resources[path] = collection
151152

152153
def add_transforms(self, path, transforms):
153154
"""Define transforms for path.
@@ -175,17 +176,30 @@ def get_sources(acc, cur):
175176
path_transforms = self.transforms.setdefault(path, [])
176177
path_transforms += transforms
177178

179+
if path not in self.localization_resources:
180+
fullpath = os.path.join(self.localization_dir, path)
181+
try:
182+
ast = self.read_ftl_resource(fullpath)
183+
except IOError:
184+
logger = logging.getLogger('migrate')
185+
logger.info(
186+
'Localization file {} does not exist and '
187+
'it will be created'.format(path))
188+
except UnicodeDecodeError:
189+
logger = logging.getLogger('migrate')
190+
logger.warn(
191+
'Localization file {} will be re-created and some '
192+
'translations might be lost'.format(path))
193+
else:
194+
self.localization_resources[path] = ast
195+
178196
def get_source(self, path, key):
179-
"""Get an entity value from the localized source.
197+
"""Get an entity value from a localized legacy source.
180198
181199
Used by the `Source` transform.
182200
"""
183-
if path.endswith('.ftl'):
184-
resource = self.localization_resources[path]
185-
return get_message(resource.body, key)
186-
else:
187-
resource = self.localization_resources[path]
188-
return resource.get(key, None)
201+
resource = self.localization_resources[path]
202+
return resource.get(key, None)
189203

190204
def merge_changeset(self, changeset=None):
191205
"""Return a generator of FTL ASTs for the changeset.
@@ -200,10 +214,11 @@ def merge_changeset(self, changeset=None):
200214
"""
201215

202216
if changeset is None:
203-
# Merge all known legacy translations.
217+
# Merge all known legacy translations. Used in tests.
204218
changeset = {
205219
(path, key)
206220
for path, strings in self.localization_resources.iteritems()
221+
if not path.endswith('.ftl')
207222
for key in strings.iterkeys()
208223
}
209224

fluent/migrate/errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class MigrationError(ValueError):
2+
pass
3+
4+
5+
class NotSupportedError(MigrationError):
6+
pass
7+
8+
9+
class UnreadableReferenceError(MigrationError):
10+
pass

fluent/migrate/merge.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ def merge_resource(ctx, reference, current, transforms, in_changeset):
1111
"""Transform legacy translations into FTL.
1212
1313
Use the `reference` FTL AST as a template. For each en-US string in the
14-
reference, first check if it's in the currently processed changeset with
15-
`in_changeset`; then check for an existing translation in the current FTL
16-
`localization` or for a migration specification in `transforms`.
14+
reference, first check for an existing translation in the current FTL
15+
`localization` and return early if it's present; then if the string has
16+
a transform defined in the migration specification and if it's in the
17+
currently processed changeset, evaluate the transform.
1718
"""
1819

1920
def merge_body(body):

fluent/migrate/transforms.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from __future__ import unicode_literals
6767

6868
import fluent.syntax.ast as FTL
69+
from .errors import NotSupportedError
6970

7071

7172
def pattern_from_text(value):
@@ -113,6 +114,11 @@ class Source(Transform):
113114
# not be replaced?
114115

115116
def __init__(self, path, key):
117+
if path.endswith('.ftl'):
118+
raise NotSupportedError(
119+
'Migrating translations from Fluent files is not supported '
120+
'({})'.format(path))
121+
116122
self.path = path
117123
self.key = key
118124

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
dnt-description =
6+
Send websites a “Do Not Track” signal
7+
that you don’t want to be tracked
8+
dnt-learn-more = Learn more
9+
dnt-always = Always

tests/migrate/fixtures/pl/existing.ftl

Whitespace-only changes.

tests/migrate/fixtures/pl/privacy.dtd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<!-- This Source Code Form is subject to the terms of the Mozilla Public
2+
- License, v. 2.0. If a copy of the MPL was not distributed with this
3+
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
4+
5+
<!ENTITY doNotTrack.description "Old Description in Polish">
6+
<!ENTITY doNotTrack.learnMore.label "Więcej informacji">
7+
<!ENTITY doNotTrack.always.label "Zawsze">

tests/migrate/fixtures/pl/privacy.ftl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
dnt-description = New Description in Polish

tests/migrate/test_concat.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
class MockContext(unittest.TestCase):
1818
def get_source(self, path, key):
19+
# Ignore path (test.properties) and get translations from self.strings
20+
# defined in setUp.
1921
return self.strings.get(key, None).val
2022

2123

@@ -36,7 +38,7 @@ def test_concat_one(self):
3638
msg = FTL.Message(
3739
FTL.Identifier('hello'),
3840
value=CONCAT(
39-
COPY(self.strings, 'hello'),
41+
COPY('test.properties', 'hello'),
4042
)
4143
)
4244

@@ -51,8 +53,8 @@ def test_concat_two(self):
5153
msg = FTL.Message(
5254
FTL.Identifier('hello'),
5355
value=CONCAT(
54-
COPY(self.strings, 'hello.start'),
55-
COPY(self.strings, 'hello.end'),
56+
COPY('test.properties', 'hello.start'),
57+
COPY('test.properties', 'hello.end'),
5658
)
5759
)
5860

@@ -86,8 +88,8 @@ def test_concat_whitespace_begin(self):
8688
msg = FTL.Message(
8789
FTL.Identifier('hello'),
8890
value=CONCAT(
89-
COPY(self.strings, 'whitespace.begin.start'),
90-
COPY(self.strings, 'whitespace.begin.end'),
91+
COPY('test.properties', 'whitespace.begin.start'),
92+
COPY('test.properties', 'whitespace.begin.end'),
9193
)
9294
)
9395

@@ -103,8 +105,8 @@ def test_concat_whitespace_end(self):
103105
msg = FTL.Message(
104106
FTL.Identifier('hello'),
105107
value=CONCAT(
106-
COPY(self.strings, 'whitespace.end.start'),
107-
COPY(self.strings, 'whitespace.end.end'),
108+
COPY('test.properties', 'whitespace.end.start'),
109+
COPY('test.properties', 'whitespace.end.end'),
108110
)
109111
)
110112

@@ -129,11 +131,11 @@ def test_concat_literal(self):
129131
msg = FTL.Message(
130132
FTL.Identifier('update-failed'),
131133
value=CONCAT(
132-
COPY(self.strings, 'update.failed.start'),
134+
COPY('test.properties', 'update.failed.start'),
133135
FTL.TextElement('<a>'),
134-
COPY(self.strings, 'update.failed.linkText'),
136+
COPY('test.properties', 'update.failed.linkText'),
135137
FTL.TextElement('</a>'),
136-
COPY(self.strings, 'update.failed.end'),
138+
COPY('test.properties', 'update.failed.end'),
137139
)
138140
)
139141

@@ -157,9 +159,9 @@ def test_concat_replace(self):
157159
msg = FTL.Message(
158160
FTL.Identifier('channel-desc'),
159161
value=CONCAT(
160-
COPY(self.strings, 'channel.description.start'),
162+
COPY('test.properties', 'channel.description.start'),
161163
FTL.Placeable(EXTERNAL_ARGUMENT('channelname')),
162-
COPY(self.strings, 'channel.description.end'),
164+
COPY('test.properties', 'channel.description.end'),
163165
)
164166
)
165167

@@ -187,7 +189,7 @@ def test_concat_replace(self):
187189
FTL.Identifier('community'),
188190
value=CONCAT(
189191
REPLACE(
190-
self.strings,
192+
'test.properties',
191193
'community.start',
192194
{
193195
'&brandShortName;': MESSAGE_REFERENCE(
@@ -197,7 +199,7 @@ def test_concat_replace(self):
197199
),
198200
FTL.TextElement('<a>'),
199201
REPLACE(
200-
self.strings,
202+
'test.properties',
201203
'community.mozillaLink',
202204
{
203205
'&vendorShortName;': MESSAGE_REFERENCE(
@@ -206,11 +208,11 @@ def test_concat_replace(self):
206208
}
207209
),
208210
FTL.TextElement('</a>'),
209-
COPY(self.strings, 'community.middle'),
211+
COPY('test.properties', 'community.middle'),
210212
FTL.TextElement('<a>'),
211-
COPY(self.strings, 'community.creditsLink'),
213+
COPY('test.properties', 'community.creditsLink'),
212214
FTL.TextElement('</a>'),
213-
COPY(self.strings, 'community.end')
215+
COPY('test.properties', 'community.end')
214216
)
215217
)
216218

0 commit comments

Comments
 (0)