/**
* @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 �0�!`;
expect(getTranslationForTemplate(message)).toEqual(message);
message = `Hello �#2��0��/#2�!`;
expect(getTranslationForTemplate(message)).toEqual(message);
// Embedded sub-templates
message = `�0� is rendered as: �*2:1�before�*1:2�middle�/*1:2�after�/*2:1�!`;
expect(getTranslationForTemplate(message)).toEqual('�0� is rendered as: �*2:1��/*2:1�!');
expect(getTranslationForTemplate(message, 1)).toEqual('before�*1:2��/*1:2�after');
expect(getTranslationForTemplate(message, 2)).toEqual('middle');
// Embedded & sibling sub-templates
message =
`�0� is rendered as: �*2:1�before�*1:2�middle�/*1:2�after�/*2:1� and also �*4:3�before�*1:4�middle�/*1:4�after�/*4:3�!`;
expect(getTranslationForTemplate(message))
.toEqual('�0� is rendered as: �*2:1��/*2:1� and also �*4:3��/*4:3�!');
expect(getTranslationForTemplate(message, 1)).toEqual('before�*1:2��/*1:2�after');
expect(getTranslationForTemplate(message, 2)).toEqual('middle');
expect(getTranslationForTemplate(message, 3)).toEqual('before�*1:4��/*1:4�after');
expect(getTranslationForTemplate(message, 4)).toEqual('middle');
});
it('should throw if the template is malformed', () => {
const message = `�*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 �#2�world�/#2� and �#3�universe�/#3�!`;
// Template: `
Hello
world
and
universe!`
// 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 �0�!`;
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 �0� and �1�, again �0�!`;
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:
//
// {{value}} is rendered as:
//
// before middle after
//
// !
//
const message =
`�0� is rendered as: �*2:1��#1:1�before�*2:2��#1:2�middle�/#1:2��/*2:2�after�/#1:1��/*2:1�!`;
/**** Root template ****/
// �0� is rendered as: �*2:1��/*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 ****/
// �#1:1�before�*2:2�middle�/*2:2�after�/#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 = `{�0�, plural,
=0 {no
emails!}
=1 {one
email}
other {�0�
emails}
}`;
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: [
{
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 = `{�0�, plural,
=0 {zero}
other {�0� {�1�, 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 �0�!`;
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 �0� and �1�, again �0�!`;
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 �0�!`;
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 = ['�*1:1��#2:1�', '�#4:1�', '�6:1�', '�/#2:1��/*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: '�1:2�'},
'My ICU: {�1:2�, select, =1 {one} other {other}}'
],
[
'My ICU: {\n\n\tVAR_SELECT_1 \n\n, select, =1 {one} other {other}}',
{VAR_SELECT_1: '�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: '�1:2�'},
'My ICU: {�1:2�, plural, one {1} other {other}}'
],
[
'My ICU: {\n\n\tVAR_PLURAL_1 \n\n, select, =1 {one} other {other}}',
{VAR_PLURAL_1: '�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: '�1:2�'},
'My ICU: {�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: '�1:2�', VAR_SELECT: '�5�'},
'My ICU: {�1:2�, plural, one {1 - {�5�, 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: '�1:2�', VAR_PLURAL_1: '�5�'},
'My ICU: {�1:2�, plural, one {1 - {�5�, age, 50 {fifty} other {other}}} other {other}}'
],
// ICU replacement
[
'My ICU #1: �I18N_EXP_ICU�, My ICU #2: �I18N_EXP_ICU�',
{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: �I18N_EXP_ICU� and ${str}, ${str} and one more ICU: �I18N_EXP_ICU� and end.`,
{VAR_SELECT: '�1:2�', ICU: ['ICU_VALUE_1', 'ICU_VALUE_2']},
`Start: ${arr[0]}, ${arr[1]}. ICU: {�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', () => {
/**
*
*
* Hello - 1
*
*
* Hello - 2
*
* Hello - 3
*
* Hello - 4
*
*
*
*
* Hello - 5
*
*
*/
const generated = `
[�#2�|�#4�] Bonjour - 1 [�/#2�|�/#1:3��/*2:3�|�/#1:2��/*2:2�|�/#1:1��/*3:1�|�/#4�]
[�*3:1��#1:1�|�*2:2��#1:2�|�*2:3��#1:3�]
Bonjour - 2
[�*3:1��#1:1�|�*2:2��#1:2�|�*2:3��#1:3�]
Bonjour - 3
[�*3:1��#1:1�|�*2:2��#1:2�|�*2:3��#1:3�] Bonjour - 4 [�/#2�|�/#1:3��/*2:3�|�/#1:2��/*2:2�|�/#1:1��/*3:1�|�/#4�]
[�/#2�|�/#1:3��/*2:3�|�/#1:2��/*2:2�|�/#1:1��/*3:1�|�/#4�]
[�/#2�|�/#1:3��/*2:3�|�/#1:2��/*2:2�|�/#1:1��/*3:1�|�/#4�]
[�#2�|�#4�] Bonjour - 5 [�/#2�|�/#1:3��/*2:3�|�/#1:2��/*2:2�|�/#1:1��/*3:1�|�/#4�]
`;
const final = `
�#2� Bonjour - 1 �/#2�
�*3:1�
�#1:1�
Bonjour - 2
�*2:2�
�#1:2�
Bonjour - 3
�*2:3�
�#1:3� Bonjour - 4 �/#1:3�
�/*2:3�
�/#1:2�
�/*2:2�
�/#1:1�
�/*3:1�
�#4� Bonjour - 5 �/#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: �I18N_EXP_ICU�, My ICU #2: �I18N_EXP_ICU�', {ICU: ['ICU_VALUE_1']}))
.toThrowError();
});
});
});