/** * @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 {i18nApply, i18nExpMapping, i18nInterpolation, i18nInterpolationV, i18nMapping} from '../../src/render3/i18n'; import {bind, container, containerRefreshEnd, containerRefreshStart, 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}`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{START_A: 1, START_B: 2, START_REMOVE_ME: 3, START_C: 4}]); // Initial template: //
// // // // // //
// Translated to: //
// trad 1 // // trad 2 // trad 3 // //
function createTemplate() { elementStart(0, 'div'); { // Start of translated section 1 // - i18n sections do not contain any text() instruction elementStart(1, 'a'); // START_A { elementStart(2, 'b'); // START_B elementEnd(); elementStart(3, 'remove-me'); // START_REMOVE_ME elementEnd(); } elementEnd(); elementStart(4, 'c'); // START_C elementEnd(); } // 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`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{EXP_1: 1, EXP_2: 2}]); 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) { 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`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{EXP_1: 1}]); 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) { 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) { elementStart(0, 'div'); // Start of translated section 1 // End of translated section 1 elementEnd(); } if (rf & RenderFlags.Update) { elementProperty(0, 'title', i18nInterpolation(i18n_1, 2, 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`; const 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}]); const i18n_2 = i18nExpMapping(MSG_ATTR_1, {EXP_1: 0, EXP_2: 1}); 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) { elementStart(0, 'div'); { // Start of translated section 1 text(1); // EXP_1 elementStart(2, 'remove-me-1'); // START_REMOVE_ME_1 { elementStart(3, 'remove-me-2'); // START_REMOVE_ME_2 elementEnd(); elementStart(4, 'remove-me-3'); // START_REMOVE_ME_3 elementEnd(); } 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', i18nInterpolation(i18n_2, 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`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{EXP_1: 2}]); const i18n_2 = i18nMapping(MSG_DIV_SECTION_2, [{START_C: 5}]); const i18n_3 = i18nExpMapping(MSG_ATTR_1, {EXP_1: 0, EXP_2: 1}); 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) { 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 elementStart(5, 'c'); // START_C elementEnd(); // 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', i18nInterpolation(i18n_3, 2, 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 const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{EXP_1: 0}]); 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) { 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` const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{START_B: 2, START_C: 3}], null, null, 4); // Initial template: //
// // // // // // //
// Translated to: //
// // // // // //
function createTemplate() { elementStart(0, 'div'); { elementStart(1, 'a'); elementEnd(); { // Start of translated section 1 elementStart(2, 'b'); // START_B elementEnd(); elementStart(3, 'c'); // START_C elementEnd(); // End of translated section 1 } elementStart(4, 'd'); elementEnd(); } 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 const i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{START_LI: 1}, {START_LI: 0}], [null, {EXP_1: 1}], ['START_LI']); 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) { 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 const i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [null, {START_LI_0: 0}, {START_LI_1: 0}], [{START_LI_0: 1, START_LI_1: 2}, {EXP_1: 1}, {EXP_2: 1}], ['START_LI_0', 'START_LI_1']); 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) { 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 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 const i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [null, {START_LI: 0}, {START_SPAN: 0}], [{START_LI: 1}, {START_SPAN: 1}, {EXP_1: 1}], ['START_LI', 'START_SPAN']); 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) { 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 directives 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 const i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{START_LI_0: 1, START_LI_2: 3}, {START_LI_1: 0}], [{START_LI_1: 2}, {EXP_1: 1}], ['START_LI_1']); 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) { elementStart(0, 'ul'); { // Start of translated section 1 elementStart(1, 'li'); // START_LI_0 elementEnd(); 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 containers', () => { const MSG_DIV_SECTION_1 = `loop`; // The indexes are based on each template function const i18n_1 = i18nMapping( MSG_DIV_SECTION_1, [{START_LI: 1}, {START_LI: 0}], [null, {EXP_1: 1}], ['START_LI']); 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) { 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(0); elementStart(1, 'p'); { projection(2, 0); } elementEnd(); } } }); } const MSG_DIV_SECTION_1 = `{$START_CHILD}Je suis projeté depuis {$START_B}{$EXP_1}{$END_B}{$END_CHILD}`; const 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}]); const MSG_ATTR_1 = `Enfant de {$EXP_1}`; const i18n_2 = i18nExpMapping(MSG_ATTR_1, {EXP_1: 0}); @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) { elementStart(0, 'div'); { // Start of translated section 1 elementStart(1, 'child'); // START_CHILD { elementStart(2, 'b'); // START_B { text(3); // EXP_1 elementStart(4, 'remove-me-1'); // START_REMOVE_ME_1 elementEnd(); } elementEnd(); elementStart(5, 'remove-me-2'); // START_REMOVE_ME_2 elementEnd(); } elementEnd(); elementStart(6, 'remove-me-3'); // START_REMOVE_ME_3 elementEnd(); // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(2, 'title', i18nInterpolation(i18n_2, 1, 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(0); elementStart(1, 'p'); { projection(2, 0); } elementEnd(); } } }); } const MSG_DIV_SECTION_1 = `Je suis projeté depuis {$EXP_1}`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{EXP_1: 4}]); const MSG_ATTR_1 = `Enfant de {$EXP_1}`; const i18n_2 = i18nExpMapping(MSG_ATTR_1, {EXP_1: 0}); @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) { elementStart(0, 'div'); { elementStart(1, 'child'); { elementStart(2, 'any'); elementEnd(); elementStart(3, 'b'); { // Start of translated section 1 text(4); // EXP_1 // End of translated section 1 } elementEnd(); elementStart(5, 'any'); elementEnd(); } elementEnd(); } elementEnd(); i18nApply(4, i18n_1[0]); } if (rf & RenderFlags.Update) { elementProperty(3, 'title', i18nInterpolation(i18n_2, 1, 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(0); elementStart(1, 'div'); { projection(2, 0); } 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(0); elementStart(1, 'grand-child'); { projection(2, 0); } elementEnd(); } } }); } const MSG_DIV_SECTION_1 = `{$START_B}Bonjour{$END_B} Monde!`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{START_B: 1}]); @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) { elementStart(0, 'child'); { // Start of translated section 1 elementStart(1, 'b'); // START_B elementEnd(); // 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(0, [[['span']]], ['span']); projection(1, 0, 1); } } }); } const MSG_DIV_SECTION_1 = `{$START_SPAN_0}Contenu{$END_SPAN_0}`; const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{START_SPAN_0: 1, START_SPAN_1: 2}]); @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) { elementStart(0, 'child'); { // Start of translated section 1 elementStart(1, 'span', ['title', 'keepMe']); // START_SPAN_0 elementEnd(); elementStart(2, 'span', ['title', 'deleteMe']); // START_SPAN_1 elementEnd(); // End of translated section 1 } elementEnd(); i18nApply(1, i18n_1[0]); } } }); } const fixture = new ComponentFixture(Parent); expect(fixture.html).toEqual('Contenu'); }); }); 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) { elementStart(0, 'div'); // Start of translated section 1 // End of translated section 1 elementEnd(); } if (rf & RenderFlags.Update) { interpolation = i18nInterpolation(i18n_1, 2, 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); }); });