There were two issues with multiple ICU expressions in the same i18n block: - the regexp that was used to parse the text wasn't able to handle multiple ICU expressions, I've replaced it with parsing the text and searching for brackets (which is what we ended up doing in the end anyway) - we allocate node indexes for nodes generated by the ICU expressions which increases the expando value, but we would create the nodes for those cases during the update phase. In the mean time we would create some nodes during the creation phase (comment nodes for ICU expressions, text nodes, ...) with an auto increment index. This means that any node created after an ICU expression would get the following index value, but the ICU case nodes expected to use the same index as well... There was a mismatch between the auto generated index, and the expected index which was causing problems when we needed to select those nodes for updates later on. To fix it, I've added the expected node index to the list of mutate codes that we generate, and we do not use an auto increment value anymore. FW-905 #resolve PR Close #28083
1879 lines
64 KiB
TypeScript
1879 lines
64 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 {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, nextContext, bind, elementProperty, projectionDef, projection, elementContainerStart, elementContainerEnd} from '../../src/render3/instructions';
|
||
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 {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 <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);
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 1,
|
||
expandoStartIndex: nbConsts,
|
||
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,
|
||
expandoStartIndex: nbConsts,
|
||
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).toEqual({
|
||
vars: 1,
|
||
expandoStartIndex: nbConsts,
|
||
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,
|
||
expandoStartIndex: nbConsts,
|
||
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,
|
||
expandoStartIndex: nbConsts,
|
||
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,
|
||
expandoStartIndex: nbConsts,
|
||
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,
|
||
expandoStartIndex: nbConsts,
|
||
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;
|
||
|
||
expect(opCodes).toEqual({
|
||
vars: 5,
|
||
expandoStartIndex: nbConsts,
|
||
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],
|
||
expandoStartIndex: icuCommentNodeIndex + 1,
|
||
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,
|
||
expandoStartIndex: nbConsts,
|
||
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],
|
||
expandoStartIndex: lastTextNodeIndex + 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],
|
||
expandoStartIndex: icuCommentNodeIndex + 1,
|
||
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(`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(`<div>${MSG_DIV}</div>`);
|
||
});
|
||
|
||
it('for bindings', () => {
|
||
const MSG_DIV = `Hello <20>0<EFBFBD>!`;
|
||
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('<div></div>');
|
||
|
||
// 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 <20>#3<>world<6C>/#3<> and <20>#2<>universe<73>/#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('<div>Hello <span>world</span> and <div>universe</div>!</div>');
|
||
});
|
||
|
||
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('<div>Hello</div> 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 <20>#3<>world<6C>/#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('<div>Hello <span>world</span></div><div>!</div>');
|
||
});
|
||
|
||
it('for sub-templates', () => {
|
||
// Template: `<div>Content: <div>before<span>middle</span>after</div>!</div>`;
|
||
const MSG_DIV =
|
||
`Content: <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<>!`;
|
||
|
||
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, 'span', ['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, 'div', ['ngIf', '']);
|
||
i18nEnd();
|
||
elementEnd();
|
||
}
|
||
if (rf & RenderFlags.Update) {
|
||
elementProperty(2, 'ngIf', true);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const fixture = new ComponentFixture(MyApp);
|
||
expect(fixture.html)
|
||
.toEqual('<div>Content: <div>before<span>middle</span>after</div>!</div>');
|
||
});
|
||
|
||
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 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('<div><!--ICU 2--></div>');
|
||
});
|
||
|
||
it('for multiple 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>}
|
||
} - {<7B>0<EFBFBD>, select,
|
||
other {(<28>0<EFBFBD>)}
|
||
}`;
|
||
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('<div><!--ICU 2--> - <!--ICU 8--></div>');
|
||
});
|
||
|
||
it('for multiple ICU expressions inside html', () => {
|
||
const MSG_DIV = `<EFBFBD>#2<>{<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>}
|
||
}<7D>/#2<><32>#3<>{<7B>0<EFBFBD>, select,
|
||
other {(<28>0<EFBFBD>)}
|
||
}<7D>/#3<>`;
|
||
const fixture = prepareFixture(() => {
|
||
elementStart(0, 'div');
|
||
i18nStart(1, MSG_DIV);
|
||
element(2, 'span');
|
||
element(3, 'span');
|
||
i18nEnd();
|
||
elementEnd();
|
||
}, null, 4);
|
||
|
||
// Template should be empty because there is no update template function
|
||
expect(fixture.html).toEqual('<div><span><!--ICU 4--></span><span><!--ICU 9--></span></div>');
|
||
});
|
||
|
||
it('for ICU expressions inside templates', () => {
|
||
const MSG_DIV = `<EFBFBD>*2:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||
=0 {no <b title="none">emails</b>!}
|
||
=1 {one <i>email</i>}
|
||
other {<7B>0:1<> <span title="<22>1:1<>">emails</span>}
|
||
}<7D>/#1:1<><31>/*2:1<>`;
|
||
|
||
function subTemplate_1(rf: RenderFlags, ctx: any) {
|
||
if (rf & RenderFlags.Create) {
|
||
i18nStart(0, MSG_DIV, 1);
|
||
element(1, 'span');
|
||
i18nEnd();
|
||
}
|
||
if (rf & RenderFlags.Update) {
|
||
const ctx = nextContext();
|
||
i18nExp(bind(ctx.value0));
|
||
i18nExp(bind(ctx.value1));
|
||
i18nApply(0);
|
||
}
|
||
}
|
||
|
||
class MyApp {
|
||
value0 = 0;
|
||
value1 = 'emails label';
|
||
|
||
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, 2, 2, 'span', [3, 'ngIf']);
|
||
i18nEnd();
|
||
elementEnd();
|
||
}
|
||
if (rf & RenderFlags.Update) {
|
||
elementProperty(2, 'ngIf', true);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const fixture = new ComponentFixture(MyApp);
|
||
expect(fixture.html)
|
||
.toEqual('<div><span>no <b title="none">emails</b>!<!--ICU 4--></span></div>');
|
||
|
||
// Update the value
|
||
fixture.component.value0 = 3;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>3 <span title="emails label">emails</span><!--ICU 4--></span></div>');
|
||
});
|
||
|
||
it('for ICU expressions inside <ng-container>', () => {
|
||
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 fixture = prepareFixture(
|
||
() => {
|
||
elementStart(0, 'div');
|
||
{
|
||
elementContainerStart(1);
|
||
{ i18n(2, MSG_DIV); }
|
||
elementContainerEnd();
|
||
}
|
||
elementEnd();
|
||
},
|
||
() => {
|
||
i18nExp(bind(0));
|
||
i18nExp(bind('more than one'));
|
||
i18nApply(2);
|
||
},
|
||
3, 2);
|
||
|
||
expect(fixture.html).toEqual('<div>no <b title="none">emails</b>!<!--ICU 5--></div>');
|
||
});
|
||
|
||
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 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('<div><!--ICU 2--></div>');
|
||
});
|
||
});
|
||
|
||
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(`i18nExp & i18nApply`, () => {
|
||
it('for text bindings', () => {
|
||
const MSG_DIV = `Hello <20>0<EFBFBD>!`;
|
||
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('<div>Hello world!</div>');
|
||
});
|
||
|
||
it('for attribute bindings', () => {
|
||
const MSG_title = `Hello <20>0<EFBFBD>!`;
|
||
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('<div title="Hello world!"></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div title="Hello world!"></div>');
|
||
|
||
ctx.value = 'universe';
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div title="Hello universe!"></div>');
|
||
});
|
||
|
||
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('<div title="Hello world!"></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div title="Hello world!"></div>');
|
||
});
|
||
|
||
it('for multiple attribute bindings', () => {
|
||
const MSG_title = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
|
||
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('<div title="Hello world and universe, again world!"></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div title="Hello world and universe, again world!"></div>');
|
||
|
||
ctx.value0 = 'earth';
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div title="Hello earth and universe, again earth!"></div>');
|
||
|
||
ctx.value0 = 'earthlings';
|
||
ctx.value1 = 'martians';
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual('<div title="Hello earthlings and martians, again earthlings!"></div>');
|
||
});
|
||
|
||
it('for bindings of multiple attributes', () => {
|
||
const MSG_title = `Hello <20>0<EFBFBD>!`;
|
||
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('<div aria-label="Hello world!" title="Hello world!"></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div aria-label="Hello world!" title="Hello world!"></div>');
|
||
|
||
ctx.value = 'universe';
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual('<div aria-label="Hello universe!" title="Hello universe!"></div>');
|
||
});
|
||
|
||
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 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('<div>no <b title="none">emails</b>!<!--ICU 4--></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>no <b title="none">emails</b>!<!--ICU 4--></div>');
|
||
|
||
ctx.value0 = 1;
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>one <i>email</i><!--ICU 4--></div>');
|
||
|
||
ctx.value0 = 10;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual('<div>10 <span title="emails label">emails</span><!--ICU 4--></div>');
|
||
|
||
ctx.value1 = '10 emails';
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual('<div>10 <span title="10 emails">emails</span><!--ICU 4--></div>');
|
||
|
||
ctx.value0 = 0;
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>no <b title="none">emails</b>!<!--ICU 4--></div>');
|
||
});
|
||
|
||
it('for multiple 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>}
|
||
} - {<7B>0<EFBFBD>, select,
|
||
other {(<28>0<EFBFBD>)}
|
||
}`;
|
||
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('<div>no <b title="none">emails</b>!<!--ICU 4--> - (0)<!--ICU 10--></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual('<div>no <b title="none">emails</b>!<!--ICU 4--> - (0)<!--ICU 10--></div>');
|
||
|
||
ctx.value0 = 1;
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>one <i>email</i><!--ICU 4--> - (1)<!--ICU 10--></div>');
|
||
|
||
ctx.value0 = 10;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div>10 <span title="emails label">emails</span><!--ICU 4--> - (10)<!--ICU 10--></div>');
|
||
|
||
ctx.value1 = '10 emails';
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div>10 <span title="10 emails">emails</span><!--ICU 4--> - (10)<!--ICU 10--></div>');
|
||
|
||
ctx.value0 = 0;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual('<div>no <b title="none">emails</b>!<!--ICU 4--> - (0)<!--ICU 10--></div>');
|
||
});
|
||
|
||
it('for multiple ICU expressions', () => {
|
||
const MSG_DIV = `<EFBFBD>#2<>{<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>}
|
||
}<7D>/#2<><32>#3<>{<7B>0<EFBFBD>, select,
|
||
other {(<28>0<EFBFBD>)}
|
||
}<7D>/#3<>`;
|
||
const ctx = {value0: 0, value1: 'emails label'};
|
||
|
||
const fixture = prepareFixture(
|
||
() => {
|
||
elementStart(0, 'div');
|
||
i18nStart(1, MSG_DIV);
|
||
element(2, 'span');
|
||
element(3, 'span');
|
||
i18nEnd();
|
||
elementEnd();
|
||
},
|
||
() => {
|
||
i18nExp(bind(ctx.value0));
|
||
i18nExp(bind(ctx.value1));
|
||
i18nApply(1);
|
||
},
|
||
4, 2);
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>no <b title="none">emails</b>!<!--ICU 6--></span><span>(0)<!--ICU 11--></span></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>no <b title="none">emails</b>!<!--ICU 6--></span><span>(0)<!--ICU 11--></span></div>');
|
||
|
||
ctx.value0 = 1;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>one <i>email</i><!--ICU 6--></span><span>(1)<!--ICU 11--></span></div>');
|
||
|
||
ctx.value0 = 10;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>10 <span title="emails label">emails</span><!--ICU 6--></span><span>(10)<!--ICU 11--></span></div>');
|
||
|
||
ctx.value1 = '10 emails';
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>10 <span title="10 emails">emails</span><!--ICU 6--></span><span>(10)<!--ICU 11--></span></div>');
|
||
|
||
ctx.value0 = 0;
|
||
fixture.update();
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><span>no <b title="none">emails</b>!<!--ICU 6--></span><span>(0)<!--ICU 11--></span></div>');
|
||
});
|
||
|
||
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 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('<div>zero<!--ICU 4--></div>');
|
||
|
||
// Change detection cycle, no model changes
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>zero<!--ICU 4--></div>');
|
||
|
||
ctx.value0 = 10;
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>10 cats<!--nested ICU 0-->!<!--ICU 4--></div>');
|
||
|
||
ctx.value1 = 'squirrel';
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>10 animals<!--nested ICU 0-->!<!--ICU 4--></div>');
|
||
|
||
ctx.value0 = 0;
|
||
fixture.update();
|
||
expect(fixture.html).toEqual('<div>zero<!--ICU 4--></div>');
|
||
});
|
||
});
|
||
|
||
describe('integration', () => {
|
||
it('should support multiple i18n blocks', () => {
|
||
// Translated template:
|
||
// <div>
|
||
// <a i18n>
|
||
// trad {{exp1}}
|
||
// </a>
|
||
// hello
|
||
// <b i18n i18n-title title="start {{exp2}} middle {{exp1}} end">
|
||
// <e></e>
|
||
// <c>trad</c>
|
||
// </b>
|
||
// </div>
|
||
|
||
const MSG_DIV_1 = `trad <20>0<EFBFBD>`;
|
||
const MSG_DIV_2_ATTR = ['title', `start <20>1<EFBFBD> middle <20>0<EFBFBD> end`];
|
||
const MSG_DIV_2 = `<EFBFBD>#9<><39>/#9<><39>#7<>trad<61>/#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(
|
||
`<div><a>trad 1</a>hello<b title="start 2 middle 1 end"><e></e><c>trad</c></b></div>`);
|
||
});
|
||
|
||
it('should support attribute translations on removed elements', () => {
|
||
// Translated template:
|
||
// <div i18n i18n-title title="start {{exp2}} middle {{exp1}} end">
|
||
// trad {{exp1}}
|
||
// </div>
|
||
|
||
const MSG_DIV_1 = `trad <20>0<EFBFBD>`;
|
||
const MSG_DIV_1_ATTR_1 = ['title', `start <20>1<EFBFBD> middle <20>0<EFBFBD> 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(`<div title="start 2 middle 1 end">trad 1</div>`);
|
||
});
|
||
|
||
describe('projection', () => {
|
||
it('should project the translations', () => {
|
||
@Component({selector: 'child', template: '<p><ng-content></ng-content></p>'})
|
||
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 = `<EFBFBD>#2<>Je suis projeté depuis <20>#3<><33>0<EFBFBD><30>/#3<><33>/#2<>`;
|
||
const MSG_ATTR_1 = ['title', `Enfant de <20>0<EFBFBD>`];
|
||
|
||
@Component({
|
||
selector: 'parent',
|
||
template: `
|
||
<div i18n>
|
||
<child>
|
||
I am projected from
|
||
<b i18n-title title="Child of {{name}}">{{name}}
|
||
<remove-me-1></remove-me-1>
|
||
</b>
|
||
<remove-me-2></remove-me-2>
|
||
</child>
|
||
<remove-me-3></remove-me-3>
|
||
</div>`
|
||
// Translated to:
|
||
// <div i18n>
|
||
// <child>
|
||
// <p>
|
||
// Je suis projeté depuis <b i18n-title title="Enfant de {{name}}">{{name}}</b>
|
||
// </p>
|
||
// </child>
|
||
// </div>
|
||
})
|
||
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(
|
||
'<div><child><p>Je suis projeté depuis <b title="Enfant de Parent">Parent</b></p></child></div>');
|
||
// <div><child><p><b title="Enfant de Parent">Parent</b></p></child></div>
|
||
// <div><child><p>Je suis projeté depuis <b title="Enfant de
|
||
// Parent">Parent</b></p></child></div>
|
||
});
|
||
|
||
it('should project a translated i18n block', () => {
|
||
@Component({selector: 'child', template: '<p><ng-content></ng-content></p>'})
|
||
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 <20>0<EFBFBD>`;
|
||
const MSG_ATTR_1 = ['title', `Enfant de <20>0<EFBFBD>`];
|
||
|
||
@Component({
|
||
selector: 'parent',
|
||
template: `
|
||
<div>
|
||
<child>
|
||
<any></any>
|
||
<b i18n i18n-title title="Child of {{name}}">I am projected from {{name}}</b>
|
||
<any></any>
|
||
</child>
|
||
</div>`
|
||
// Translated to:
|
||
// <div>
|
||
// <child>
|
||
// <any></any>
|
||
// <b i18n i18n-title title="Enfant de {{name}}">Je suis projeté depuis {{name}}</b>
|
||
// <any></any>
|
||
// </child>
|
||
// </div>
|
||
})
|
||
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(
|
||
'<div><child><p><any></any><b title="Enfant de Parent">Je suis projeté depuis Parent</b><any></any></p></child></div>');
|
||
|
||
// it should be able to render a new component with the same template code
|
||
const fixture2 = new ComponentFixture(Parent);
|
||
expect(fixture2.html).toEqual(fixture.html);
|
||
|
||
// Updating the fixture should work
|
||
fixture2.component.name = 'Parent 2';
|
||
fixture.update();
|
||
fixture2.update();
|
||
expect(fixture2.html)
|
||
.toEqual(
|
||
'<div><child><p><any></any><b title="Enfant de Parent 2">Je suis projeté depuis Parent 2</b><any></any></p></child></div>');
|
||
|
||
// The first fixture should not have changed
|
||
expect(fixture.html)
|
||
.toEqual(
|
||
'<div><child><p><any></any><b title="Enfant de Parent">Je suis projeté depuis Parent</b><any></any></p></child></div>');
|
||
});
|
||
|
||
it('should re-project translations when multiple projections', () => {
|
||
@Component({selector: 'grand-child', template: '<div><ng-content></ng-content></div>'})
|
||
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: '<grand-child><ng-content></ng-content></grand-child>'})
|
||
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 = `<EFBFBD>#2<>Bonjour<75>/#2<> Monde!`;
|
||
|
||
@Component({
|
||
selector: 'parent',
|
||
template: `<child i18n><b>Hello</b> World!</child>`
|
||
// Translated to:
|
||
// <child i18n><grand-child><div><b>Bonjour</b> Monde!</div></grand-child></child>
|
||
})
|
||
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('<child><grand-child><div><b>Bonjour</b> Monde!</div></grand-child></child>');
|
||
// <child><grand-child><div><b>Bonjour</b></div></grand-child></child>
|
||
// <child><grand-child><div><b>Bonjour</b> Monde!</div></grand-child></child>
|
||
});
|
||
|
||
xit('should re-project translations when removed placeholders', () => {
|
||
@Component({selector: 'grand-child', template: '<div><ng-content></ng-content></div>'})
|
||
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: '<grand-child><ng-content></ng-content></grand-child>'})
|
||
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: `<child i18n><b>Hello</b> World!</child>`
|
||
// Translated to:
|
||
// <child i18n><grand-child><div>Bonjour Monde!</div></grand-child></child>
|
||
})
|
||
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('<child><grand-child><div>Bonjour Monde!</div></grand-child></child>');
|
||
});
|
||
|
||
it('should project translations with selectors', () => {
|
||
@Component({
|
||
selector: 'child',
|
||
template: `
|
||
<ng-content select="span"></ng-content>
|
||
`
|
||
})
|
||
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 = `<EFBFBD>#2<>Contenu<6E>/#2<>`;
|
||
|
||
@Component({
|
||
selector: 'parent',
|
||
template: `
|
||
<child i18n>
|
||
<span title="keepMe"></span>
|
||
<span title="deleteMe"></span>
|
||
</child>
|
||
`
|
||
// Translated to:
|
||
// <child i18n><span title="keepMe">Contenu</span></child>
|
||
})
|
||
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('<child><span title="keepMe">Contenu</span></child>');
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('i18nPostprocess', () => {
|
||
it('should handle valid cases', () => {
|
||
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 = [
|
||
// 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 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();
|
||
});
|
||
});
|
||
});
|
||
|
||
});
|