diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index 6c64793290..78e011c7b4 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -76,10 +76,11 @@ export function i18nMapping( expressions?: (PlaceholderMap | null)[] | null, templateRoots?: string[] | null, lastChildIndex?: number | null): I18nInstruction[][] { const translationParts = translation.split(i18nTagRegex); - const instructions: I18nInstruction[][] = []; + const nbTemplates = templateRoots ? templateRoots.length + 1 : 1; + const instructions: I18nInstruction[][] = (new Array(nbTemplates)).fill(undefined); generateMappingInstructions( - 0, translationParts, instructions, elements, expressions, templateRoots, lastChildIndex); + 0, 0, translationParts, instructions, elements, expressions, templateRoots, lastChildIndex); return instructions; } @@ -90,7 +91,9 @@ export function i18nMapping( * * See `i18nMapping()` for more details. * - * @param index The current index in `translationParts`. + * @param tmplIndex The order of appearance of the template. + * 0 for the root template, following indexes match the order in `templateRoots`. + * @param partIndex The current index in `translationParts`. * @param translationParts The translation string split into an array of placeholders and text * elements. * @param instructions The current list of instructions to update. @@ -102,13 +105,14 @@ export function i18nMapping( * generating the instructions for their parent template. * @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is * an ng-container. + * * @returns the current index in `translationParts` */ function generateMappingInstructions( - index: number, translationParts: string[], instructions: I18nInstruction[][], - elements: (PlaceholderMap | null)[] | null, expressions?: (PlaceholderMap | null)[] | null, - templateRoots?: string[] | null, lastChildIndex?: number | null): number { - const tmplIndex = instructions.length; + tmplIndex: number, partIndex: number, translationParts: string[], + instructions: I18nInstruction[][], elements: (PlaceholderMap | null)[] | null, + expressions?: (PlaceholderMap | null)[] | null, templateRoots?: string[] | null, + lastChildIndex?: number | null): number { const tmplInstructions: I18nInstruction[] = []; const phVisited: string[] = []; let openedTagCount = 0; @@ -118,20 +122,20 @@ function generateMappingInstructions( let currentExpressions: PlaceholderMap|null = expressions && expressions[tmplIndex] ? expressions[tmplIndex] : null; - instructions.push(tmplInstructions); + instructions[tmplIndex] = tmplInstructions; - for (; index < translationParts.length; index++) { - const value = translationParts[index]; + for (; partIndex < translationParts.length; partIndex++) { + // The value can either be text or the name of a placeholder (element/template root/expression) + const value = translationParts[partIndex]; // Odd indexes are placeholders - if (index & 1) { + if (partIndex & 1) { let phIndex; if (currentElements && currentElements[value] !== undefined) { phIndex = currentElements[value]; - // The placeholder represents a DOM element - // Add an instruction to move the element - const isTemplateRoot = templateRoots && templateRoots[tmplIndex] === value; - if (isTemplateRoot) { + // The placeholder represents a DOM element, add an instruction to move it + let templateRootIndex = templateRoots ? templateRoots.indexOf(value) : -1; + if (templateRootIndex !== -1 && (templateRootIndex + 1) !== tmplIndex) { // This is a template root, it has no closing tag, not treating it as an element tmplInstructions.push(phIndex | I18nInstructions.TemplateRoot); } else { @@ -141,8 +145,7 @@ function generateMappingInstructions( phVisited.push(value); } else if (currentExpressions && currentExpressions[value] !== undefined) { phIndex = currentExpressions[value]; - // The placeholder represents an expression - // Add an instruction to move the expression + // The placeholder represents an expression, add an instruction to move it tmplInstructions.push(phIndex | I18nInstructions.Expression); phVisited.push(value); } else { @@ -163,11 +166,13 @@ function generateMappingInstructions( maxIndex = phIndex; } - if (templateRoots && templateRoots.indexOf(value) !== -1 && - templateRoots.indexOf(value) >= tmplIndex) { - index = generateMappingInstructions( - index, translationParts, instructions, elements, expressions, templateRoots, - lastChildIndex); + if (templateRoots) { + const newTmplIndex = templateRoots.indexOf(value) + 1; + if (newTmplIndex !== 0 && newTmplIndex !== tmplIndex) { + partIndex = generateMappingInstructions( + newTmplIndex, partIndex, translationParts, instructions, elements, expressions, + templateRoots, lastChildIndex); + } } } else if (value) { @@ -237,7 +242,7 @@ function generateMappingInstructions( } } - return index; + return partIndex; } function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) { diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index d185583385..0c96c43d20 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -720,6 +720,116 @@ describe('Runtime i18n', () => { ''); }); + it('should support changing the order of multiple template roots in the same template', () => { + const MSG_DIV_SECTION_1 = + `{$START_LI_1}valeur bis: {$EXP_2}!{$END_LI_1}{$START_LI_0}valeur: {$EXP_1}!{$END_LI_0}`; + // The indexes are based on each template function + let i18n_1: I18nInstruction[][]; + class MyApp { + items: string[] = ['1', '2']; + + static ngComponentDef = defineComponent({ + type: MyApp, + factory: () => new MyApp(), + selectors: [['my-app']], + // Initial template: + // + + // Translated to: + // + template: (rf: RenderFlags, myApp: MyApp) => { + if (rf & RenderFlags.Create) { + if (!i18n_1) { + i18n_1 = i18nMapping( + MSG_DIV_SECTION_1, + [{'START_LI_0': 1, 'START_LI_1': 2}, {'START_LI_0': 0}, {'START_LI_1': 0}], + [null, {'EXP_1': 1}, {'EXP_2': 1}], ['START_LI_0', 'START_LI_1']); + } + + elementStart(0, 'ul'); + { + // Start of translated section 1 + container(1, liTemplate, null, ['ngForOf', '']); // START_LI_0 + container(2, liTemplateBis, null, ['ngForOf', '']); // START_LI_1 + // End of translated section 1 + } + elementEnd(); + i18nApply(1, i18n_1[0]); + } + if (rf & RenderFlags.Update) { + elementProperty(1, 'ngForOf', bind(myApp.items)); + elementProperty(2, 'ngForOf', bind(myApp.items)); + } + + function liTemplate(rf1: RenderFlags, row: NgForOfContext) { + if (rf1 & RenderFlags.Create) { + // This is a container so the whole template is a translated section + // Start of translated section 2 + elementStart(0, 'li'); // START_LI_0 + { text(1); } // EXP_1 + elementEnd(); + // End of translated section 2 + i18nApply(0, i18n_1[1]); + } + if (rf1 & RenderFlags.Update) { + textBinding(1, bind(row.$implicit)); + } + } + + function liTemplateBis(rf1: RenderFlags, row: NgForOfContext) { + if (rf1 & RenderFlags.Create) { + // This is a container so the whole template is a translated section + // Start of translated section 3 + elementStart(0, 'li'); // START_LI_1 + { text(1); } // EXP_2 + elementEnd(); + // End of translated section 3 + i18nApply(0, i18n_1[2]); + } + if (rf1 & RenderFlags.Update) { + textBinding(1, bind(row.$implicit)); + } + } + }, + directives: () => [NgForOf] + }); + } + + const fixture = new ComponentFixture(MyApp); + expect(fixture.html) + .toEqual( + ''); + + // Change detection cycle, no model changes + fixture.update(); + expect(fixture.html) + .toEqual( + ''); + + // Remove the last item + fixture.component.items.length = 1; + fixture.update(); + expect(fixture.html).toEqual(''); + + // Change an item + fixture.component.items[0] = 'one'; + fixture.update(); + expect(fixture.html).toEqual(''); + + // Add an item + fixture.component.items.push('two'); + fixture.update(); + expect(fixture.html) + .toEqual( + ''); + }); + it('should support nested embedded templates', () => { const MSG_DIV_SECTION_1 = `{$START_LI}{$START_SPAN}valeur: {$EXP_1}!{$END_SPAN}{$END_LI}`; // The indexes are based on each template function @@ -831,7 +941,7 @@ describe('Runtime i18n', () => { ''); }); - it('should be able to move template directives around', () => { + it('should be able to move template roots around', () => { const MSG_DIV_SECTION_1 = `{$START_LI_0}début{$END_LI_0}{$START_LI_1}valeur: {$EXP_1}{$END_LI_1}fin`; // The indexes are based on each template function @@ -928,7 +1038,7 @@ describe('Runtime i18n', () => { .toEqual(''); }); - it('should be able to remove containers', () => { + it('should be able to remove template roots', () => { const MSG_DIV_SECTION_1 = `loop`; // The indexes are based on each template function let i18n_1: I18nInstruction[][];