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:
parent
8cd4099db9
commit
68a9a01a64
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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="<span>" dispEnd="</span>">element <pc id="1" equivStart="START_BOLD_TEXT" equivEnd=` +
|
||||||
|
`"CLOSE_BOLD_TEXT" type="fmt" dispStart="<b>" dispEnd="</b>">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="<` +
|
||||||
|
`span>" dispEnd="</span>"><ph id="2" equiv="INTERPOLATION" disp="{{ interpolation}}"/> tnemele</pc>` +
|
||||||
|
` elbatalsnart <pc id="1" equivStart="START_BOLD_TEXT" equivEnd="CLOSE_BOLD_TEXT" type="fmt" dispStart=` +
|
||||||
|
`"<b>" dispEnd="</b>">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:
|
||||||
|
|
|
@ -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" ?>`,
|
||||||
|
|
Loading…
Reference in New Issue