Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fc4c3c3

Browse files
petebacondarwinmatsko
authored andcommitted
fix(localize): merge translation from all XLIFF <file> elements (#35936)
XLIFF translation files can contain multiple `<file>` elements, each of which contains translations. In ViewEngine all these translations are merged into a single translation bundle. Previously in Ivy only the translations from the last `<file>` element were being loaded. Now all the translations from each `<file>` are merged into a single translation bundle. Fixes #35839 PR Close #35936
1 parent 1882451 commit fc4c3c3

File tree

4 files changed

+283
-35
lines changed

4 files changed

+283
-35
lines changed

packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,24 @@ export class Xliff1TranslationParser implements TranslationParser<XmlTranslation
5959
ParseErrorLevel.WARNING);
6060
}
6161

62-
const bundle = {
63-
locale: getAttribute(files[0], 'target-language'),
64-
translations: {}, diagnostics,
65-
};
62+
const bundle: ParsedTranslationBundle = {locale: undefined, translations: {}, diagnostics};
6663
const translationVisitor = new XliffTranslationVisitor();
67-
visitAll(translationVisitor, files[0].children, bundle);
64+
const localesFound = new Set<string>();
65+
for (const file of files) {
66+
const locale = getAttribute(file, 'target-language');
67+
if (locale !== undefined) {
68+
localesFound.add(locale);
69+
bundle.locale = locale;
70+
}
71+
visitAll(translationVisitor, file.children, bundle);
72+
}
73+
74+
if (localesFound.size > 1) {
75+
addParseDiagnostic(
76+
diagnostics, element.sourceSpan,
77+
`More than one locale found in translation file: ${JSON.stringify(Array.from(localesFound))}. Using "${bundle.locale}"`,
78+
ParseErrorLevel.WARNING);
79+
}
6880

6981
return bundle;
7082
}

packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,6 @@ export class Xliff2TranslationParser implements TranslationParser<XmlTranslation
4040
const diagnostics = new Diagnostics();
4141
errors.forEach(e => addParseError(diagnostics, e));
4242

43-
if (element.children.length === 0) {
44-
addParseDiagnostic(
45-
diagnostics, element.sourceSpan, 'Missing expected <file> element',
46-
ParseErrorLevel.WARNING);
47-
return {locale: undefined, translations: {}, diagnostics};
48-
}
49-
5043
const locale = getAttribute(element, 'trgLang');
5144
const files = element.children.filter(isFileElement);
5245
if (files.length === 0) {
@@ -61,8 +54,9 @@ export class Xliff2TranslationParser implements TranslationParser<XmlTranslation
6154

6255
const bundle = {locale, translations: {}, diagnostics};
6356
const translationVisitor = new Xliff2TranslationVisitor();
64-
visitAll(translationVisitor, files[0].children, {bundle});
65-
57+
for (const file of files) {
58+
visitAll(translationVisitor, file.children, {bundle});
59+
}
6660
return bundle;
6761
}
6862

packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts

Lines changed: 156 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,34 @@ describe('Xliff1TranslationParser', () => {
3131
});
3232

