This commit updates the code to move generated i18n statements into the `consts` field of
ComponentDef to avoid invoking `$localize` function before component initialization (to better
support runtime translations) and also avoid problems with lazy-loading when i18n defs may not
be present in a chunk where it's referenced.
Prior to this change the i18n statements were generated at the top leve:
```
var I18N_0;
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
    var MSG_X = goog.getMsg(“…”);
    I18N_0 = MSG_X;
} else {
    I18N_0 = $localize('...');
}
defineComponent({
    // ...
    template: function App_Template(rf, ctx) {
        i0.ɵɵi18n(2, I18N_0);
    }
});
```
This commit updates the logic to generate the following code instead:
```
defineComponent({
    // ...
    consts: function() {
        var I18N_0;
        if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
            var MSG_X = goog.getMsg(“…”);
            I18N_0 = MSG_X;
        } else {
            I18N_0 = $localize('...');
        }
        return [
            I18N_0
        ];
    },
    template: function App_Template(rf, ctx) {
        i0.ɵɵi18n(2, 0);
    }
});
```
Also note that i18n template instructions now refer to the `consts` array using an index
(similar to other template instructions).
PR Close #38404
		
	
			
		
			
				
	
	
		
			638 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			638 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | ||
|  * @license
 | ||
|  * Copyright Google LLC 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 {ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '@angular/core';
 | ||
| import {getTranslationForTemplate} from '@angular/core/src/render3/i18n/i18n_parse';
 | ||
| 
 | ||
| import {noop} from '../../../compiler/src/render3/view/util';
 | ||
| import {setDelayProjection, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all';
 | ||
| import {I18nUpdateOpCodes, TI18n, TIcu} from '../../src/render3/interfaces/i18n';
 | ||
| import {TConstants} from '../../src/render3/interfaces/node';
 | ||
| import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view';
 | ||
| import {getNativeByIndex} from '../../src/render3/util/view_utils';
 | ||
| 
 | ||
| import {TemplateFixture} from './render_util';
 | ||
| import {debugMatch} from './utils';
 | ||
| 
 | ||
| describe('Runtime i18n', () => {
 | ||
|   afterEach(() => {
 | ||
|     setDelayProjection(false);
 | ||
|   });
 | ||
|   describe('getTranslationForTemplate', () => {
 | ||
|     it('should crop messages for the selected template', () => {
 | ||
|       let message = `simple text`;
 | ||
|       expect(getTranslationForTemplate(message)).toEqual(message);
 | ||
| 
 | ||
|       message = `Hello <20>0<EFBFBD>!`;
 | ||
|       expect(getTranslationForTemplate(message)).toEqual(message);
 | ||
| 
 | ||
|       message = `Hello <20>#2<><32>0<EFBFBD><30>/#2<>!`;
 | ||
|       expect(getTranslationForTemplate(message)).toEqual(message);
 | ||
| 
 | ||
|       // Embedded sub-templates
 | ||
|       message = `<EFBFBD>0<EFBFBD> is rendered as: <20>*2:1<>before<72>*1:2<>middle<6C>/*1:2<>after<65>/*2:1<>!`;
 | ||
|       expect(getTranslationForTemplate(message)).toEqual('<27>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<>!');
 | ||
|       expect(getTranslationForTemplate(message, 1)).toEqual('before<72>*1:2<><32>/*1:2<>after');
 | ||
|       expect(getTranslationForTemplate(message, 2)).toEqual('middle');
 | ||
| 
 | ||
|       // Embedded & sibling sub-templates
 | ||
|       message =
 | ||
|           `<EFBFBD>0<EFBFBD> is rendered as: <20>*2:1<>before<72>*1:2<>middle<6C>/*1:2<>after<65>/*2:1<> and also <20>*4:3<>before<72>*1:4<>middle<6C>/*1:4<>after<65>/*4:3<>!`;
 | ||
|       expect(getTranslationForTemplate(message))
 | ||
|           .toEqual('<27>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<> and also <20>*4:3<><33>/*4:3<>!');
 | ||
|       expect(getTranslationForTemplate(message, 1)).toEqual('before<72>*1:2<><32>/*1:2<>after');
 | ||
|       expect(getTranslationForTemplate(message, 2)).toEqual('middle');
 | ||
|       expect(getTranslationForTemplate(message, 3)).toEqual('before<72>*1:4<><34>/*1:4<>after');
 | ||
|       expect(getTranslationForTemplate(message, 4)).toEqual('middle');
 | ||
|     });
 | ||
| 
 | ||
|     it('should throw if the template is malformed', () => {
 | ||
|       const message = `<EFBFBD>*2:1<>message!`;
 | ||
|       expect(() => getTranslationForTemplate(message)).toThrowError(/Tag mismatch/);
 | ||
|     });
 | ||
|   });
 | ||
| 
 | ||
|   function prepareFixture(
 | ||
|       createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts = 0, nbVars = 0,
 | ||
|       consts: TConstants = []): TemplateFixture {
 | ||
|     return new TemplateFixture(
 | ||
|         createTemplate, updateTemplate || noop, nbConsts, nbVars, null, null, null, undefined,
 | ||
|         consts);
 | ||
|   }
 | ||
| 
 | ||
|   function getOpCodes(
 | ||
|       messageOrAtrs: string|string[], createTemplate: () => void, updateTemplate: (() => void)|null,
 | ||
|       nbConsts: number, index: number): TI18n|I18nUpdateOpCodes {
 | ||
|     const fixture =
 | ||
|         prepareFixture(createTemplate, updateTemplate, nbConsts, undefined, [messageOrAtrs]);
 | ||
|     const tView = fixture.hostView[TVIEW];
 | ||
|     return tView.data[index + HEADER_OFFSET] as TI18n;
 | ||
|   }
 | ||
| 
 | ||
|   describe('i18nStart', () => {
 | ||
|     it('for text', () => {
 | ||
|       const message = 'simple text';
 | ||
|       const nbConsts = 1;
 | ||
|       const index = 0;
 | ||
|       const opCodes = getOpCodes(message, () => {
 | ||
|                         ɵɵi18nStart(index, 0);
 | ||
|                       }, null, nbConsts, index) as TI18n;
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 1,
 | ||
|         create: debugMatch([
 | ||
|           'lView[1] = document.createTextNode("simple text")',
 | ||
|           '(lView[0] as Element).appendChild(lView[1])'
 | ||
|         ]),
 | ||
|         update: [],
 | ||
|         icus: null
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('for elements', () => {
 | ||
|       const message = `Hello <20>#2<>world<6C>/#2<> and <20>#3<>universe<73>/#3<>!`;
 | ||
|       // Template: `<div>Hello <div>world</div> and <span>universe</span>!`
 | ||
|       // 3 consts for the 2 divs and 1 span + 1 const for `i18nStart` = 4 consts
 | ||
|       const nbConsts = 4;
 | ||
|       const index = 1;
 | ||
|       const opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 5,
 | ||
|         create: debugMatch([
 | ||
|           'lView[4] = document.createTextNode("Hello ")',
 | ||
|           '(lView[1] as Element).appendChild(lView[4])',
 | ||
|           '(lView[1] as Element).appendChild(lView[2])',
 | ||
|           'lView[5] = document.createTextNode("world")',
 | ||
|           '(lView[2] as Element).appendChild(lView[5])',
 | ||
|           'setPreviousOrParentTNode(tView.data[2] as TNode)',
 | ||
|           'lView[6] = document.createTextNode(" and ")',
 | ||
|           '(lView[1] as Element).appendChild(lView[6])',
 | ||
|           '(lView[1] as Element).appendChild(lView[3])',
 | ||
|           'lView[7] = document.createTextNode("universe")',
 | ||
|           '(lView[3] as Element).appendChild(lView[7])',
 | ||
|           'setPreviousOrParentTNode(tView.data[3] as TNode)',
 | ||
|           'lView[8] = document.createTextNode("!")',
 | ||
|           '(lView[1] as Element).appendChild(lView[8])',
 | ||
|         ]),
 | ||
|         update: [],
 | ||
|         icus: null
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('for simple bindings', () => {
 | ||
|       const message = `Hello <20>0<EFBFBD>!`;
 | ||
|       const nbConsts = 2;
 | ||
|       const index = 1;
 | ||
|       const opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect((opCodes as any).update.debug).toEqual([
 | ||
|         'if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }'
 | ||
|       ]);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 1,
 | ||
|         create: debugMatch([
 | ||
|           'lView[2] = document.createTextNode("")',
 | ||
|           '(lView[1] as Element).appendChild(lView[2])',
 | ||
|         ]),
 | ||
|         update: debugMatch(
 | ||
|             ['if (mask & 0b1) { (lView[2] as Text).textContent = `Hello ${lView[1]}!`; }']),
 | ||
|         icus: null
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('for multiple bindings', () => {
 | ||
|       const message = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
 | ||
|       const nbConsts = 2;
 | ||
|       const index = 1;
 | ||
|       const opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 1,
 | ||
|         create: debugMatch([
 | ||
|           'lView[2] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[2])'
 | ||
|         ]),
 | ||
|         update: debugMatch([
 | ||
|           'if (mask & 0b11) { (lView[2] as Text).textContent = `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`; }'
 | ||
|         ]),
 | ||
|         icus: null
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('for sub-templates', () => {
 | ||
|       // Template:
 | ||
|       // <div>
 | ||
|       //   {{value}} is rendered as:
 | ||
|       //   <span *ngIf>
 | ||
|       //     before <b *ngIf>middle</b> after
 | ||
|       //   </span>
 | ||
|       //   !
 | ||
|       // </div>
 | ||
|       const message =
 | ||
|           `<EFBFBD>0<EFBFBD> is rendered as: <20>*2:1<><31>#1:1<>before<72>*2:2<><32>#1:2<>middle<6C>/#1:2<><32>/*2:2<>after<65>/#1:1<><31>/*2:1<>!`;
 | ||
| 
 | ||
|       /**** Root template ****/
 | ||
|       // <20>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<>!
 | ||
|       let nbConsts = 3;
 | ||
|       let index = 1;
 | ||
|       let opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 2,
 | ||
|         create: debugMatch([
 | ||
|           'lView[3] = document.createTextNode("")', '(lView[1] as Element).appendChild(lView[3])',
 | ||
|           '(lView[1] as Element).appendChild(lView[16381])',
 | ||
|           'lView[4] = document.createTextNode("!")', '(lView[1] as Element).appendChild(lView[4])'
 | ||
|         ]),
 | ||
|         update: debugMatch([
 | ||
|           'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} is rendered as: `; }'
 | ||
|         ]),
 | ||
|         icus: null
 | ||
|       });
 | ||
| 
 | ||
| 
 | ||
|       /**** First sub-template ****/
 | ||
|       // <20>#1:1<>before<72>*2:2<>middle<6C>/*2:2<>after<65>/#1:1<>
 | ||
|       nbConsts = 3;
 | ||
|       index = 0;
 | ||
|       opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0, 1);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 2,
 | ||
|         create: debugMatch([
 | ||
|           '(lView[0] as Element).appendChild(lView[1])',
 | ||
|           'lView[3] = document.createTextNode("before")',
 | ||
|           '(lView[1] as Element).appendChild(lView[3])',
 | ||
|           '(lView[1] as Element).appendChild(lView[16381])',
 | ||
|           'lView[4] = document.createTextNode("after")',
 | ||
|           '(lView[1] as Element).appendChild(lView[4])',
 | ||
|           'setPreviousOrParentTNode(tView.data[1] as TNode)'
 | ||
|         ]),
 | ||
|         update: [],
 | ||
|         icus: null
 | ||
|       });
 | ||
| 
 | ||
| 
 | ||
|       /**** Second sub-template ****/
 | ||
|       // middle
 | ||
|       nbConsts = 2;
 | ||
|       index = 0;
 | ||
|       opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0, 2);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 1,
 | ||
|         create: debugMatch([
 | ||
|           '(lView[0] as Element).appendChild(lView[1])',
 | ||
|           'lView[2] = document.createTextNode("middle")',
 | ||
|           '(lView[1] as Element).appendChild(lView[2])',
 | ||
|           'setPreviousOrParentTNode(tView.data[1] as TNode)'
 | ||
|         ]),
 | ||
|         update: [],
 | ||
|         icus: null
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('for ICU expressions', () => {
 | ||
|       const message = `{<7B>0<EFBFBD>, plural,
 | ||
|         =0 {no <b title="none">emails</b>!}
 | ||
|         =1 {one <i>email</i>}
 | ||
|         other {<7B>0<EFBFBD> <span title="<22>1<EFBFBD>">emails</span>}
 | ||
|       }`;
 | ||
|       const nbConsts = 1;
 | ||
|       const index = 0;
 | ||
|       const opCodes = getOpCodes(message, () => {
 | ||
|                         ɵɵi18nStart(index, 0);
 | ||
|                       }, null, nbConsts, index) as TI18n;
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 6,
 | ||
|         update: debugMatch([
 | ||
|           'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 0, `${lView[1]}`); }',
 | ||
|           'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 0); }',
 | ||
|         ]),
 | ||
|         create: debugMatch([
 | ||
|           'lView[1] = document.createComment("ICU 1")',
 | ||
|           '(lView[0] as Element).appendChild(lView[1])',
 | ||
|         ]),
 | ||
|         icus: [<TIcu>{
 | ||
|           type: 1,
 | ||
|           currentCaseLViewIndex: 22,
 | ||
|           vars: [5, 4, 4],
 | ||
|           childIcus: [[], [], []],
 | ||
|           cases: ['0', '1', 'other'],
 | ||
|           create: [
 | ||
|             debugMatch([
 | ||
|               'lView[3] = document.createTextNode("no ")',
 | ||
|               '(lView[1] as Element).appendChild(lView[3])',
 | ||
|               'lView[4] = document.createElement("b")',
 | ||
|               '(lView[1] as Element).appendChild(lView[4])',
 | ||
|               '(lView[4] as Element).setAttribute("title", "none")',
 | ||
|               'lView[5] = document.createTextNode("emails")',
 | ||
|               '(lView[4] as Element).appendChild(lView[5])',
 | ||
|               'lView[6] = document.createTextNode("!")',
 | ||
|               '(lView[1] as Element).appendChild(lView[6])',
 | ||
|             ]),
 | ||
|             debugMatch([
 | ||
|               'lView[3] = document.createTextNode("one ")',
 | ||
|               '(lView[1] as Element).appendChild(lView[3])',
 | ||
|               'lView[4] = document.createElement("i")',
 | ||
|               '(lView[1] as Element).appendChild(lView[4])',
 | ||
|               'lView[5] = document.createTextNode("email")',
 | ||
|               '(lView[4] as Element).appendChild(lView[5])',
 | ||
|             ]),
 | ||
|             debugMatch([
 | ||
|               'lView[3] = document.createTextNode("")',
 | ||
|               '(lView[1] as Element).appendChild(lView[3])',
 | ||
|               'lView[4] = document.createElement("span")',
 | ||
|               '(lView[1] as Element).appendChild(lView[4])',
 | ||
|               'lView[5] = document.createTextNode("emails")',
 | ||
|               '(lView[4] as Element).appendChild(lView[5])',
 | ||
|             ])
 | ||
|           ],
 | ||
|           remove: [
 | ||
|             debugMatch([
 | ||
|               '(lView[0] as Element).remove(lView[3])',
 | ||
|               '(lView[0] as Element).remove(lView[5])',
 | ||
|               '(lView[0] as Element).remove(lView[4])',
 | ||
|               '(lView[0] as Element).remove(lView[6])',
 | ||
|             ]),
 | ||
|             debugMatch([
 | ||
|               '(lView[0] as Element).remove(lView[3])',
 | ||
|               '(lView[0] as Element).remove(lView[5])',
 | ||
|               '(lView[0] as Element).remove(lView[4])',
 | ||
|             ]),
 | ||
|             debugMatch([
 | ||
|               '(lView[0] as Element).remove(lView[3])',
 | ||
|               '(lView[0] as Element).remove(lView[5])',
 | ||
|               '(lView[0] as Element).remove(lView[4])',
 | ||
|             ])
 | ||
|           ],
 | ||
|           update: [
 | ||
|             debugMatch([]), debugMatch([]), debugMatch([
 | ||
|               'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} `; }',
 | ||
|               'if (mask & 0b10) { (lView[4] as Element).setAttribute(\'title\', `${lView[2]}`); }'
 | ||
|             ])
 | ||
|           ]
 | ||
|         }]
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('for nested ICU expressions', () => {
 | ||
|       const message = `{<7B>0<EFBFBD>, plural,
 | ||
|         =0 {zero}
 | ||
|         other {<7B>0<EFBFBD> {<7B>1<EFBFBD>, select,
 | ||
|                        cat {cats}
 | ||
|                        dog {dogs}
 | ||
|                        other {animals}
 | ||
|                      }!}
 | ||
|       }`;
 | ||
|       const nbConsts = 1;
 | ||
|       const index = 0;
 | ||
|       const opCodes = getOpCodes(message, () => {
 | ||
|         ɵɵi18nStart(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual({
 | ||
|         vars: 9,
 | ||
|         create: debugMatch([
 | ||
|           'lView[1] = document.createComment("ICU 1")',
 | ||
|           '(lView[0] as Element).appendChild(lView[1])'
 | ||
|         ]),
 | ||
|         update: debugMatch([
 | ||
|           'if (mask & 0b1) { icuSwitchCase(lView[1] as Comment, 1, `${lView[1]}`); }',
 | ||
|           'if (mask & 0b11) { icuUpdateCase(lView[1] as Comment, 1); }'
 | ||
|         ]),
 | ||
|         icus: [
 | ||
|           {
 | ||
|             type: 0,
 | ||
|             vars: [2, 2, 2],
 | ||
|             currentCaseLViewIndex: 26,
 | ||
|             childIcus: [[], [], []],
 | ||
|             cases: ['cat', 'dog', 'other'],
 | ||
|             create: [
 | ||
|               debugMatch([
 | ||
|                 'lView[7] = document.createTextNode("cats")',
 | ||
|                 '(lView[4] as Element).appendChild(lView[7])'
 | ||
|               ]),
 | ||
|               debugMatch([
 | ||
|                 'lView[7] = document.createTextNode("dogs")',
 | ||
|                 '(lView[4] as Element).appendChild(lView[7])'
 | ||
|               ]),
 | ||
|               debugMatch([
 | ||
|                 'lView[7] = document.createTextNode("animals")',
 | ||
|                 '(lView[4] as Element).appendChild(lView[7])'
 | ||
|               ]),
 | ||
|             ],
 | ||
|             remove: [
 | ||
|               debugMatch(['(lView[0] as Element).remove(lView[7])']),
 | ||
|               debugMatch(['(lView[0] as Element).remove(lView[7])']),
 | ||
|               debugMatch(['(lView[0] as Element).remove(lView[7])'])
 | ||
|             ],
 | ||
|             update: [
 | ||
|               debugMatch([]),
 | ||
|               debugMatch([]),
 | ||
|               debugMatch([]),
 | ||
|             ]
 | ||
|           },
 | ||
|           {
 | ||
|             type: 1,
 | ||
|             vars: [2, 6],
 | ||
|             childIcus: [[], [0]],
 | ||
|             currentCaseLViewIndex: 22,
 | ||
|             cases: ['0', 'other'],
 | ||
|             create: [
 | ||
|               debugMatch([
 | ||
|                 'lView[3] = document.createTextNode("zero")',
 | ||
|                 '(lView[1] as Element).appendChild(lView[3])'
 | ||
|               ]),
 | ||
|               debugMatch([
 | ||
|                 'lView[3] = document.createTextNode("")',
 | ||
|                 '(lView[1] as Element).appendChild(lView[3])',
 | ||
|                 'lView[4] = document.createComment("nested ICU 0")',
 | ||
|                 '(lView[1] as Element).appendChild(lView[4])',
 | ||
|                 'lView[5] = document.createTextNode("!")',
 | ||
|                 '(lView[1] as Element).appendChild(lView[5])'
 | ||
|               ]),
 | ||
|             ],
 | ||
|             remove: [
 | ||
|               debugMatch(['(lView[0] as Element).remove(lView[3])']),
 | ||
|               debugMatch([
 | ||
|                 '(lView[0] as Element).remove(lView[3])', '(lView[0] as Element).remove(lView[5])',
 | ||
|                 'removeNestedICU(0)', '(lView[0] as Element).remove(lView[4])'
 | ||
|               ]),
 | ||
|             ],
 | ||
|             update: [
 | ||
|               debugMatch([]),
 | ||
|               debugMatch([
 | ||
|                 'if (mask & 0b1) { (lView[3] as Text).textContent = `${lView[1]} `; }',
 | ||
|                 'if (mask & 0b10) { icuSwitchCase(lView[4] as Comment, 0, `${lView[2]}`); }',
 | ||
|                 'if (mask & 0b10) { icuUpdateCase(lView[4] as Comment, 0); }'
 | ||
|               ]),
 | ||
|             ]
 | ||
|           }
 | ||
|         ]
 | ||
|       });
 | ||
|     });
 | ||
|   });
 | ||
| 
 | ||
|   describe(`i18nAttribute`, () => {
 | ||
|     it('for text', () => {
 | ||
|       const message = `Hello world!`;
 | ||
|       const attrs = ['title', message];
 | ||
|       const nbConsts = 2;
 | ||
|       const index = 1;
 | ||
|       const fixture = prepareFixture(() => {
 | ||
|         ɵɵelementStart(0, 'div');
 | ||
|         ɵɵi18nAttributes(index, 0);
 | ||
|         ɵɵelementEnd();
 | ||
|       }, null, nbConsts, index, [attrs]);
 | ||
|       const tView = fixture.hostView[TVIEW];
 | ||
|       const opCodes = tView.data[index + HEADER_OFFSET] as I18nUpdateOpCodes;
 | ||
| 
 | ||
|       expect(opCodes).toEqual([]);
 | ||
|       expect(
 | ||
|           (getNativeByIndex(0, fixture.hostView as LView) as any as Element).getAttribute('title'))
 | ||
|           .toEqual(message);
 | ||
|     });
 | ||
| 
 | ||
|     it('for simple bindings', () => {
 | ||
|       const message = `Hello <20>0<EFBFBD>!`;
 | ||
|       const attrs = ['title', message];
 | ||
|       const nbConsts = 2;
 | ||
|       const index = 1;
 | ||
|       const opCodes = getOpCodes(attrs, () => {
 | ||
|         ɵɵi18nAttributes(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual(debugMatch([
 | ||
|         'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }'
 | ||
|       ]));
 | ||
|     });
 | ||
| 
 | ||
|     it('for multiple bindings', () => {
 | ||
|       const message = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
 | ||
|       const attrs = ['title', message];
 | ||
|       const nbConsts = 2;
 | ||
|       const index = 1;
 | ||
|       const opCodes = getOpCodes(attrs, () => {
 | ||
|         ɵɵi18nAttributes(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual(debugMatch([
 | ||
|         'if (mask & 0b11) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]} and ${lView[2]}, again ${lView[1]}!`); }'
 | ||
|       ]));
 | ||
|     });
 | ||
| 
 | ||
|     it('for multiple attributes', () => {
 | ||
|       const message = `Hello <20>0<EFBFBD>!`;
 | ||
|       const attrs = ['title', message, 'aria-label', message];
 | ||
|       const nbConsts = 2;
 | ||
|       const index = 1;
 | ||
|       const opCodes = getOpCodes(attrs, () => {
 | ||
|         ɵɵi18nAttributes(index, 0);
 | ||
|       }, null, nbConsts, index);
 | ||
| 
 | ||
|       expect(opCodes).toEqual(debugMatch([
 | ||
|         'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'title\', `Hello ${lView[1]}!`); }',
 | ||
|         'if (mask & 0b1) { (lView[0] as Element).setAttribute(\'aria-label\', `Hello ${lView[1]}!`); }'
 | ||
|       ]));
 | ||
|     });
 | ||
|   });
 | ||
| 
 | ||
|   describe('i18nPostprocess', () => {
 | ||
|     it('should handle valid cases', () => {
 | ||
|       const arr = ['<27>*1:1<><31>#2:1<>', '<27>#4:1<>', '<27>6:1<>', '<27>/#2:1<><31>/*1:1<>'];
 | ||
|       const str = `[${arr.join('|')}]`;
 | ||
| 
 | ||
|       const cases = [
 | ||
|         // empty string
 | ||
|         ['', {}, ''],
 | ||
| 
 | ||
|         // string without any special cases
 | ||
|         ['Foo [1,2,3] Bar - no ICU here', {}, 'Foo [1,2,3] Bar - no ICU here'],
 | ||
| 
 | ||
|         // multi-value cases
 | ||
|         [
 | ||
|           `Start: ${str}, ${str} and ${str}, ${str} end.`, {},
 | ||
|           `Start: ${arr[0]}, ${arr[1]} and ${arr[2]}, ${arr[3]} end.`
 | ||
|         ],
 | ||
| 
 | ||
|         // replace VAR_SELECT
 | ||
|         [
 | ||
|           'My ICU: {VAR_SELECT, select, =1 {one} other {other}}', {VAR_SELECT: '<27>1:2<>'},
 | ||
|           'My ICU: {<7B>1:2<>, select, =1 {one} other {other}}'
 | ||
|         ],
 | ||
| 
 | ||
|         [
 | ||
|           'My ICU: {\n\n\tVAR_SELECT_1 \n\n, select, =1 {one} other {other}}',
 | ||
|           {VAR_SELECT_1: '<27>1:2<>'}, 'My ICU: {\n\n\t<>1:2<> \n\n, select, =1 {one} other {other}}'
 | ||
|         ],
 | ||
| 
 | ||
|         // replace VAR_PLURAL
 | ||
|         [
 | ||
|           'My ICU: {VAR_PLURAL, plural, one {1} other {other}}', {VAR_PLURAL: '<27>1:2<>'},
 | ||
|           'My ICU: {<7B>1:2<>, plural, one {1} other {other}}'
 | ||
|         ],
 | ||
| 
 | ||
|         [
 | ||
|           'My ICU: {\n\n\tVAR_PLURAL_1 \n\n, select, =1 {one} other {other}}',
 | ||
|           {VAR_PLURAL_1: '<27>1:2<>'}, 'My ICU: {\n\n\t<>1:2<> \n\n, select, =1 {one} other {other}}'
 | ||
|         ],
 | ||
| 
 | ||
|         // do not replace VAR_* anywhere else in a string (only in ICU)
 | ||
|         [
 | ||
|           'My ICU: {VAR_PLURAL, plural, one {1} other {other}} VAR_PLURAL and VAR_SELECT',
 | ||
|           {VAR_PLURAL: '<27>1:2<>'},
 | ||
|           'My ICU: {<7B>1:2<>, plural, one {1} other {other}} VAR_PLURAL and VAR_SELECT'
 | ||
|         ],
 | ||
| 
 | ||
|         // replace VAR_*'s in nested ICUs
 | ||
|         [
 | ||
|           'My ICU: {VAR_PLURAL, plural, one {1 - {VAR_SELECT, age, 50 {fifty} other {other}}} other {other}}',
 | ||
|           {VAR_PLURAL: '<27>1:2<>', VAR_SELECT: '<27>5<EFBFBD>'},
 | ||
|           'My ICU: {<7B>1:2<>, plural, one {1 - {<7B>5<EFBFBD>, age, 50 {fifty} other {other}}} other {other}}'
 | ||
|         ],
 | ||
| 
 | ||
|         [
 | ||
|           'My ICU: {VAR_PLURAL, plural, one {1 - {VAR_PLURAL_1, age, 50 {fifty} other {other}}} other {other}}',
 | ||
|           {VAR_PLURAL: '<27>1:2<>', VAR_PLURAL_1: '<27>5<EFBFBD>'},
 | ||
|           'My ICU: {<7B>1:2<>, plural, one {1 - {<7B>5<EFBFBD>, age, 50 {fifty} other {other}}} other {other}}'
 | ||
|         ],
 | ||
| 
 | ||
|         // ICU replacement
 | ||
|         [
 | ||
|           'My ICU #1: <20>I18N_EXP_ICU<43>, My ICU #2: <20>I18N_EXP_ICU<43>',
 | ||
|           {ICU: ['ICU_VALUE_1', 'ICU_VALUE_2']}, 'My ICU #1: ICU_VALUE_1, My ICU #2: ICU_VALUE_2'
 | ||
|         ],
 | ||
| 
 | ||
|         // mixed case
 | ||
|         [
 | ||
|           `Start: ${str}, ${str}. ICU: {VAR_SELECT, count, 10 {ten} other {other}}.
 | ||
|           Another ICU: <20>I18N_EXP_ICU<43> and ${str}, ${str} and one more ICU: <20>I18N_EXP_ICU<43> and end.`,
 | ||
|           {VAR_SELECT: '<27>1:2<>', ICU: ['ICU_VALUE_1', 'ICU_VALUE_2']},
 | ||
|           `Start: ${arr[0]}, ${arr[1]}. ICU: {<7B>1:2<>, count, 10 {ten} other {other}}.
 | ||
|           Another ICU: ICU_VALUE_1 and ${arr[2]}, ${arr[3]} and one more ICU: ICU_VALUE_2 and end.`,
 | ||
|         ],
 | ||
|       ];
 | ||
|       cases.forEach(([input, replacements, output]) => {
 | ||
|         expect(ɵɵi18nPostprocess(input as string, replacements as any)).toEqual(output as string);
 | ||
|       });
 | ||
|     });
 | ||
| 
 | ||
|     it('should handle nested template represented by multi-value placeholders', () => {
 | ||
|       /**
 | ||
|        * <div i18n>
 | ||
|        *   <span>
 | ||
|        *     Hello - 1
 | ||
|        *   </span>
 | ||
|        *   <span *ngIf="visible">
 | ||
|        *     Hello - 2
 | ||
|        *     <span *ngIf="visible">
 | ||
|        *       Hello - 3
 | ||
|        *       <span *ngIf="visible">
 | ||
|        *         Hello - 4
 | ||
|        *       </span>
 | ||
|        *     </span>
 | ||
|        *   </span>
 | ||
|        *   <span>
 | ||
|        *     Hello - 5
 | ||
|        *   </span>
 | ||
|        * </div>
 | ||
|        */
 | ||
|       const generated = `
 | ||
|         [<5B>#2<>|<7C>#4<>] Bonjour - 1 [<5B>/#2<>|<7C>/#1:3<><33>/*2:3<>|<7C>/#1:2<><32>/*2:2<>|<7C>/#1:1<><31>/*3:1<>|<7C>/#4<>]
 | ||
|         [<5B>*3:1<><31>#1:1<>|<7C>*2:2<><32>#1:2<>|<7C>*2:3<><33>#1:3<>]
 | ||
|           Bonjour - 2
 | ||
|           [<5B>*3:1<><31>#1:1<>|<7C>*2:2<><32>#1:2<>|<7C>*2:3<><33>#1:3<>]
 | ||
|             Bonjour - 3
 | ||
|             [<5B>*3:1<><31>#1:1<>|<7C>*2:2<><32>#1:2<>|<7C>*2:3<><33>#1:3<>] Bonjour - 4 [<5B>/#2<>|<7C>/#1:3<><33>/*2:3<>|<7C>/#1:2<><32>/*2:2<>|<7C>/#1:1<><31>/*3:1<>|<7C>/#4<>]
 | ||
|           [<5B>/#2<>|<7C>/#1:3<><33>/*2:3<>|<7C>/#1:2<><32>/*2:2<>|<7C>/#1:1<><31>/*3:1<>|<7C>/#4<>]
 | ||
|         [<5B>/#2<>|<7C>/#1:3<><33>/*2:3<>|<7C>/#1:2<><32>/*2:2<>|<7C>/#1:1<><31>/*3:1<>|<7C>/#4<>]
 | ||
|         [<5B>#2<>|<7C>#4<>] Bonjour - 5 [<5B>/#2<>|<7C>/#1:3<><33>/*2:3<>|<7C>/#1:2<><32>/*2:2<>|<7C>/#1:1<><31>/*3:1<>|<7C>/#4<>]
 | ||
|       `;
 | ||
|       const final = `
 | ||
|         <20>#2<> Bonjour - 1 <20>/#2<>
 | ||
|         <20>*3:1<>
 | ||
|           <20>#1:1<>
 | ||
|             Bonjour - 2
 | ||
|             <20>*2:2<>
 | ||
|               <20>#1:2<>
 | ||
|                 Bonjour - 3
 | ||
|                 <20>*2:3<>
 | ||
|                   <20>#1:3<> Bonjour - 4 <20>/#1:3<>
 | ||
|                 <20>/*2:3<>
 | ||
|               <20>/#1:2<>
 | ||
|             <20>/*2:2<>
 | ||
|           <20>/#1:1<>
 | ||
|         <20>/*3:1<>
 | ||
|         <20>#4<> Bonjour - 5 <20>/#4<>
 | ||
|       `;
 | ||
|       expect(ɵɵi18nPostprocess(generated.replace(/\s+/g, ''))).toEqual(final.replace(/\s+/g, ''));
 | ||
|     });
 | ||
| 
 | ||
|     it('should throw in case we have invalid string', () => {
 | ||
|       expect(
 | ||
|           () => ɵɵi18nPostprocess(
 | ||
|               'My ICU #1: <20>I18N_EXP_ICU<43>, My ICU #2: <20>I18N_EXP_ICU<43>', {ICU: ['ICU_VALUE_1']}))
 | ||
|           .toThrowError();
 | ||
|     });
 | ||
|   });
 | ||
| }); |