/**
* @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 {noop} from '../../../compiler/src/render3/view/util';
import {Component as _Component} from '../../src/core';
import {defineComponent} from '../../src/render3/definition';
import {getTranslationForTemplate, i18n, i18nApply, i18nAttributes, i18nEnd, i18nExp, i18nPostprocess, i18nStart} from '../../src/render3/i18n';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {getNativeByIndex} from '../../src/render3/util';
import {NgIf} from './common_with_def';
import {element, elementEnd, elementStart, template, text, bind, elementProperty, projectionDef, projection} from '../../src/render3/instructions';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n';
import {HEADER_OFFSET, LViewData, TVIEW} from '../../src/render3/interfaces/view';
import {ComponentFixture, TemplateFixture} from './render_util';
const Component: typeof _Component = function(...args: any[]): any {
// In test we use @Component for documentation only so it's safe to mock out the implementation.
return () => undefined;
} as any;
describe('Runtime i18n', () => {
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): TemplateFixture {
return new TemplateFixture(createTemplate, updateTemplate || noop, nbConsts, nbVars);
}
function getOpCodes(
createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts: number,
index: number): TI18n|I18nUpdateOpCodes {
const fixture = prepareFixture(createTemplate, updateTemplate, nbConsts);
const tView = fixture.hostView[TVIEW];
return tView.data[index + HEADER_OFFSET] as TI18n;
}
describe('i18nStart', () => {
it('for text', () => {
const MSG_DIV = `simple text`;
const nbConsts = 1;
const index = 0;
const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 1,
expandoStartIndex: nbConsts,
create:
['simple text', index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild],
update: [],
icus: null
});
});
it('for elements', () => {
const MSG_DIV = `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 elementIndex = 2;
const elementIndex2 = 3;
const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 5,
expandoStartIndex: nbConsts,
create: [
'Hello ',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'world',
elementIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
' and ',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'universe',
elementIndex2 << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
'!',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
],
update: [],
icus: null
});
});
it('for simple bindings', () => {
const MSG_DIV = `Hello �0�!`;
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 1,
expandoStartIndex: nbConsts,
create: ['', index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild],
update: [
0b1, // bindings mask
4, // if no update, skip 4
'Hello ',
-1, // binding index
'!', (index + 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text
],
icus: null
});
});
it('for multiple bindings', () => {
const MSG_DIV = `Hello �0� and �1�, again �0�!`;
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 1,
expandoStartIndex: nbConsts,
create: ['', index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild],
update: [
0b11, // bindings mask
8, // if no update, skip 8
'Hello ', -1, ' and ', -2, ', again ', -1, '!',
(index + 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text
],
icus: null
});
});
it('for sub-templates', () => {
// Template:
//
// {{value}} is rendered as:
//
// before middle after
//
// !
//
const MSG_DIV =
`�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;
const firstTextNode = 3;
let opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 2,
expandoStartIndex: nbConsts,
create: [
'',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'!',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
],
update: [
0b1, // bindings mask
3, // if no update, skip 3
-1, // binding index
' is rendered as: ', firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text
],
icus: null
});
/**** First sub-template ****/
// �#1:1�before�*2:2�middle�/*2:2�after�/#1:1�
nbConsts = 3;
index = 0;
const spanElement = 1;
const bElementSubTemplate = 2;
opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV, 1); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 2,
expandoStartIndex: nbConsts,
create: [
spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'before',
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'after',
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
],
update: [],
icus: null
});
/**** Second sub-template ****/
// middle
nbConsts = 2;
index = 0;
const bElement = 1;
opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV, 2); }, null, nbConsts, index);
expect(opCodes).toEqual({
vars: 1,
expandoStartIndex: nbConsts,
create: [
bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'middle',
bElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
],
update: [],
icus: null
});
});
it('for ICU expressions', () => {
const MSG_DIV = `{�0�, plural,
=0 {no
emails!}
=1 {one
email}
other {�0�
emails}
}`;
const nbConsts = 1;
const index = 0;
const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
const tIcuIndex = 0;
const icuCommentNodeIndex = index + 1;
const firstTextNode = index + 2;
const bElementNodeIndex = index + 3;
const iElementNodeIndex = index + 3;
const spanElementNodeIndex = index + 3;
const innerTextNode = index + 4;
const lastTextNode = index + 5;
expect(opCodes).toEqual({
vars: 5,
expandoStartIndex: nbConsts,
create: [
COMMENT_MARKER, 'ICU 1',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
],
update: [
0b1, // mask for ICU main binding
3, // skip 3 if not changed
-1, // icu main binding
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex,
0b11, // mask for all ICU bindings
2, // skip 2 if not changed
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex
],
icus: [{
type: 1,
vars: [4, 3, 3],
expandoStartIndex: icuCommentNodeIndex + 1,
childIcus: [[], [], []],
cases: ['0', '1', 'other'],
create: [
[
'no ',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
ELEMENT_MARKER,
'b',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr,
'title',
'none',
'emails',
bElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'!',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
],
[
'one ',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
ELEMENT_MARKER, 'i',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'email',
iElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
],
[
'',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
ELEMENT_MARKER, 'span',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'emails',
spanElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
]
],
remove: [
[
firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
],
[
firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
iElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
],
[
firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
spanElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
]
],
update: [
[], [],
[
0b1, // mask for the first binding
3, // skip 3 if not changed
-1, // binding index
' ', // text string to concatenate to the binding value
firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text,
0b10, // mask for the title attribute binding
4, // skip 4 if not changed
-2, // binding index
bElementNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr,
'title', // attribute name
null // sanitize function
]
]
}]
});
});
it('for nested ICU expressions', () => {
const MSG_DIV = `{�0�, plural,
=0 {zero}
other {�0� {�1�, select,
cat {cats}
dog {dogs}
other {animals}
}!}
}`;
const nbConsts = 1;
const index = 0;
const opCodes = getOpCodes(() => { i18nStart(index, MSG_DIV); }, null, nbConsts, index);
const icuCommentNodeIndex = index + 1;
const firstTextNode = index + 2;
const nestedIcuCommentNodeIndex = index + 3;
const lastTextNode = index + 4;
const nestedTextNode = index + 5;
const tIcuIndex = 1;
const nestedTIcuIndex = 0;
expect(opCodes).toEqual({
vars: 6,
expandoStartIndex: nbConsts,
create: [
COMMENT_MARKER, 'ICU 1',
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
],
update: [
0b1, // mask for ICU main binding
3, // skip 3 if not changed
-1, // icu main binding
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex,
0b11, // mask for all ICU bindings
2, // skip 2 if not changed
icuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex
],
icus: [
{
type: 0,
vars: [1, 1, 1],
expandoStartIndex: lastTextNode + 1,
childIcus: [[], [], []],
cases: ['cat', 'dog', 'other'],
create: [
[
'cats', nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT |
I18nMutateOpCode.AppendChild
],
[
'dogs', nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT |
I18nMutateOpCode.AppendChild
],
[
'animals', nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT |
I18nMutateOpCode.AppendChild
]
],
remove: [
[nestedTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
[nestedTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
[nestedTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove]
],
update: [[], [], []]
},
{
type: 1,
vars: [1, 4],
expandoStartIndex: icuCommentNodeIndex + 1,
childIcus: [[], [0]],
cases: ['0', 'other'],
create: [
[
'zero',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
],
[
'',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
COMMENT_MARKER, 'nested ICU 0',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
'!',
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
]
],
remove: [
[firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
[
firstTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
0 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
nestedIcuCommentNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
]
],
update: [
[],
[
0b1, // mask for ICU main binding
3, // skip 3 if not changed
-1, // binding index
' ', // text string to concatenate to the binding value
firstTextNode << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Text,
0b10, // mask for inner ICU main binding
3, // skip 3 if not changed
-2, // inner ICU main binding
nestedIcuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF |
I18nUpdateOpCode.IcuSwitch,
nestedTIcuIndex,
0b10, // mask for all inner ICU bindings
2, // skip 2 if not changed
nestedIcuCommentNodeIndex << I18nUpdateOpCode.SHIFT_REF |
I18nUpdateOpCode.IcuUpdate,
nestedTIcuIndex
]
]
}
]
});
});
});
describe(`i18nEnd`, () => {
it('for text', () => {
const MSG_DIV = `simple text`;
const fixture = prepareFixture(() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
}, null, 2);
expect(fixture.html).toEqual(`
${MSG_DIV}
`);
});
it('for bindings', () => {
const MSG_DIV = `Hello �0�!`;
const fixture = prepareFixture(() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
}, null, 2);
// Template should be empty because there is no update template function
expect(fixture.html).toEqual('
');
// But it should have created an empty text node in `viewData`
const textTNode = fixture.hostView[HEADER_OFFSET + 2] as Node;
expect(textTNode.nodeType).toEqual(Node.TEXT_NODE);
});
it('for elements', () => {
const MSG_DIV = `Hello �#3�world�/#3� and �#2�universe�/#2�!`;
let fixture = prepareFixture(() => {
elementStart(0, 'div');
i18nStart(1, MSG_DIV);
element(2, 'div');
element(3, 'span');
i18nEnd();
elementEnd();
}, null, 4);
expect(fixture.html).toEqual('
Hello
world and
universe
!
');
});
it('for translations without top level element', () => {
// When it's the first node
let MSG_DIV = `Hello world`;
let fixture = prepareFixture(() => { i18n(0, MSG_DIV); }, null, 1);
expect(fixture.html).toEqual('Hello world');
// When the first node is a text node
MSG_DIV = ` world`;
fixture = prepareFixture(() => {
text(0, 'Hello');
i18n(1, MSG_DIV);
}, null, 2);
expect(fixture.html).toEqual('Hello world');
// When the first node is an element
fixture = prepareFixture(() => {
elementStart(0, 'div');
text(1, 'Hello');
elementEnd();
i18n(2, MSG_DIV);
}, null, 3);
expect(fixture.html).toEqual('
Hello
world');
// When there is a node after
MSG_DIV = `Hello `;
fixture = prepareFixture(() => {
i18n(0, MSG_DIV);
text(1, 'world');
}, null, 2);
expect(fixture.html).toEqual('Hello world');
});
it('for deleted placeholders', () => {
const MSG_DIV = `Hello �#3�world�/#3�`;
let fixture = prepareFixture(() => {
elementStart(0, 'div');
{
i18nStart(1, MSG_DIV);
{
element(2, 'div'); // Will be removed
element(3, 'span');
}
i18nEnd();
}
elementEnd();
elementStart(4, 'div');
{ text(5, '!'); }
elementEnd();
}, null, 6);
expect(fixture.html).toEqual('
Hello world
!
');
});
it('for sub-templates', () => {
// Template: `
Content:
beforemiddleafter
!
`;
const MSG_DIV =
`Content: �*2:1��#1:1�before�*2:2��#1:2�middle�/#1:2��/*2:2�after�/#1:1��/*2:1�!`;
function subTemplate_1(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
i18nStart(0, MSG_DIV, 1);
elementStart(1, 'div');
template(2, subTemplate_2, 2, 0, null, ['ngIf', '']);
elementEnd();
i18nEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(2, 'ngIf', bind(true));
}
}
function subTemplate_2(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
i18nStart(0, MSG_DIV, 2);
element(1, 'span');
i18nEnd();
}
}
class MyApp {
static ngComponentDef = defineComponent({
type: MyApp,
selectors: [['my-app']],
directives: [NgIf],
factory: () => new MyApp(),
consts: 3,
vars: 1,
template: (rf: RenderFlags, ctx: MyApp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
i18nStart(1, MSG_DIV);
template(2, subTemplate_1, 3, 1, null, ['ngIf', '']);
i18nEnd();
elementEnd();
}
if (rf & RenderFlags.Update) {
elementProperty(2, 'ngIf', true);
}
}
});
}
const fixture = new ComponentFixture(MyApp);
expect(fixture.html)
.toEqual('
Content:
beforemiddleafter
!
');
});
it('for ICU expressions', () => {
const MSG_DIV = `{�0�, plural,
=0 {no
emails!}
=1 {one
email}
other {�0�
emails}
}`;
const fixture = prepareFixture(() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
}, null, 2);
// Template should be empty because there is no update template function
expect(fixture.html).toEqual('
');
});
it('for nested ICU expressions', () => {
const MSG_DIV = `{�0�, plural,
=0 {zero}
other {�0� {�1�, select,
cat {cats}
dog {dogs}
other {animals}
}!}
}`;
const fixture = prepareFixture(() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
}, null, 2);
// Template should be empty because there is no update template function
expect(fixture.html).toEqual('
');
});
});
describe(`i18nAttribute`, () => {
it('for text', () => {
const MSG_title = `Hello world!`;
const MSG_div_attr = ['title', MSG_title];
const nbConsts = 2;
const index = 1;
const fixture = prepareFixture(() => {
elementStart(0, 'div');
i18nAttributes(index, MSG_div_attr);
elementEnd();
}, null, nbConsts, index);
const tView = fixture.hostView[TVIEW];
const opCodes = tView.data[index + HEADER_OFFSET] as I18nUpdateOpCodes;
expect(opCodes).toEqual([]);
expect((getNativeByIndex(0, fixture.hostView as LViewData) as any as Element)
.getAttribute('title'))
.toEqual(MSG_title);
});
it('for simple bindings', () => {
const MSG_title = `Hello �0�!`;
const MSG_div_attr = ['title', MSG_title];
const nbConsts = 2;
const index = 1;
const opCodes =
getOpCodes(() => { i18nAttributes(index, MSG_div_attr); }, null, nbConsts, index);
expect(opCodes).toEqual([
0b1, // bindings mask
6, // if no update, skip 4
'Hello ',
-1, // binding index
'!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null
]);
});
it('for multiple bindings', () => {
const MSG_title = `Hello �0� and �1�, again �0�!`;
const MSG_div_attr = ['title', MSG_title];
const nbConsts = 2;
const index = 1;
const opCodes =
getOpCodes(() => { i18nAttributes(index, MSG_div_attr); }, null, nbConsts, index);
expect(opCodes).toEqual([
0b11, // bindings mask
10, // size
'Hello ', -1, ' and ', -2, ', again ', -1, '!',
(index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null
]);
});
it('for multiple attributes', () => {
const MSG_title = `Hello �0�!`;
const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title];
const nbConsts = 2;
const index = 1;
const opCodes =
getOpCodes(() => { i18nAttributes(index, MSG_div_attr); }, null, nbConsts, index);
expect(opCodes).toEqual([
0b1, // bindings mask
6, // if no update, skip 4
'Hello ',
-1, // binding index
'!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null,
0b1, // bindings mask
6, // if no update, skip 4
'Hello ',
-1, // binding index
'!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'aria-label', null
]);
});
});
describe(`i18nExp & i18nApply`, () => {
it('for text bindings', () => {
const MSG_DIV = `Hello �0�!`;
const ctx = {value: 'world'};
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
},
() => {
i18nExp(bind(ctx.value));
i18nApply(1);
},
2, 1);
// Template should be empty because there is no update template function
expect(fixture.html).toEqual('
Hello world!
');
});
it('for attribute bindings', () => {
const MSG_title = `Hello �0�!`;
const MSG_div_attr = ['title', MSG_title];
const ctx = {value: 'world'};
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18nAttributes(1, MSG_div_attr);
elementEnd();
},
() => {
i18nExp(bind(ctx.value));
i18nApply(1);
},
2, 1);
expect(fixture.html).toEqual('
');
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html).toEqual('
');
ctx.value = 'universe';
fixture.update();
expect(fixture.html).toEqual('
');
});
it('for attributes with no bindings', () => {
const MSG_title = `Hello world!`;
const MSG_div_attr = ['title', MSG_title];
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18nAttributes(1, MSG_div_attr);
elementEnd();
},
() => { i18nApply(1); }, 2, 1);
expect(fixture.html).toEqual('
');
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html).toEqual('
');
});
it('for multiple attribute bindings', () => {
const MSG_title = `Hello �0� and �1�, again �0�!`;
const MSG_div_attr = ['title', MSG_title];
const ctx = {value0: 'world', value1: 'universe'};
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18nAttributes(1, MSG_div_attr);
elementEnd();
},
() => {
i18nExp(bind(ctx.value0));
i18nExp(bind(ctx.value1));
i18nApply(1);
},
2, 2);
expect(fixture.html).toEqual('
');
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html).toEqual('
');
ctx.value0 = 'earth';
fixture.update();
expect(fixture.html).toEqual('
');
ctx.value0 = 'earthlings';
ctx.value1 = 'martians';
fixture.update();
expect(fixture.html)
.toEqual('
');
});
it('for bindings of multiple attributes', () => {
const MSG_title = `Hello �0�!`;
const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title];
const ctx = {value: 'world'};
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18nAttributes(1, MSG_div_attr);
elementEnd();
},
() => {
i18nExp(bind(ctx.value));
i18nApply(1);
},
2, 1);
expect(fixture.html).toEqual('
');
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html).toEqual('
');
ctx.value = 'universe';
fixture.update();
expect(fixture.html)
.toEqual('
');
});
it('for ICU expressions', () => {
const MSG_DIV = `{�0�, plural,
=0 {no
emails!}
=1 {one
email}
other {�0�
emails}
}`;
const ctx = {value0: 0, value1: 'emails label'};
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
},
() => {
i18nExp(bind(ctx.value0));
i18nExp(bind(ctx.value1));
i18nApply(1);
},
2, 2);
expect(fixture.html).toEqual('
no emails!
');
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html).toEqual('
no emails!
');
ctx.value0 = 1;
fixture.update();
expect(fixture.html).toEqual('
one email
');
ctx.value0 = 10;
fixture.update();
expect(fixture.html)
.toEqual('
10 emails
');
ctx.value1 = '10 emails';
fixture.update();
expect(fixture.html)
.toEqual('
10 emails
');
ctx.value0 = 0;
fixture.update();
expect(fixture.html).toEqual('
no emails!
');
});
it('for nested ICU expressions', () => {
const MSG_DIV = `{�0�, plural,
=0 {zero}
other {�0� {�1�, select,
cat {cats}
dog {dogs}
other {animals}
}!}
}`;
const ctx = {value0: 0, value1: 'cat'};
const fixture = prepareFixture(
() => {
elementStart(0, 'div');
i18n(1, MSG_DIV);
elementEnd();
},
() => {
i18nExp(bind(ctx.value0));
i18nExp(bind(ctx.value1));
i18nApply(1);
},
2, 2);
expect(fixture.html).toEqual('
zero
');
// Change detection cycle, no model changes
fixture.update();
expect(fixture.html).toEqual('
zero
');
ctx.value0 = 10;
fixture.update();
expect(fixture.html).toEqual('
10 cats!
');
ctx.value1 = 'squirrel';
fixture.update();
expect(fixture.html).toEqual('
10 animals!
');
ctx.value0 = 0;
fixture.update();
expect(fixture.html).toEqual('
zero
');
});
});
describe('integration', () => {
it('should support multiple i18n blocks', () => {
// Translated template:
//
const MSG_DIV_1 = `trad �0�`;
const MSG_DIV_2_ATTR = ['title', `start �1� middle �0� end`];
const MSG_DIV_2 = `�#9��/#9��#7�trad�/#7�`;
class MyApp {
exp1 = '1';
exp2 = '2';
static ngComponentDef = defineComponent({
type: MyApp,
selectors: [['my-app']],
factory: () => new MyApp(),
consts: 10,
vars: 2,
template: (rf: RenderFlags, ctx: MyApp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{
elementStart(1, 'a');
{ i18n(2, MSG_DIV_1); }
elementEnd();
text(3, 'hello');
elementStart(4, 'b');
{
i18nAttributes(5, MSG_DIV_2_ATTR);
i18nStart(6, MSG_DIV_2);
{
element(7, 'c');
element(8, 'd'); // will be removed
element(9, 'e'); // will be moved before `c`
}
i18nEnd();
}
elementEnd();
}
elementEnd();
}
if (rf & RenderFlags.Update) {
i18nExp(bind(ctx.exp1));
i18nApply(2);
i18nExp(bind(ctx.exp1));
i18nExp(bind(ctx.exp2));
i18nApply(5);
}
}
});
}
const fixture = new ComponentFixture(MyApp);
expect(fixture.html)
.toEqual(
`
`);
});
it('should support attribute translations on removed elements', () => {
// Translated template:
//
// trad {{exp1}}
//
const MSG_DIV_1 = `trad �0�`;
const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`];
class MyApp {
exp1 = '1';
exp2 = '2';
static ngComponentDef = defineComponent({
type: MyApp,
selectors: [['my-app']],
factory: () => new MyApp(),
consts: 5,
vars: 5,
template: (rf: RenderFlags, ctx: MyApp) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{
i18nAttributes(1, MSG_DIV_1_ATTR_1);
i18nStart(2, MSG_DIV_1);
{
elementStart(3, 'b'); // Will be removed
{ i18nAttributes(4, MSG_DIV_1_ATTR_1); }
elementEnd();
}
i18nEnd();
}
elementEnd();
}
if (rf & RenderFlags.Update) {
i18nExp(bind(ctx.exp1));
i18nExp(bind(ctx.exp2));
i18nApply(1);
i18nExp(bind(ctx.exp1));
i18nApply(2);
i18nExp(bind(ctx.exp1));
i18nExp(bind(ctx.exp2));
i18nApply(4);
}
}
});
}
const fixture = new ComponentFixture(MyApp);
expect(fixture.html).toEqual(`
trad 1
`);
});
describe('projection', () => {
it('should project the translations', () => {
@Component({selector: 'child', template: '
'})
class Child {
static ngComponentDef = defineComponent({
type: Child,
selectors: [['child']],
factory: () => new Child(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, cmp: Child) => {
if (rf & RenderFlags.Create) {
projectionDef();
elementStart(0, 'p');
{ projection(1); }
elementEnd();
}
}
});
}
const MSG_DIV_SECTION_1 = `�#2�Je suis projeté depuis �#3��0��/#3��/#2�`;
const MSG_ATTR_1 = ['title', `Enfant de �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(),
consts: 8,
vars: 2,
template: (rf: RenderFlags, cmp: Parent) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{
i18nStart(1, MSG_DIV_SECTION_1);
{
elementStart(2, 'child');
{
elementStart(3, 'b');
{
i18nAttributes(4, MSG_ATTR_1);
element(5, 'remove-me-1');
}
elementEnd();
element(6, 'remove-me-2');
}
elementEnd();
element(7, 'remove-me-3');
}
i18nEnd();
}
elementEnd();
}
if (rf & RenderFlags.Update) {
i18nExp(bind(cmp.name));
i18nApply(1);
i18nExp(bind(cmp.name));
i18nApply(4);
}
}
});
}
const fixture = new ComponentFixture(Parent);
expect(fixture.html)
.toEqual(
'
Je suis projeté depuis Parent
');
//
//
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(),
consts: 2,
vars: 0,
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 �0�`;
const MSG_ATTR_1 = ['title', `Enfant de �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(),
consts: 7,
vars: 2,
template: (rf: RenderFlags, cmp: Parent) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'div');
{
elementStart(1, 'child');
{
element(2, 'any');
elementStart(3, 'b');
{
i18nAttributes(4, MSG_ATTR_1);
i18n(5, MSG_DIV_SECTION_1);
}
elementEnd();
element(6, 'any');
}
elementEnd();
}
elementEnd();
}
if (rf & RenderFlags.Update) {
i18nExp(bind(cmp.name));
i18nApply(4);
i18nExp(bind(cmp.name));
i18nApply(5);
}
}
});
}
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(),
consts: 2,
vars: 0,
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(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, cmp: Child) => {
if (rf & RenderFlags.Create) {
projectionDef();
elementStart(0, 'grand-child');
{ projection(1); }
elementEnd();
}
}
});
}
const MSG_DIV_SECTION_1 = `�#2�Bonjour�/#2� Monde!`;
@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(),
consts: 3,
vars: 0,
template: (rf: RenderFlags, cmp: Parent) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
{
i18nStart(1, MSG_DIV_SECTION_1);
{ element(2, 'b'); }
i18nEnd();
}
elementEnd();
}
}
});
}
const fixture = new ComponentFixture(Parent);
expect(fixture.html)
.toEqual('
Bonjour Monde!
');
//
Bonjour
//
Bonjour Monde!
});
xit('should re-project translations when removed placeholders', () => {
@Component({selector: 'grand-child', template: '
'})
class GrandChild {
static ngComponentDef = defineComponent({
type: GrandChild,
selectors: [['grand-child']],
factory: () => new GrandChild(),
consts: 3,
vars: 0,
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(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, cmp: Child) => {
if (rf & RenderFlags.Create) {
projectionDef();
elementStart(0, 'grand-child');
{ projection(1); }
elementEnd();
}
}
});
}
const MSG_DIV_SECTION_1 = `Bonjour Monde!`;
@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(),
consts: 2,
vars: 0,
template: (rf: RenderFlags, cmp: Parent) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
{
i18nStart(1, MSG_DIV_SECTION_1);
{
element(2, 'b'); // will be removed
}
i18nEnd();
}
elementEnd();
}
}
});
}
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(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, cmp: Child) => {
if (rf & RenderFlags.Create) {
projectionDef([[['span']]], ['span']);
projection(0, 1);
}
}
});
}
const MSG_DIV_SECTION_1 = `�#2�Contenu�/#2�`;
@Component({
selector: 'parent',
template: `
`
// Translated to:
//
Contenu
})
class Parent {
static ngComponentDef = defineComponent({
type: Parent,
selectors: [['parent']],
directives: [Child],
factory: () => new Parent(),
consts: 4,
vars: 0,
template: (rf: RenderFlags, cmp: Parent) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
{
i18nStart(1, MSG_DIV_SECTION_1);
{
element(2, 'span', ['title', 'keepMe']);
element(3, 'span', ['title', 'deleteMe']);
}
i18nEnd();
}
elementEnd();
}
}
});
}
const fixture = new ComponentFixture(Parent);
expect(fixture.html).toEqual('
Contenu');
});
});
});
describe('i18nPostprocess', () => {
it('should handle valid cases', () => {
const arr = ['�*1:1��#2:1�', '�#4:2�', '�6:4�', '�/#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 throw in case we have invalid string', () => {
const arr = ['�*1:1��#2:1�', '�#4:2�', '�6:4�', '�/#2:1��/*1:1�'];
const str = `[${arr.join('|')}]`;
const cases = [
// less placeholders than we have
[`Start: ${str}, ${str} and ${str} end.`, {}],
// more placeholders than we have
[`Start: ${str}, ${str} and ${str}, ${str} ${str} end.`, {}],
// not enough ICU replacements
['My ICU #1: �I18N_EXP_ICU�, My ICU #2: �I18N_EXP_ICU�', {ICU: ['ICU_VALUE_1']}]
];
cases.forEach(([input, replacements, output]) => {
expect(() => i18nPostprocess(input as string, replacements as any)).toThrowError();
});
});
});
});