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,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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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="<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', () => {
|
||||
/**
|
||||
* Source HTML:
|
||||
|
|
|
@ -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" ?>`,
|
||||
|
|
Loading…
Reference in New Issue