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,30 +75,10 @@ export class MessageSerializer<T> extends BaseVisitor {
}
visitContainedNodes(nodes: Node[]): void {
const length = nodes.length;
let index = 0;
while (index < length) {
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));
visitAll(this, nodes);
this.renderer.closeContainer();
}
}
if (index < length) {
nodes[index].visit(this, undefined);
}
index++;
}
}
visitPlaceholder(name: string, body: string|undefined): void {
this.renderer.placeholder(name, body);

View File

@ -212,6 +212,46 @@ describe('Xliff1TranslationParser', () => {
['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', () => {
/**
* Source HTML:

View File

@ -172,13 +172,13 @@ describe(
* Source HTML:
*
* ```
* <div i18n>translatable element <b>>with placeholders</b> {{ interpolation}}</div>
* <div i18n>translatable element <b>with placeholders</b> {{ 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="5057824347511785081">`,
` <unit id="6949438802869886378">`,
` <notes>`,
` <note category="location">file.ts:3</note>`,
` </notes>`,
@ -193,12 +193,58 @@ describe(
const result = doParse('/some/file.xlf', XLIFF);
expect(
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(
['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
['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', () => {
/**
* Source HTML:

View File

@ -140,6 +140,38 @@ describe('XtbTranslationParser', () => {
ɵ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', () => {
const XTB = [
`<?xml version="1.0" encoding="UTF-8" ?>`,