fix(localize): parse all parts of a translation with nested HTML (#38452)

Previously nested container placeholders (i.e. HTML elements) were
not being fully parsed from translation files. This resulted in bad
translation of messages that contain these placeholders.

Note that this causes the canonical message ID to change for
such messages. Currently all messages generated from
templates use "legacy" message ids that are not affected by
this change, so this fix should not be seen as a breaking change.

Fixes #38422

PR Close #38452
This commit is contained in:
Pete Bacon Darwin 2020-08-16 16:11:30 +01:00 committed by Misko Hevery
parent 8cd4099db9
commit 68a9a01a64
4 changed files with 124 additions and 26 deletions

View File

@ -75,29 +75,9 @@ export class MessageSerializer<T> extends BaseVisitor {
} }
visitContainedNodes(nodes: Node[]): void { visitContainedNodes(nodes: Node[]): void {
const length = nodes.length; this.renderer.startContainer();
let index = 0; visitAll(this, nodes);
while (index < length) { this.renderer.closeContainer();
if (!this.isPlaceholderContainer(nodes[index])) {
const startOfContainedNodes = index;
while (index < length - 1) {
index++;
if (this.isPlaceholderContainer(nodes[index])) {
break;
}
}
if (index - startOfContainedNodes > 1) {
// Only create a container if there are two or more contained Nodes in a row
this.renderer.startContainer();
visitAll(this, nodes.slice(startOfContainedNodes, index - 1));
this.renderer.closeContainer();
}
}
if (index < length) {
nodes[index].visit(this, undefined);
}
index++;
}
} }
visitPlaceholder(name: string, body: string|undefined): void { visitPlaceholder(name: string, body: string|undefined): void {

View File

@ -212,6 +212,46 @@ describe('Xliff1TranslationParser', () => {
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
}); });
it('should extract nested placeholder containers (i.e. nested HTML elements)', () => {
/**
* Source HTML:
*
* ```
* <div i18n>
* translatable <span>element <b>with placeholders</b></span> {{ interpolation}}
* </div>
* ```
*/
const XLIFF = [
`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">`,
` <file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">`,
` <body>`,
` <trans-unit id="9051630253697141670" datatype="html">`,
` <source>translatable <x id="START_TAG_SPAN"/>element <x id="START_BOLD_TEXT"/>with placeholders<x id="CLOSE_BOLD_TEXT"/><x id="CLOSE_TAG_SPAN"/> <x id="INTERPOLATION"/></source>`,
` <target><x id="START_TAG_SPAN"/><x id="INTERPOLATION"/> tnemele<x id="CLOSE_TAG_SPAN"/> elbatalsnart <x id="START_BOLD_TEXT"/>sredlohecalp htiw<x id="CLOSE_BOLD_TEXT"/></target>`,
` <context-group purpose="location">`,
` <context context-type="sourcefile">file.ts</context>`,
` <context context-type="linenumber">3</context>`,
` </context-group>`,
` </trans-unit>`,
` </body>`,
` </file>`,
`</xliff>`,
].join('\n');
const result = doParse('/some/file.xlf', XLIFF);
expect(result.translations[ɵcomputeMsgId(
'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' +
'{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')])
.toEqual(ɵmakeParsedTranslation(
['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [
'START_TAG_SPAN',
'INTERPOLATION',
'CLOSE_TAG_SPAN',
'START_BOLD_TEXT',
'CLOSE_BOLD_TEXT',
]));
});
it('should extract translations with placeholders containing hyphens', () => { it('should extract translations with placeholders containing hyphens', () => {
/** /**
* Source HTML: * Source HTML:

View File

@ -172,13 +172,13 @@ describe(
* Source HTML: * Source HTML:
* *
* ``` * ```
* <div i18n>translatable element <b>>with placeholders</b> {{ interpolation}}</div> * <div i18n>translatable element <b>with placeholders</b> {{ interpolation}}</div>
* ``` * ```
*/ */
const XLIFF = [ const XLIFF = [
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`, `<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`,
` <file original="ng.template" id="ngi18n">`, ` <file original="ng.template" id="ngi18n">`,
` <unit id="5057824347511785081">`, ` <unit id="6949438802869886378">`,
` <notes>`, ` <notes>`,
` <note category="location">file.ts:3</note>`, ` <note category="location">file.ts:3</note>`,
` </notes>`, ` </notes>`,
@ -193,12 +193,58 @@ describe(
const result = doParse('/some/file.xlf', XLIFF); const result = doParse('/some/file.xlf', XLIFF);
expect( expect(
result.translations[ɵcomputeMsgId( result.translations[ɵcomputeMsgId(
'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')]) 'translatable element {$START_BOLD_TEXT}with placeholders{$CLOSE_BOLD_TEXT} {$INTERPOLATION}')])
.toEqual(ɵmakeParsedTranslation( .toEqual(ɵmakeParsedTranslation(
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''], ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT'])); ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
}); });
it('should extract nested placeholder containers (i.e. nested HTML elements)', () => {
/**
* Source HTML:
*
* ```
* <div i18n>
* translatable <span>element <b>with placeholders</b></span> {{ interpolation}}
* </div>
* ```
*/
const XLIFF = [
`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en" trgLang="fr">`,
` <file original="ng.template" id="ngi18n">`,
` <unit id="9051630253697141670">`,
` <notes>`,
` <note category="location">file.ts:3</note>`,
` </notes>`,
` <segment>`,
` <source>translatable <pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="other"` +
` dispStart="&lt;span&gt;" dispEnd="&lt;/span&gt;">element <pc id="1" equivStart="START_BOLD_TEXT" equivEnd=` +
`"CLOSE_BOLD_TEXT" type="fmt" dispStart="&lt;b&gt;" dispEnd="&lt;/b&gt;">with placeholders</pc></pc>` +
` <ph id="2" equiv="INTERPOLATION" disp="{{ interpolation}}"/></source>`,
` <target><pc id="0" equivStart="START_TAG_SPAN" equivEnd="CLOSE_TAG_SPAN" type="fmt" dispStart="&lt;` +
`span&gt;" dispEnd="&lt;/span&gt;"><ph id="2" equiv="INTERPOLATION" disp="{{ interpolation}}"/> tnemele</pc>` +
` elbatalsnart <pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart=` +
`"&lt;b&gt;" dispEnd="&lt;/b&gt;">sredlohecalp htiw</pc></target>`,
` </segment>`,
` </unit>`,
` </file>`,
`</xliff>`,
].join('\n');
const result = doParse('/some/file.xlf', XLIFF);
expect(
result.translations[ɵcomputeMsgId(
'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' +
'{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')])
.toEqual(ɵmakeParsedTranslation(
['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [
'START_TAG_SPAN',
'INTERPOLATION',
'CLOSE_TAG_SPAN',
'START_BOLD_TEXT',
'CLOSE_BOLD_TEXT',
]));
});
it('should extract translations with simple ICU expressions', () => { it('should extract translations with simple ICU expressions', () => {
/** /**
* Source HTML: * Source HTML:

View File

@ -140,6 +140,38 @@ describe('XtbTranslationParser', () => {
ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH'])); ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH']));
}); });
it('should extract nested placeholder containers (i.e. nested HTML elements)', () => {
/**
* Source HTML:
*
* ```
* <div i18n>
* translatable <span>element <b>with placeholders</b></span> {{ interpolation}}
* </div>
* ```
*/
const XLIFF = [
`<?xml version="1.0" encoding="UTF-8"?>`,
`<translationbundle>`,
` <translation id="9051630253697141670">` +
`<ph name="START_TAG_SPAN"/><ph name="INTERPOLATION"/> tnemele<ph name="CLOSE_TAG_SPAN"/> elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>` +
`</translation>`,
`</translationbundle>`,
].join('\n');
const result = doParse('/some/file.xtb', XLIFF);
expect(result.translations[ɵcomputeMsgId(
'translatable {$START_TAG_SPAN}element {$START_BOLD_TEXT}with placeholders' +
'{$CLOSE_BOLD_TEXT}{$CLOSE_TAG_SPAN} {$INTERPOLATION}')])
.toEqual(ɵmakeParsedTranslation(
['', '', ' tnemele', ' elbatalsnart ', 'sredlohecalp htiw', ''], [
'START_TAG_SPAN',
'INTERPOLATION',
'CLOSE_TAG_SPAN',
'START_BOLD_TEXT',
'CLOSE_BOLD_TEXT',
]));
});
it('should extract translations with simple ICU expressions', () => { it('should extract translations with simple ICU expressions', () => {
const XTB = [ const XTB = [
`<?xml version="1.0" encoding="UTF-8" ?>`, `<?xml version="1.0" encoding="UTF-8" ?>`,