802 lines
31 KiB
TypeScript
802 lines
31 KiB
TypeScript
/**
|
||
* @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 {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n';
|
||
import {ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all';
|
||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n';
|
||
import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view';
|
||
import {getNativeByIndex} from '../../src/render3/util/view_utils';
|
||
import {TemplateFixture} from './render_util';
|
||
|
||
describe('Runtime i18n', () => {
|
||
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): 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);
|
||
|
||
// Check debug
|
||
const debugOps = (opCodes as any).create.debug !.operations;
|
||
expect(debugOps[0].__raw_opCode).toBe('simple text');
|
||
expect(debugOps[0].type).toBe('Create Text Node');
|
||
expect(debugOps[0].nodeIndex).toBe(1);
|
||
expect(debugOps[0].text).toBe('simple text');
|
||
expect(debugOps[1].__raw_opCode).toBe(1);
|
||
expect(debugOps[1].type).toBe('AppendChild');
|
||
expect(debugOps[1].nodeIndex).toBe(0);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 1,
|
||
create: [
|
||
'simple text', nbConsts,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||
],
|
||
update: [],
|
||
icus: null
|
||
});
|
||
});
|
||
|
||
it('for elements', () => {
|
||
const MSG_DIV = `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 elementIndex = 2;
|
||
const elementIndex2 = 3;
|
||
const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 5,
|
||
create: [
|
||
'Hello ',
|
||
nbConsts,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'world',
|
||
nbConsts + 1,
|
||
elementIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
elementIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||
' and ',
|
||
nbConsts + 2,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'universe',
|
||
nbConsts + 3,
|
||
elementIndex2 << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
elementIndex2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||
'!',
|
||
nbConsts + 4,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
],
|
||
update: [],
|
||
icus: null
|
||
});
|
||
});
|
||
|
||
it('for simple bindings', () => {
|
||
const MSG_DIV = `Hello <20>0<EFBFBD>!`;
|
||
const nbConsts = 2;
|
||
const index = 1;
|
||
const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index);
|
||
|
||
expect((opCodes as any).update.debug.operations).toEqual([
|
||
{__raw_opCode: 8, checkBit: 1, type: 'Text', nodeIndex: 2, text: 'Hello <20>0<EFBFBD>!'}
|
||
]);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 1,
|
||
create:
|
||
['', nbConsts, 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 <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
|
||
const nbConsts = 2;
|
||
const index = 1;
|
||
const opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 1,
|
||
create:
|
||
['', nbConsts, 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:
|
||
// <div>
|
||
// {{value}} is rendered as:
|
||
// <span *ngIf>
|
||
// before <b *ngIf>middle</b> after
|
||
// </span>
|
||
// !
|
||
// </div>
|
||
const MSG_DIV =
|
||
`<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;
|
||
const firstTextNode = 3;
|
||
let opCodes = getOpCodes(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 2,
|
||
create: [
|
||
'',
|
||
nbConsts,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
2 << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'!',
|
||
nbConsts + 1,
|
||
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 ****/
|
||
// <20>#1:1<>before<72>*2:2<>middle<6C>/*2:2<>after<65>/#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,
|
||
create: [
|
||
spanElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'before',
|
||
nbConsts,
|
||
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
bElementSubTemplate << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
spanElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'after',
|
||
nbConsts + 1,
|
||
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,
|
||
create: [
|
||
bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
index << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'middle',
|
||
nbConsts,
|
||
bElement << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
bElement << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd,
|
||
],
|
||
update: [],
|
||
icus: null
|
||
});
|
||
});
|
||
|
||
it('for ICU expressions', () => {
|
||
const MSG_DIV = `{<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(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index);
|
||
const tIcuIndex = 0;
|
||
const icuCommentNodeIndex = index + 1;
|
||
const firstTextNodeIndex = index + 2;
|
||
const bElementNodeIndex = index + 3;
|
||
const iElementNodeIndex = index + 3;
|
||
const spanElementNodeIndex = index + 3;
|
||
const innerTextNode = index + 4;
|
||
const lastTextNode = index + 5;
|
||
|
||
const debugOps = (opCodes as any).update.debug.operations;
|
||
expect(debugOps[0].__raw_opCode).toBe(6);
|
||
expect(debugOps[0].checkBit).toBe(1);
|
||
expect(debugOps[0].type).toBe('IcuSwitch');
|
||
expect(debugOps[0].nodeIndex).toBe(1);
|
||
expect(debugOps[0].tIcuIndex).toBe(0);
|
||
expect(debugOps[0].mainBinding).toBe('<27>0<EFBFBD>');
|
||
|
||
expect(debugOps[1].__raw_opCode).toBe(7);
|
||
expect(debugOps[1].checkBit).toBe(3);
|
||
expect(debugOps[1].type).toBe('IcuUpdate');
|
||
expect(debugOps[1].nodeIndex).toBe(1);
|
||
expect(debugOps[1].tIcuIndex).toBe(0);
|
||
|
||
const icuDebugOps = (opCodes as any).icus[0].create[0].debug.operations;
|
||
let op: any;
|
||
let i = 0;
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe('no ');
|
||
expect(op.type).toBe('Create Text Node');
|
||
expect(op.nodeIndex).toBe(2);
|
||
expect(op.text).toBe('no ');
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe(131073);
|
||
expect(op.type).toBe('AppendChild');
|
||
expect(op.nodeIndex).toBe(1);
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toEqual({marker: 'element'});
|
||
expect(op.type).toBe('ELEMENT_MARKER');
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe('b');
|
||
expect(op.type).toBe('Create Text Node');
|
||
expect(op.nodeIndex).toBe(3);
|
||
expect(op.text).toBe('b');
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe(131073);
|
||
expect(op.type).toBe('AppendChild');
|
||
expect(op.nodeIndex).toBe(1);
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe(28);
|
||
expect(op.type).toBe('Attr');
|
||
expect(op.nodeIndex).toBe(3);
|
||
expect(op.attrName).toBe('title');
|
||
expect(op.attrValue).toBe('none');
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe('emails');
|
||
expect(op.type).toBe('Create Text Node');
|
||
expect(op.nodeIndex).toBe(4);
|
||
expect(op.text).toBe('emails');
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe(393217);
|
||
expect(op.type).toBe('AppendChild');
|
||
expect(op.nodeIndex).toBe(3);
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe('!');
|
||
expect(op.type).toBe('Create Text Node');
|
||
expect(op.nodeIndex).toBe(5);
|
||
expect(op.text).toBe('!');
|
||
|
||
op = icuDebugOps[i++];
|
||
expect(op.__raw_opCode).toBe(131073);
|
||
expect(op.type).toBe('AppendChild');
|
||
expect(op.nodeIndex).toBe(1);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 5,
|
||
create: [
|
||
COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex,
|
||
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],
|
||
childIcus: [[], [], []],
|
||
cases: ['0', '1', 'other'],
|
||
create: [
|
||
[
|
||
'no ',
|
||
firstTextNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
ELEMENT_MARKER,
|
||
'b',
|
||
bElementNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr,
|
||
'title',
|
||
'none',
|
||
'emails',
|
||
innerTextNode,
|
||
bElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'!',
|
||
lastTextNode,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
],
|
||
[
|
||
'one ', firstTextNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
ELEMENT_MARKER, 'i', iElementNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'email', innerTextNode,
|
||
iElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||
],
|
||
[
|
||
'', firstTextNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
ELEMENT_MARKER, 'span', spanElementNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'emails', innerTextNode,
|
||
spanElementNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||
]
|
||
],
|
||
remove: [
|
||
[
|
||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
bElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
lastTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
],
|
||
[
|
||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
innerTextNode << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
iElementNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
],
|
||
[
|
||
firstTextNodeIndex << 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
|
||
firstTextNodeIndex << 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 = `{<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(() => { ɵɵi18nStart(index, MSG_DIV); }, null, nbConsts, index);
|
||
const icuCommentNodeIndex = index + 1;
|
||
const firstTextNodeIndex = index + 2;
|
||
const nestedIcuCommentNodeIndex = index + 3;
|
||
const lastTextNodeIndex = index + 4;
|
||
const nestedTextNodeIndex = index + 5;
|
||
const tIcuIndex = 1;
|
||
const nestedTIcuIndex = 0;
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 6,
|
||
create: [
|
||
COMMENT_MARKER, 'ICU 1', icuCommentNodeIndex,
|
||
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],
|
||
childIcus: [[], [], []],
|
||
cases: ['cat', 'dog', 'other'],
|
||
create: [
|
||
[
|
||
'cats', nestedTextNodeIndex, nestedIcuCommentNodeIndex
|
||
<< I18nMutateOpCode.SHIFT_PARENT |
|
||
I18nMutateOpCode.AppendChild
|
||
],
|
||
[
|
||
'dogs', nestedTextNodeIndex, nestedIcuCommentNodeIndex
|
||
<< I18nMutateOpCode.SHIFT_PARENT |
|
||
I18nMutateOpCode.AppendChild
|
||
],
|
||
[
|
||
'animals', nestedTextNodeIndex, nestedIcuCommentNodeIndex
|
||
<< I18nMutateOpCode.SHIFT_PARENT |
|
||
I18nMutateOpCode.AppendChild
|
||
]
|
||
],
|
||
remove: [
|
||
[nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
|
||
[nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
|
||
[nestedTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove]
|
||
],
|
||
update: [[], [], []]
|
||
},
|
||
{
|
||
type: 1,
|
||
vars: [1, 4],
|
||
childIcus: [[], [0]],
|
||
cases: ['0', 'other'],
|
||
create: [
|
||
[
|
||
'zero', firstTextNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||
],
|
||
[
|
||
'', firstTextNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
COMMENT_MARKER, 'nested ICU 0', nestedIcuCommentNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild,
|
||
'!', lastTextNodeIndex,
|
||
icuCommentNodeIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild
|
||
]
|
||
],
|
||
remove: [
|
||
[firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove],
|
||
[
|
||
firstTextNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove,
|
||
lastTextNodeIndex << 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
|
||
firstTextNodeIndex << 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(`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 LView) as any as Element).getAttribute('title'))
|
||
.toEqual(MSG_title);
|
||
});
|
||
|
||
it('for simple bindings', () => {
|
||
const MSG_title = `Hello <20>0<EFBFBD>!`;
|
||
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 <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
|
||
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 <20>0<EFBFBD>!`;
|
||
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('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', () => {
|
||
const arr = ['<27>*1:1<><31>#2:1<>', '<27>#4:2<>', '<27>6:4<>', '<27>/#2:1<><31>/*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: <20>I18N_EXP_ICU<43>, My ICU #2: <20>I18N_EXP_ICU<43>', {ICU: ['ICU_VALUE_1']}]
|
||
];
|
||
cases.forEach(([input, replacements, output]) => {
|
||
expect(() => ɵɵi18nPostprocess(input as string, replacements as any)).toThrowError();
|
||
});
|
||
});
|
||
});
|
||
});
|