3333
describe('parse() [without hint]', () => {
34-
it('should extract the locale from the file contents', () => {
35-
const XLIFF = `
34+
it('should extract the locale from the last `<file>` element to contain a `target-language` attribute',
35+
() => {
36+
const XLIFF = `
3637
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
38+
<file source-language="en" datatype="plaintext" original="ng2.template">
39+
<body></body>
40+
</file>
3741
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
38-
<body>
39-
</body>
42+
<body></body>
43+
</file>
44+
<file source-language="en" datatype="plaintext" original="ng2.template">
45+
<body></body>
46+
</file>
47+
<file source-language="en" target-language="de" datatype="plaintext" original="ng2.template">
48+
<body></body>
49+
</file>
50+
<file source-language="en" datatype="plaintext" original="ng2.template">
51+
<body></body>
4052
</file>
4153
</xliff>`;
42-
const parser = new Xliff1TranslationParser();
43-
const result = parser.parse('/some/file.xlf', XLIFF);
44-
expect(result.locale).toEqual('fr');
45-
});
54+
const parser = new Xliff1TranslationParser();
55+
const hint = parser.canParse('/some/file.xlf', XLIFF);
56+
if (!hint) {
57+
return fail('expected XLIFF to be valid');
58+
}
59+
const result = parser.parse('/some/file.xlf', XLIFF, hint);
60+
expect(result.locale).toEqual('de');
61+
});
4662

4763
it('should return an undefined locale if there is no locale in the file', () => {
4864
const XLIFF = `
@@ -437,6 +453,58 @@ describe('Xliff1TranslationParser', () => {
437453
.toEqual(ɵmakeParsedTranslation(['Weiter']));
438454
});
439455

456+
it('should merge messages from each `<file>` element', () => {
457+
/**
458+
* Source HTML:
459+
*
460+
* ```
461+
* <div i18n>translatable attribute</div>
462+
* ```
463+
464+
* ```
465+
* <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
466+
* ```
467+
*/
468+
const XLIFF = `
469+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
470+
<file source-language="en" target-language="fr" datatype="plaintext" original="file-1">
471+
<body>
472+
<trans-unit id="1933478729560469763" datatype="html">
473+
<source>translatable attribute</source>
474+
<target>etubirtta elbatalsnart</target>
475+
<context-group purpose="location">
476+
<context context-type="sourcefile">file.ts</context>
477+
<context context-type="linenumber">1</context>
478+
</context-group>
479+
</trans-unit>
480+
</body>
481+
</file>
482+
<file source-language="en" target-language="fr" datatype="plaintext" original="file-2">
483+
<body>
484+
<trans-unit id="5057824347511785081" datatype="html">
485+
<source>translatable element <x id="START_BOLD_TEXT" ctype="b"/>with placeholders<x id="CLOSE_BOLD_TEXT" ctype="b"/> <x id="INTERPOLATION"/></source>
486+
<target><x id="INTERPOLATION"/> tnemele elbatalsnart <x id="START_BOLD_TEXT" ctype="x-b"/>sredlohecalp htiw<x id="CLOSE_BOLD_TEXT" ctype="x-b"/></target>
487+
<context-group purpose="location">
488+
<context context-type="sourcefile">file.ts</context>
489+
<context context-type="linenumber">2</context>
490+
</context-group>
491+
</trans-unit>
492+
</body>
493+
</file>
494+
</xliff>`;
495+
const parser = new Xliff1TranslationParser();
496+
const result = parser.parse('/some/file.xlf', XLIFF);
497+
498+
expect(result.translations[ɵcomputeMsgId('translatable attribute')])
499+
.toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
500+
expect(
501+
result.translations[ɵcomputeMsgId(
502+
'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
503+
.toEqual(ɵmakeParsedTranslation(
504+
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
505+
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
506+
});
507+
440508
describe('[structure errors]', () => {
441509
it('should throw when a trans-unit has no translation', () => {
442510
const XLIFF = `
@@ -547,22 +615,34 @@ describe('Xliff1TranslationParser', () => {
547615
});
548616

549617
describe('parse() [with hint]', () => {
550-
it('should extract the locale from the file contents', () => {
551-
const XLIFF = `
618+
it('should extract the locale from the last `<file>` element to contain a `target-language` attribute',
619+
() => {
620+
const XLIFF = `
552621
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
622+
<file source-language="en" datatype="plaintext" original="ng2.template">
623+
<body></body>
624+
</file>
553625
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
554-
<body>
555-
</body>
626+
<body></body>
627+
</file>
628+
<file source-language="en" datatype="plaintext" original="ng2.template">
629+
<body></body>
630+
</file>
631+
<file source-language="en" target-language="de" datatype="plaintext" original="ng2.template">
632+
<body></body>
633+
</file>
634+
<file source-language="en" datatype="plaintext" original="ng2.template">
635+
<body></body>
556636
</file>
557637
</xliff>`;
558-
const parser = new Xliff1TranslationParser();
559-
const hint = parser.canParse('/some/file.xlf', XLIFF);
560-
if (!hint) {
561-
return fail('expected XLIFF to be valid');
562-
}
563-
const result = parser.parse('/some/file.xlf', XLIFF, hint);
564-
expect(result.locale).toEqual('fr');
565-
});
638+
const parser = new Xliff1TranslationParser();
639+
const hint = parser.canParse('/some/file.xlf', XLIFF);
640+
if (!hint) {
641+
return fail('expected XLIFF to be valid');
642+
}
643+
const result = parser.parse('/some/file.xlf', XLIFF, hint);
644+
expect(result.locale).toEqual('de');
645+
});
566646

567647
it('should return an undefined locale if there is no locale in the file', () => {
568648
const XLIFF = `
@@ -1005,6 +1085,62 @@ describe('Xliff1TranslationParser', () => {
10051085
.toEqual(ɵmakeParsedTranslation(['Weiter']));
10061086
});
10071087

1088+
it('should merge messages from each `<file>` element', () => {
1089+
/**
1090+
* Source HTML:
1091+
*
1092+
* ```
1093+
* <div i18n>translatable attribute</div>
1094+
* ```
1095+
*
1096+
* ```
1097+
* <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
1098+
* ```
1099+
*/
1100+
const XLIFF = `
1101+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
1102+
<file source-language="en" target-language="fr" datatype="plaintext" original="file-1">
1103+
<body>
1104+
<trans-unit id="1933478729560469763" datatype="html">
1105+
<source>translatable attribute</source>
1106+
<target>etubirtta elbatalsnart</target>
1107+
<context-group purpose="location">
1108+
<context context-type="sourcefile">file.ts</context>
1109+
<context context-type="linenumber">1</context>
1110+
</context-group>
1111+
</trans-unit>
1112+
</body>
1113+
</file>
1114+
<file source-language="en" target-language="fr" datatype="plaintext" original="file-2">
1115+
<body>
1116+
<trans-unit id="5057824347511785081" datatype="html">
1117+
<source>translatable element <x id="START_BOLD_TEXT" ctype="b"/>with placeholders<x id="CLOSE_BOLD_TEXT" ctype="b"/> <x id="INTERPOLATION"/></source>
1118+
<target><x id="INTERPOLATION"/> tnemele elbatalsnart <x id="START_BOLD_TEXT" ctype="x-b"/>sredlohecalp htiw<x id="CLOSE_BOLD_TEXT" ctype="x-b"/></target>
1119+
<context-group purpose="location">
1120+
<context context-type="sourcefile">file.ts</context>
1121+
<context context-type="linenumber">2</context>
1122+
</context-group>
1123+
</trans-unit>
1124+
</body>
1125+
</file>
1126+
</xliff>`;
1127+
const parser = new Xliff1TranslationParser();
1128+
const hint = parser.canParse('/some/file.xlf', XLIFF);
1129+
if (!hint) {
1130+
return fail('expected XLIFF to be valid');
1131+
}
1132+
const result = parser.parse('/some/file.xlf', XLIFF, hint);
1133+
1134+
expect(result.translations[ɵcomputeMsgId('translatable attribute')])
1135+
.toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
1136+
expect(
1137+
result.translations[ɵcomputeMsgId(
1138+
'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
1139+
.toEqual(ɵmakeParsedTranslation(
1140+
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
1141+
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
1142+
});
1143+
10081144
describe('[structure errors]', () => {
10091145
it('should provide a diagnostic error when a trans-unit has no translation', () => {
10101146
const XLIFF = `

packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('Xliff2TranslationParser', () => {
8787
* Source HTML:
8888
*
8989
* ```
90-
* <div i18n>translatable element <b>>with placeholders</b> {{ interpolation}}</div>
90+
* <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
9191
* ```
9292
*/
9393
const XLIFF = `
@@ -373,6 +373,57 @@ describe('Xliff2TranslationParser', () => {
373373
.toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
374374
});
375375

376+
it('should merge messages from each `<file>` element', () => {
377+
/**
378+
* Source HTML:
379+
*
380+
* ```
381+
* <div i18n>translatable attribute</div>
382+
* ```
383+
*
384+
* ```
385+
* <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
386+
* ```
387+
*/
388+
const XLIFF = `
389+
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">
390+
<file original="ng.template" id="file-1">
391+
<unit id="1933478729560469763">
392+
<notes>
393+
<note category="location">file.ts:2</note>
394+
</notes>
395+
<segment>
396+
<source>translatable attribute</source>
397+
<target>etubirtta elbatalsnart</target>
398+
</segment>
399+
</unit>
400+
</file>
401+
<file original="ng.template" id="file-2">
402+
<unit id="5057824347511785081">
403+
<notes>
404+
<note category="location">file.ts:3</note>
405+
</notes>
406+
<segment>
407+
<source>translatable element <pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="&lt;b&gt;" dispEnd="&lt;/b&gt;">with placeholders</pc> <ph id="1" equiv="INTERPOLATION" disp="{{ interpolation}}"/></source>
408+
<target><ph id="1" equiv="INTERPOLATION" disp="{{ interpolation}}"/> tnemele elbatalsnart <pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="&lt;b&gt;" dispEnd="&lt;/b&gt;">sredlohecalp htiw</pc></target>
409+
</segment>
410+
</unit>
411+
</file>
412+
</xliff>`;
413+
const parser = new Xliff2TranslationParser();
414+
const result = parser.parse('/some/file.xlf', XLIFF);
415+
416+
expect(result.translations[ɵcomputeMsgId('translatable attribute', '')])
417+
.toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
418+
expect(
419+
result.translations[ɵcomputeMsgId(
420+
'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
421+
.toEqual(ɵmakeParsedTranslation(
422+
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
423+
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
424+
425+
});
426+
376427
describe('[structure errors]', () => {
377428
it('should throw when a trans-unit has no translation', () => {
378429
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
@@ -866,6 +917,61 @@ describe('Xliff2TranslationParser', () => {
866917
.toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
867918
});
868919

920+
it('should merge messages from each `<file>` element', () => {
921+
/**
922+
* Source HTML:
923+
*
924+
* ```
925+
* <div i18n>translatable attribute</div>
926+
* ```
927+
*
928+
* ```
929+
* <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
930+
* ```
931+
*/
932+
const XLIFF = `
933+
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">
934+
<file original="ng.template" id="file-1">
935+
<unit id="1933478729560469763">
936+
<notes>
937+
<note category="location">file.ts:2</note>
938+
</notes>
939+
<segment>
940+
<source>translatable attribute</source>
941+
<target>etubirtta elbatalsnart</target>
942+
</segment>
943+
</unit>
944+
</file>
945+
<file original="ng.template" id="file-2">
946+
<unit id="5057824347511785081">
947+
<notes>
948+
<note category="location">file.ts:3</note>
949+
</notes>
950+
<segment>
951+
<source>translatable element <pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="&lt;b&gt;" dispEnd="&lt;/b&gt;">with placeholders</pc> <ph id="1" equiv="INTERPOLATION" disp="{{ interpolation}}"/></source>
952+
<target><ph id="1" equiv="INTERPOLATION" disp="{{ interpolation}}"/> tnemele elbatalsnart <pc id="0" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart="&lt;b&gt;" dispEnd="&lt;/b&gt;">sredlohecalp htiw</pc></target>
953+
</segment>
954+
</unit>
955+
</file>
956+
</xliff>`;
957+
const parser = new Xliff2TranslationParser();
958+
const hint = parser.canParse('/some/file.xlf', XLIFF);
959+
if (!hint) {
960+
return fail('expected XLIFF to be valid');
961+
}
962+
const result = parser.parse('/some/file.xlf', XLIFF, hint);
963+
964+
expect(result.translations[ɵcomputeMsgId('translatable attribute', '')])
965+
.toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
966+
expect(
967+
result.translations[ɵcomputeMsgId(
968+
'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
969+
.toEqual(ɵmakeParsedTranslation(
970+
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
971+
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
972+
973+
});
974+
869975
describe('[structure errors]', () => {
870976
it('should provide a diagnostic error when a trans-unit has no translation', () => {
871977
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>

0 commit comments

Comments
 (0)