/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {NgForOfContext} from '@angular/common'; import {Component} from '../../src/core'; import {defineComponent} from '../../src/render3/definition'; import {I18nExpInstruction, I18nInstruction, i18nApply, i18nExpMapping, i18nInterpolation1, i18nInterpolation2, i18nInterpolation3, i18nInterpolation4, i18nInterpolation5, i18nInterpolation6, i18nInterpolation7, i18nInterpolation8, i18nInterpolationV, i18nMapping} from '../../src/render3/i18n'; import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; describe('Runtime i18n', () => { it('should support html elements', () => { // Html tags are replaced by placeholders. // Open tag placeholders are never re-used (closing tag placeholders can be). const MSG_DIV_SECTION_1 = `{$START_C}trad 1{$END_C}{$START_A}trad 2{$START_B}trad 3{$END_B}{$END_A}`; let i18n_1: I18nInstruction[][]; // Initial template: //
// // // // // //
// Translated to: //
// trad 1 // // trad 2 // trad 3 // //
function createTemplate() { if (!i18n_1) { i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{'START_A': 1, 'START_B': 2, 'START_REMOVE_ME': 3, 'START_C': 4}]); } elementStart(0, 'div'); { // Start of translated section 1 // - i18n sections do not contain any text() instruction elementStart(1, 'a'); // START_A { element(2, 'b'); // START_B element(3, 'remove-me'); // START_REMOVE_ME } elementEnd(); element(4, 'c'); // START_C } // End of translated section 1 elementEnd(); i18nApply(1, i18n_1[0]); } const fixture = new TemplateFixture(createTemplate); expect(fixture.html).toEqual('
trad 1trad 2trad 3
'); }); it('should support expressions', () => { const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; let i18n_1: I18nInstruction[][]; class MyApp { exp1 = '1'; exp2 = '2'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// {{exp1}} {{exp2}} //
// Translated to: //
// start {{exp2}} middle {{exp1}} end //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 1, 'EXP_2': 2}]); } elementStart(0, 'div'); { // Start of translated section 1 // One text node is added per expression in the interpolation text(1); // EXP_1 text(2); // EXP_2 // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { textBinding(1, bind(ctx.exp1)); textBinding(2, bind(ctx.exp2)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
start 2 middle 1 end
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html).toEqual('
start 2 middle 1 end
'); // Change the expressions fixture.component.exp1 = 'expr 1'; fixture.component.exp2 = 'expr 2'; fixture.update(); expect(fixture.html).toEqual('
start expr 2 middle expr 1 end
'); }); it('should support expressions on removed nodes', () => { const MSG_DIV_SECTION_1 = `message`; let i18n_1: I18nInstruction[][]; class MyApp { exp1 = '1'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// {{exp1}} //
// Translated to: //
// message //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 1}]); } elementStart(0, 'div'); { // Start of translated section 1 text(1); // EXP_1 will be removed // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { textBinding(1, bind(ctx.exp1)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
message
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html).toEqual('
message
'); // Change the expressions fixture.component.exp1 = 'expr 1'; fixture.update(); expect(fixture.html).toEqual('
message
'); }); it('should support expressions in attributes', () => { const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1}); class MyApp { exp1: any = '1'; exp2: any = '2'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty(0, 'title', i18nInterpolation2(i18n_1, ctx.exp1, ctx.exp2)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html).toEqual('
'); // Change the expressions fixture.component.exp1 = function test() {}; fixture.component.exp2 = null; fixture.update(); expect(fixture.html).toEqual('
'); }); it('should support both html elements, expressions and expressions in attributes', () => { const MSG_DIV_SECTION_1 = `{$EXP_1} {$START_P}trad {$EXP_2}{$END_P}`; const MSG_ATTR_1 = `start {$EXP_2} middle {$EXP_1} end`; let i18n_1: I18nInstruction[][]; let i18n_2: I18nExpInstruction[]; class MyApp { exp1 = '1'; exp2 = '2'; exp3 = '3'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// {{exp1}} // // // // //

// {{exp2}} //

// {{exp3}} //
// Translated to: //
// {{exp1}} //

// trad {{exp2}} //

//
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{ 'START_REMOVE_ME_1': 2, 'START_REMOVE_ME_2': 3, 'START_REMOVE_ME_3': 4, 'START_P': 5 }], [{'EXP_1': 1, 'EXP_2': 6, 'EXP_3': 7}]); } if (!i18n_2) { i18n_2 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0, 'EXP_2': 1}); } elementStart(0, 'div'); { // Start of translated section 1 text(1); // EXP_1 elementStart(2, 'remove-me-1'); // START_REMOVE_ME_1 { element(3, 'remove-me-2'); // START_REMOVE_ME_2 element(4, 'remove-me-3'); // START_REMOVE_ME_3 } elementEnd(); elementStart(5, 'p'); // START_P { text(6); } // EXP_2 elementEnd(); text(7); // EXP_3 // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { textBinding(1, bind(ctx.exp1)); textBinding(6, bind(ctx.exp2)); textBinding(7, bind(ctx.exp3)); elementProperty(0, 'title', i18nInterpolation2(i18n_2, ctx.exp1, ctx.exp2)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
1

trad 2

'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html).toEqual('
1

trad 2

'); // Change the expressions fixture.component.exp1 = 'expr 1'; fixture.component.exp2 = 'expr 2'; fixture.update(); expect(fixture.html) .toEqual('
expr 1

trad expr 2

'); }); it('should support multiple i18n elements', () => { const MSG_DIV_SECTION_1 = `trad {$EXP_1}`; const MSG_DIV_SECTION_2 = `{$START_C}trad{$END_C}`; const MSG_ATTR_1 = `start {$EXP_2} middle {$EXP_1} end`; let i18n_1: I18nInstruction[][]; let i18n_2: I18nInstruction[][]; let i18n_3: I18nExpInstruction[]; class MyApp { exp1 = '1'; exp2 = '2'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// // {{exp1}} // // hello // // // //
// Translated to: //
// // trad {{exp1}} // // hello // // trad // //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 2}]); } if (!i18n_2) { i18n_2 = i18nMapping(MSG_DIV_SECTION_2, [{'START_C': 5}]); } if (!i18n_3) { i18n_3 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0, 'EXP_2': 1}); } elementStart(0, 'div'); { elementStart(1, 'a'); { // Start of translated section 1 text(2); // EXP_1 // End of translated section 1 } elementEnd(); text(3, 'hello'); elementStart(4, 'b'); { // Start of translated section 2 element(5, 'c'); // START_C // End of translated section 2 } elementEnd(); } elementEnd(); i18nApply(2, i18n_1[0]); i18nApply(5, i18n_2[0]); } if (rf & RenderFlags.Update) { textBinding(2, bind(ctx.exp1)); elementProperty(4, 'title', i18nInterpolation2(i18n_3, ctx.exp1, ctx.exp2)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html) .toEqual('
trad 1hellotrad
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html) .toEqual('
trad 1hellotrad
'); // Change the expressions fixture.component.exp1 = 'expr 1'; fixture.component.exp2 = 'expr 2'; fixture.update(); expect(fixture.html) .toEqual( '
trad expr 1hellotrad
'); }); describe('view containers / embedded templates', () => { it('should support containers', () => { const MSG_DIV_SECTION_1 = `valeur: {$EXP_1}`; // The indexes are based on the main template function let i18n_1: I18nInstruction[][]; class MyApp { exp1 = '1'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: // before ( // % if (condition) { // with i18n // value: {{exp1}} // % } // ) after // Translated : // before ( // % if (condition) { // with i18n // valeur: {{exp1}} // % } // ) after template: (rf: RenderFlags, myApp: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 0}]); } text(0, 'before ('); container(1); text(2, ') after'); } if (rf & RenderFlags.Update) { containerRefreshStart(1); { let rf0 = embeddedViewStart(0); if (rf0 & RenderFlags.Create) { // Start of translated section 1 text(0); // EXP_1 // End of translated section 1 i18nApply(0, i18n_1[0]); } if (rf0 & RenderFlags.Update) { textBinding(0, bind(myApp.exp1)); } embeddedViewEnd(); } containerRefreshEnd(); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('before (valeur: 1) after'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html).toEqual('before (valeur: 1) after'); }); it('should support ng-container', () => { const MSG_DIV_SECTION_1 = `{$START_B}{$END_B}`; // With ng-container the i18n node doesn't create any element at runtime which means that // its children are not the only children of their parent, some nodes which are not // translated might also be the children of the same parent. // This is why we need to pass the `lastChildIndex` to `i18nMapping` let i18n_1: I18nInstruction[][]; // Initial template: //
// // // // // // //
// Translated to: //
// // // // // //
function createTemplate() { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{'START_B': 2, 'START_C': 3}], null, null, 4); } elementStart(0, 'div'); { element(1, 'a'); { // Start of translated section 1 element(2, 'b'); // START_B element(3, 'c'); // START_C // End of translated section 1 } element(4, 'd'); } elementEnd(); i18nApply(2, i18n_1[0]); } const fixture = new TemplateFixture(createTemplate); expect(fixture.html).toEqual('
'); }); it('should support embedded templates', () => { const MSG_DIV_SECTION_1 = `{$START_LI}valeur: {$EXP_1}!{$END_LI}`; // 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': 1}, {'START_LI': 0}], [null, {'EXP_1': 1}], ['START_LI']); } elementStart(0, 'ul'); { // Start of translated section 1 container(1, liTemplate, null, ['ngForOf', '']); // START_LI // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(1, '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 { text(1); } // EXP_1 elementEnd(); // End of translated section 2 i18nApply(0, i18n_1[1]); } 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 sibling embedded templates', () => { const MSG_DIV_SECTION_1 = `{$START_LI_0}valeur: {$EXP_1}!{$END_LI_0}{$START_LI_1}valeur bis: {$EXP_2}!{$END_LI_1}`; // 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( '
  • valeur: 1!
  • valeur: 2!
  • valeur bis: 1!
  • valeur bis: 2!
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html) .toEqual( '
  • valeur: 1!
  • valeur: 2!
  • valeur bis: 1!
  • valeur bis: 2!
'); // Remove the last item fixture.component.items.length = 1; fixture.update(); expect(fixture.html).toEqual('
  • valeur: 1!
  • valeur bis: 1!
'); // Change an item fixture.component.items[0] = 'one'; fixture.update(); expect(fixture.html).toEqual('
  • valeur: one!
  • valeur bis: one!
'); // Add an item fixture.component.items.push('two'); fixture.update(); expect(fixture.html) .toEqual( '
  • valeur: one!
  • valeur: two!
  • valeur bis: one!
  • valeur bis: two!
'); }); 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: //
    //
  • value: {{item}}
  • //
  • value bis: {{item}}
  • //
// Translated to: //
    //
  • valeur bis: {{item}}!
  • //
  • valeur: {{item}}!
  • //
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( '
  • valeur bis: 1!
  • valeur bis: 2!
  • valeur: 1!
  • valeur: 2!
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html) .toEqual( '
  • valeur bis: 1!
  • valeur bis: 2!
  • valeur: 1!
  • valeur: 2!
'); // Remove the last item fixture.component.items.length = 1; fixture.update(); expect(fixture.html).toEqual('
  • valeur bis: 1!
  • valeur: 1!
'); // Change an item fixture.component.items[0] = 'one'; fixture.update(); expect(fixture.html).toEqual('
  • valeur bis: one!
  • valeur: one!
'); // Add an item fixture.component.items.push('two'); fixture.update(); expect(fixture.html) .toEqual( '
  • valeur bis: one!
  • valeur bis: two!
  • valeur: one!
  • valeur: two!
'); }); 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 let i18n_1: I18nInstruction[][]; class MyApp { items: string[] = ['1', '2']; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
    //
  • // value: {{item}} //
  • //
// Translated to: //
    //
  • // valeur: {{item}}! //
  • //
template: (rf: RenderFlags, myApp: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{'START_LI': 1}, {'START_LI': 0, 'START_SPAN': 1}, {'START_SPAN': 0}], [null, null, {'EXP_1': 1}], ['START_LI', 'START_SPAN']); } elementStart(0, 'ul'); { // Start of translated section 1 container(1, liTemplate, null, ['ngForOf', '']); // START_LI // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(1, '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 { container(1, spanTemplate, null, ['ngForOf', '']); // START_SPAN } elementEnd(); // End of translated section 2 i18nApply(0, i18n_1[1]); } if (rf1 & RenderFlags.Update) { elementProperty(1, 'ngForOf', bind(myApp.items)); } } function spanTemplate(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, 'span'); // START_SPAN { text(1); } // EXP_1 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( '
  • valeur: 1!valeur: 2!
  • valeur: 1!valeur: 2!
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html) .toEqual( '
  • valeur: 1!valeur: 2!
  • valeur: 1!valeur: 2!
'); // Remove the last item fixture.component.items.length = 1; fixture.update(); expect(fixture.html).toEqual('
  • valeur: 1!
'); // Change an item fixture.component.items[0] = 'one'; fixture.update(); expect(fixture.html).toEqual('
  • valeur: one!
'); // Add an item fixture.component.items.push('two'); fixture.update(); expect(fixture.html) .toEqual( '
  • valeur: one!valeur: two!
  • valeur: one!valeur: two!
'); }); 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 let i18n_1: I18nInstruction[][]; class MyApp { items: string[] = ['first', 'second']; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
    //
  • start
  • //
  • value: {{item}}
  • //
  • delete me
  • //
// Translated to: //
    //
  • début
  • //
  • valeur: {{item}}
  • // fin //
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_2': 3}, {'START_LI_1': 0}], [null, {'EXP_1': 1}], ['START_LI_1']); } elementStart(0, 'ul'); { // Start of translated section 1 element(1, 'li'); // START_LI_0 container(2, liTemplate, null, ['ngForOf', '']); // START_LI_1 elementStart(3, 'li'); // START_LI_2 { text(4, 'delete me'); } elementEnd(); // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { 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_1 { text(1); } // EXP_1 elementEnd(); // End of translated section 2 i18nApply(0, i18n_1[1]); } if (rf1 & RenderFlags.Update) { textBinding(1, bind(row.$implicit)); } } }, directives: () => [NgForOf] }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html) .toEqual('
  • début
  • valeur: first
  • valeur: second
  • fin
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html) .toEqual('
  • début
  • valeur: first
  • valeur: second
  • fin
'); // Remove the last item fixture.component.items.length = 1; fixture.update(); expect(fixture.html).toEqual('
  • début
  • valeur: first
  • fin
'); // Change an item fixture.component.items[0] = 'one'; fixture.update(); expect(fixture.html).toEqual('
  • début
  • valeur: one
  • fin
'); // Add an item fixture.component.items.push('two'); fixture.update(); expect(fixture.html) .toEqual('
  • début
  • valeur: one
  • valeur: two
  • fin
'); }); 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[][]; class MyApp { items: string[] = ['first', 'second']; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
    //
  • value: {{item}}
  • //
// Translated to: //
    // loop //
template: (rf: RenderFlags, myApp: MyApp) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{'START_LI': 1}, {'START_LI': 0}], [null, {'EXP_1': 1}], ['START_LI']); } elementStart(0, 'ul'); { // Start of translated section 1 container(1, liTemplate, undefined, ['ngForOf', '']); // START_LI // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(1, '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 { text(1); } // EXP_1 elementEnd(); // End of translated section 2 i18nApply(0, i18n_1[1]); } if (rf1 & RenderFlags.Update) { textBinding(1, bind(row.$implicit)); } } }, directives: () => [NgForOf] }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
    loop
'); // Change detection cycle, no model changes fixture.update(); expect(fixture.html).toEqual('
    loop
'); // Remove the last item fixture.component.items.length = 1; fixture.update(); expect(fixture.html).toEqual('
    loop
'); // Change an item fixture.component.items[0] = 'one'; fixture.update(); expect(fixture.html).toEqual('
    loop
'); // Add an item fixture.component.items.push('two'); fixture.update(); expect(fixture.html).toEqual('
    loop
'); }); }); describe('projection', () => { it('should project the translations', () => { @Component({selector: 'child', template: '

'}) class Child { static ngComponentDef = defineComponent({ type: Child, selectors: [['child']], factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { projectionDef(); elementStart(0, 'p'); { projection(1); } elementEnd(); } } }); } const MSG_DIV_SECTION_1 = `{$START_CHILD}Je suis projeté depuis {$START_B}{$EXP_1}{$END_B}{$END_CHILD}`; let i18n_1: I18nInstruction[][]; const MSG_ATTR_1 = `Enfant de {$EXP_1}`; let i18n_2: I18nExpInstruction[]; @Component({ selector: 'parent', template: `
I am projected from {{name}}
` // Translated to: //
// // Je suis projeté depuis {{name}} // //
}) class Parent { name: string = 'Parent'; static ngComponentDef = defineComponent({ type: Parent, selectors: [['parent']], directives: [Child], factory: () => new Parent(), template: (rf: RenderFlags, cmp: Parent) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{ 'START_CHILD': 1, 'START_B': 2, 'START_REMOVE_ME_1': 4, 'START_REMOVE_ME_2': 5, 'START_REMOVE_ME_3': 6 }], [{'EXP_1': 3}]); } if (!i18n_2) { i18n_2 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0}); } elementStart(0, 'div'); { // Start of translated section 1 elementStart(1, 'child'); // START_CHILD { elementStart(2, 'b'); // START_B { text(3); // EXP_1 element(4, 'remove-me-1'); // START_REMOVE_ME_1 } elementEnd(); element(5, 'remove-me-2'); // START_REMOVE_ME_2 } elementEnd(); element(6, 'remove-me-3'); // START_REMOVE_ME_3 // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(2, 'title', i18nInterpolation1(i18n_2, cmp.name)); textBinding(3, bind(cmp.name)); } } }); } const fixture = new ComponentFixture(Parent); expect(fixture.html) .toEqual( '

Je suis projeté depuis Parent

'); }); it('should project a translated i18n block', () => { @Component({selector: 'child', template: '

'}) class Child { static ngComponentDef = defineComponent({ type: Child, selectors: [['child']], factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { projectionDef(); elementStart(0, 'p'); { projection(1); } elementEnd(); } } }); } const MSG_DIV_SECTION_1 = `Je suis projeté depuis {$EXP_1}`; let i18n_1: I18nInstruction[][]; const MSG_ATTR_1 = `Enfant de {$EXP_1}`; let i18n_2: I18nExpInstruction[]; @Component({ selector: 'parent', template: `
I am projected from {{name}}
` // Translated to: //
// // // Je suis projeté depuis {{name}} // // //
}) class Parent { name: string = 'Parent'; static ngComponentDef = defineComponent({ type: Parent, selectors: [['parent']], directives: [Child], factory: () => new Parent(), template: (rf: RenderFlags, cmp: Parent) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 4}]); } if (!i18n_2) { i18n_2 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0}); } elementStart(0, 'div'); { elementStart(1, 'child'); { element(2, 'any'); elementStart(3, 'b'); { // Start of translated section 1 text(4); // EXP_1 // End of translated section 1 } elementEnd(); element(5, 'any'); } elementEnd(); } elementEnd(); i18nApply(4, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(3, 'title', i18nInterpolation1(i18n_2, cmp.name)); textBinding(4, bind(cmp.name)); } } }); } const fixture = new ComponentFixture(Parent); expect(fixture.html) .toEqual( '

Je suis projeté depuis Parent

'); }); it('should re-project translations when multiple projections', () => { @Component({selector: 'grand-child', template: '
'}) class GrandChild { static ngComponentDef = defineComponent({ type: GrandChild, selectors: [['grand-child']], factory: () => new GrandChild(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { projectionDef(); elementStart(0, 'div'); { projection(1); } elementEnd(); } } }); } @Component( {selector: 'child', template: ''}) class Child { static ngComponentDef = defineComponent({ type: Child, selectors: [['child']], directives: [GrandChild], factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { projectionDef(); elementStart(0, 'grand-child'); { projection(1); } elementEnd(); } } }); } const MSG_DIV_SECTION_1 = `{$START_B}Bonjour{$END_B} Monde!`; let i18n_1: I18nInstruction[][]; @Component({ selector: 'parent', template: `Hello World!` // Translated to: //
Bonjour Monde!
}) class Parent { name: string = 'Parent'; static ngComponentDef = defineComponent({ type: Parent, selectors: [['parent']], directives: [Child], factory: () => new Parent(), template: (rf: RenderFlags, cmp: Parent) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{'START_B': 1}]); } elementStart(0, 'child'); { // Start of translated section 1 element(1, 'b'); // START_B // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } } }); } const fixture = new ComponentFixture(Parent); expect(fixture.html) .toEqual('
Bonjour Monde!
'); }); it('should project translations with selectors', () => { @Component({ selector: 'child', template: ` ` }) class Child { static ngComponentDef = defineComponent({ type: Child, selectors: [['child']], factory: () => new Child(), template: (rf: RenderFlags, cmp: Child) => { if (rf & RenderFlags.Create) { projectionDef([[['span']]], ['span']); projection(0, 1); } } }); } const MSG_DIV_SECTION_1 = `{$START_SPAN_0}Contenu{$END_SPAN_0}`; let i18n_1: I18nInstruction[][]; @Component({ selector: 'parent', template: ` ` // Translated to: // Contenu }) class Parent { static ngComponentDef = defineComponent({ type: Parent, selectors: [['parent']], directives: [Child], factory: () => new Parent(), template: (rf: RenderFlags, cmp: Parent) => { if (rf & RenderFlags.Create) { if (!i18n_1) { i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{'START_SPAN_0': 1, 'START_SPAN_1': 2}]); } elementStart(0, 'child'); { // Start of translated section 1 element(1, 'span', ['title', 'keepMe']); // START_SPAN_0 element(2, 'span', ['title', 'deleteMe']); // START_SPAN_1 // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } } }); } const fixture = new ComponentFixture(Parent); expect(fixture.html).toEqual('Contenu'); }); }); describe('i18nInterpolation', () => { it('i18nInterpolation should return the same value as i18nInterpolationV', () => { const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1}); let interpolation; let interpolationV; class MyApp { exp1: any = '1'; exp2: any = '2'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { interpolation = i18nInterpolation2(i18n_1, ctx.exp1, ctx.exp2); interpolationV = i18nInterpolationV(i18n_1, [ctx.exp1, ctx.exp2]); elementProperty(0, 'title', interpolation); } } }); } const fixture = new ComponentFixture(MyApp); expect(interpolation).toBeDefined(); expect(interpolation).toEqual(interpolationV); }); it('i18nInterpolation3 should work', () => { const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} end`; const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2}); class MyApp { exp1: any = '1'; exp2: any = '2'; exp3: any = '3'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty(0, 'title', i18nInterpolation3(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); }); it('i18nInterpolation4 should work', () => { const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} end`; const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3}); class MyApp { exp1: any = '1'; exp2: any = '2'; exp3: any = '3'; exp4: any = '4'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty( 0, 'title', i18nInterpolation4(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); }); it('i18nInterpolation5 should work', () => { const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} end`; const i18n_1 = i18nExpMapping( MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4}); class MyApp { exp1: any = '1'; exp2: any = '2'; exp3: any = '3'; exp4: any = '4'; exp5: any = '5'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty( 0, 'title', i18nInterpolation5(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); }); it('i18nInterpolation6 should work', () => { const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} _ {$EXP_6} end`; const i18n_1 = i18nExpMapping( MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4, 'EXP_6': 5}); class MyApp { exp1: any = '1'; exp2: any = '2'; exp3: any = '3'; exp4: any = '4'; exp5: any = '5'; exp6: any = '6'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty( 0, 'title', i18nInterpolation6( i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5, ctx.exp6)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); }); it('i18nInterpolation7 should work', () => { const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} _ {$EXP_6} _ {$EXP_7} end`; const i18n_1 = i18nExpMapping( MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4, 'EXP_6': 5, 'EXP_7': 6}); class MyApp { exp1: any = '1'; exp2: any = '2'; exp3: any = '3'; exp4: any = '4'; exp5: any = '5'; exp6: any = '6'; exp7: any = '7'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty( 0, 'title', i18nInterpolation7( i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5, ctx.exp6, ctx.exp7)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); }); it('i18nInterpolation8 should work', () => { const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} _ {$EXP_6} _ {$EXP_7} _ {$EXP_8} end`; const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, { 'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4, 'EXP_6': 5, 'EXP_7': 6, 'EXP_8': 7 }); class MyApp { exp1: any = '1'; exp2: any = '2'; exp3: any = '3'; exp4: any = '4'; exp5: any = '5'; exp6: any = '6'; exp7: any = '7'; exp8: any = '8'; static ngComponentDef = defineComponent({ type: MyApp, factory: () => new MyApp(), selectors: [['my-app']], // Initial template: //
// Translated to: //
template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { element(0, 'div'); // translated section 1 } if (rf & RenderFlags.Update) { elementProperty( 0, 'title', i18nInterpolation8( i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5, ctx.exp6, ctx.exp7, ctx.exp8)); } } }); } const fixture = new ComponentFixture(MyApp); expect(fixture.html).toEqual('
'); }); }); });