Skip to content

Commit e208161

Browse files
authored
Never migrate partial translations (#44)
Partial translations may break the AST because they produce `TextElements` with `None` values. For now, explicitly skip any transforms which depend on at least one missing legacy string. In the future we might be able to allow partial migrations in some situations, like migrating a subset of attributes of a message. See https://bugzilla.mozilla.org/show_bug.cgi?id=1321271.
1 parent b70c841 commit e208161

File tree

4 files changed

+267
-23
lines changed

4 files changed

+267
-23
lines changed

fluent/migrate/context.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,15 @@ def merge_changeset(self, changeset=None):
285285
286286
The input data must be configured earlier using the `add_*` methods.
287287
if given, `changeset` must be a set of (path, key) tuples describing
288-
which legacy translations are to be merged.
288+
which legacy translations are to be merged. If `changeset` is None,
289+
all legacy translations will be allowed to be migrated in a single
290+
changeset.
291+
292+
The inner `in_changeset` function is used to determine if a message
293+
should be migrated for the given changeset. It compares the legacy
294+
dependencies of the transform defined for the message with legacy
295+
translations available in the changeset. If all dependencies are
296+
present, the message will be migrated.
289297
290298
Given `changeset`, return a dict whose keys are resource paths and
291299
values are `FTL.Resource` instances. The values will also be used to
@@ -306,10 +314,18 @@ def merge_changeset(self, changeset=None):
306314
transforms = self.transforms.get(path, [])
307315

308316
def in_changeset(ident):
309-
"""Check if entity should be merged.
317+
"""Check if a message should be migrated.
318+
319+
A message will be migrated only if all of its dependencies
320+
are present in the currently processed changeset.
321+
322+
If a transform defined for this message points to a missing
323+
legacy translation, this message will not be merged. The
324+
missing legacy dependency won't be present in the changeset.
310325
311-
If at least one dependency of the entity is in the current
312-
set of changeset, merge it.
326+
This also means that partially translated messages (e.g.
327+
constructed from two legacy strings out of which only one is
328+
avaiable) will never be migrated.
313329
"""
314330
message_deps = self.dependencies.get((path, ident), None)
315331

@@ -324,9 +340,11 @@ def in_changeset(ident):
324340
if len(message_deps) == 0:
325341
return True
326342

327-
# If the intersection of the dependencies and the current
328-
# changeset is non-empty, merge this message.
329-
return message_deps & changeset
343+
# Make sure all the dependencies are present in the current
344+
# changeset. Partial migrations are not currently supported.
345+
# See https://bugzilla.mozilla.org/show_bug.cgi?id=1321271
346+
available_deps = message_deps & changeset
347+
return message_deps == available_deps
330348

331349
# Merge legacy translations with the existing ones using the
332350
# reference as a template.

tests/migrate/test_context.py

Lines changed: 221 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
EmptyLocalizationError, NotSupportedError, UnreadableReferenceError)
1717
from fluent.migrate.util import ftl, ftl_resource_to_json, to_json
1818
from fluent.migrate.context import MergeContext
19-
from fluent.migrate.transforms import COPY
19+
from fluent.migrate.transforms import CONCAT, COPY
2020

2121

2222
def here(*parts):
@@ -251,7 +251,7 @@ def test_missing_reference_file(self):
251251

252252

253253
@unittest.skipUnless(compare_locales, 'compare-locales requried')
254-
class TestEmptyLocalization(unittest.TestCase):
254+
class TestMissingLocalizationFiles(unittest.TestCase):
255255
def setUp(self):
256256
# Silence all logging.
257257
logging.disable(logging.CRITICAL)
@@ -266,7 +266,40 @@ def tearDown(self):
266266
# Resume logging.
267267
logging.disable(logging.NOTSET)
268268

269-
def test_all_localization_missing(self):
269+
def test_missing_file(self):
270+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
271+
FTL.Message(
272+
id=FTL.Identifier('title'),
273+
value=COPY(
274+
'aboutDownloads.dtd',
275+
'aboutDownloads.title'
276+
)
277+
),
278+
FTL.Message(
279+
id=FTL.Identifier('header'),
280+
value=COPY(
281+
'missing.dtd',
282+
'missing'
283+
)
284+
),
285+
])
286+
287+
expected = {
288+
'aboutDownloads.ftl': ftl_resource_to_json('''
289+
# This Source Code Form is subject to the terms of the Mozilla Public
290+
# License, v. 2.0. If a copy of the MPL was not distributed with this
291+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
292+
293+
title = Pobrane pliki
294+
''')
295+
}
296+
297+
self.assertDictEqual(
298+
to_json(self.ctx.merge_changeset()),
299+
expected
300+
)
301+
302+
def test_all_files_missing(self):
270303
pattern = ('No localization files were found')
271304
with self.assertRaisesRegexp(EmptyLocalizationError, pattern):
272305
self.ctx.add_transforms('existing.ftl', 'existing.ftl', [
@@ -281,7 +314,9 @@ def test_all_localization_missing(self):
281314

282315

283316
@unittest.skipUnless(compare_locales, 'compare-locales requried')
284-
class TestIncompleteLocalization(unittest.TestCase):
317+
class TestMissingLocalizationStrings(unittest.TestCase):
318+
maxDiff = None
319+
285320
def setUp(self):
286321
# Silence all logging.
287322
logging.disable(logging.CRITICAL)
@@ -296,20 +331,116 @@ def tearDown(self):
296331
# Resume logging.
297332
logging.disable(logging.NOTSET)
298333

299-
def test_missing_localization_file(self):
300-
self.ctx.add_transforms('existing.ftl', 'existing.ftl', [
334+
def test_missing_string_in_simple_value(self):
335+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
301336
FTL.Message(
302-
id=FTL.Identifier('foo'),
337+
id=FTL.Identifier('title'),
303338
value=COPY(
304-
'existing.dtd',
305-
'foo'
339+
'aboutDownloads.dtd',
340+
'missing'
306341
)
307342
),
343+
])
344+
345+
self.assertDictEqual(
346+
to_json(self.ctx.merge_changeset()),
347+
{}
348+
)
349+
350+
def test_missing_string_in_only_variant(self):
351+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
308352
FTL.Message(
309-
id=FTL.Identifier('bar'),
310-
value=COPY(
311-
'missing.dtd',
312-
'bar'
353+
id=FTL.Identifier('title'),
354+
value=CONCAT(
355+
FTL.SelectExpression(
356+
expression=FTL.CallExpression(
357+
callee=FTL.Identifier('PLATFORM')
358+
),
359+
variants=[
360+
FTL.Variant(
361+
key=FTL.VariantName('other'),
362+
default=True,
363+
value=COPY(
364+
'aboutDownloads.dtd',
365+
'missing'
366+
)
367+
),
368+
]
369+
),
370+
)
371+
),
372+
])
373+
374+
self.assertDictEqual(
375+
to_json(self.ctx.merge_changeset()),
376+
{}
377+
)
378+
379+
def test_missing_string_in_all_variants(self):
380+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
381+
FTL.Message(
382+
id=FTL.Identifier('title'),
383+
value=CONCAT(
384+
FTL.SelectExpression(
385+
expression=FTL.CallExpression(
386+
callee=FTL.Identifier('PLATFORM')
387+
),
388+
variants=[
389+
FTL.Variant(
390+
key=FTL.VariantName('windows'),
391+
default=False,
392+
value=COPY(
393+
'aboutDownloads.dtd',
394+
'missing.windows'
395+
)
396+
),
397+
FTL.Variant(
398+
key=FTL.VariantName('other'),
399+
default=True,
400+
value=COPY(
401+
'aboutDownloads.dtd',
402+
'missing.other'
403+
)
404+
),
405+
]
406+
),
407+
)
408+
),
409+
])
410+
411+
self.assertDictEqual(
412+
to_json(self.ctx.merge_changeset()),
413+
{}
414+
)
415+
416+
def test_missing_string_in_one_of_variants(self):
417+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
418+
FTL.Message(
419+
id=FTL.Identifier('title'),
420+
value=CONCAT(
421+
FTL.SelectExpression(
422+
expression=FTL.CallExpression(
423+
callee=FTL.Identifier('PLATFORM')
424+
),
425+
variants=[
426+
FTL.Variant(
427+
key=FTL.VariantName('windows'),
428+
default=False,
429+
value=COPY(
430+
'aboutDownloads.dtd',
431+
'aboutDownloads.title'
432+
)
433+
),
434+
FTL.Variant(
435+
key=FTL.VariantName('other'),
436+
default=True,
437+
value=COPY(
438+
'aboutDownloads.dtd',
439+
'missing.other'
440+
)
441+
),
442+
]
443+
),
313444
)
314445
),
315446
])
@@ -319,6 +450,83 @@ def test_missing_localization_file(self):
319450
{}
320451
)
321452

453+
def test_missing_string_in_only_attribute(self):
454+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
455+
FTL.Message(
456+
id=FTL.Identifier('title'),
457+
attributes=[
458+
FTL.Attribute(
459+
FTL.Identifier('one'),
460+
COPY(
461+
'aboutDownloads.dtd',
462+
'missing'
463+
)
464+
),
465+
]
466+
),
467+
])
468+
469+
self.assertDictEqual(
470+
to_json(self.ctx.merge_changeset()),
471+
{}
472+
)
473+
474+
def test_missing_string_in_all_attributes(self):
475+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
476+
FTL.Message(
477+
id=FTL.Identifier('title'),
478+
attributes=[
479+
FTL.Attribute(
480+
FTL.Identifier('one'),
481+
COPY(
482+
'aboutDownloads.dtd',
483+
'missing.one'
484+
)
485+
),
486+
FTL.Attribute(
487+
FTL.Identifier('two'),
488+
COPY(
489+
'aboutDownloads.dtd',
490+
'missing.two'
491+
)
492+
),
493+
]
494+
),
495+
])
496+
497+
self.assertDictEqual(
498+
to_json(self.ctx.merge_changeset()),
499+
{}
500+
)
501+
502+
def test_missing_string_in_one_of_attributes(self):
503+
self.ctx.add_transforms('aboutDownloads.ftl', 'aboutDownloads.ftl', [
504+
FTL.Message(
505+
id=FTL.Identifier('title'),
506+
attributes=[
507+
FTL.Attribute(
508+
FTL.Identifier('title'),
509+
COPY(
510+
'aboutDownloads.dtd',
511+
'aboutDownloads.title'
512+
)
513+
),
514+
FTL.Attribute(
515+
FTL.Identifier('missing'),
516+
COPY(
517+
'aboutDownloads.dtd',
518+
'missing'
519+
)
520+
),
521+
]
522+
),
523+
])
524+
525+
self.assertDictEqual(
526+
to_json(self.ctx.merge_changeset()),
527+
{}
528+
)
529+
322530

323531
@unittest.skipUnless(compare_locales, 'compare-locales requried')
324532
class TestExistingTarget(unittest.TestCase):
@@ -360,7 +568,6 @@ def test_existing_target_ftl_missing_string(self):
360568
''')
361569
}
362570

363-
self.maxDiff = None
364571
self.assertDictEqual(
365572
to_json(self.ctx.merge_changeset()),
366573
expected
@@ -399,7 +606,6 @@ def test_existing_target_ftl_existing_string(self):
399606
''')
400607
}
401608

402-
self.maxDiff = None
403609
self.assertDictEqual(
404610
to_json(self.ctx.merge_changeset()),
405611
expected

tests/migrate/test_context_real_examples.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ def test_merge_context_some_messages(self):
281281

282282
@unittest.skipUnless(compare_locales, 'compare-locales requried')
283283
class TestMergeAboutDialog(unittest.TestCase):
284+
maxDiff = None
285+
284286
def setUp(self):
285287
self.ctx = MergeContext(
286288
lang='pl',
@@ -363,6 +365,8 @@ def test_merge_context_all_messages(self):
363365
def test_merge_context_some_messages(self):
364366
changeset = {
365367
('aboutDialog.dtd', 'update.failed.start'),
368+
('aboutDialog.dtd', 'update.failed.linkText'),
369+
('aboutDialog.dtd', 'update.failed.end'),
366370
}
367371

368372
expected = {
@@ -379,3 +383,14 @@ def test_merge_context_some_messages(self):
379383
to_json(self.ctx.merge_changeset(changeset)),
380384
expected
381385
)
386+
387+
def test_merge_context_too_few_messages(self):
388+
changeset = {
389+
('aboutDialog.dtd', 'update.failed.start'),
390+
('aboutDialog.dtd', 'update.failed.linkText'),
391+
}
392+
393+
self.assertDictEqual(
394+
to_json(self.ctx.merge_changeset(changeset)),
395+
{}
396+
)

0 commit comments

Comments
 (0)