From e22a302cad20091554e332740027b2269e13e94c Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Tue, 13 Nov 2018 09:36:30 +0100 Subject: [PATCH] feat(ivy): support for i18n & ICU expressions (#27101) PR Close #27101 --- .../compliance/r3_view_compiler_i18n_spec.ts | 28 +- .../compiler/src/render3/r3_identifiers.ts | 2 +- .../compiler/src/render3/view/template.ts | 2 +- .../core/src/core_render3_private_export.ts | 16 +- packages/core/src/render3/STATUS.md | 12 +- packages/core/src/render3/VIEW_DATA.md | 12 +- packages/core/src/render3/i18n.md | 85 +- packages/core/src/render3/i18n.ts | 2074 +++++++---- packages/core/src/render3/index.ts | 16 +- packages/core/src/render3/instructions.ts | 26 +- packages/core/src/render3/interfaces/i18n.ts | 40 +- packages/core/src/render3/interfaces/node.ts | 19 +- .../src/render3/interfaces/sanitization.ts | 12 + packages/core/src/render3/interfaces/view.ts | 6 +- packages/core/src/render3/jit/environment.ts | 2 +- .../core/src/render3/node_manipulation.ts | 39 +- packages/core/src/render3/state.ts | 4 + packages/core/src/render3/util.ts | 14 +- .../core/src/sanitization/html_sanitizer.ts | 10 +- .../bundle.golden_symbols.json | 3 + .../hello_world/bundle.golden_symbols.json | 3 + .../hello_world_r2/bundle.golden_symbols.json | 5 +- .../bundling/todo/bundle.golden_symbols.json | 3 + .../todo_r2/bundle.golden_symbols.json | 7 +- packages/core/test/render3/i18n_spec.ts | 3077 ++++++++--------- 25 files changed, 2980 insertions(+), 2537 deletions(-) create mode 100644 packages/core/src/render3/interfaces/sanitization.ts diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 4b748509e5..e15a714944 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -83,23 +83,23 @@ describe('i18n support in the view compiler', () => { $r3$.ɵi18nEnd(); $r3$.ɵelementEnd(); $r3$.ɵelementStart(2, "div"); - $r3$.ɵi18nAttribute(3, $_c2$); + $r3$.ɵi18nAttributes(3, $_c2$); $r3$.ɵtext(4, "Content B"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(5, "div"); - $r3$.ɵi18nAttribute(6, $_c4$); + $r3$.ɵi18nAttributes(6, $_c4$); $r3$.ɵtext(7, "Content C"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(8, "div"); - $r3$.ɵi18nAttribute(9, $_c6$); + $r3$.ɵi18nAttributes(9, $_c6$); $r3$.ɵtext(10, "Content D"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(11, "div"); - $r3$.ɵi18nAttribute(12, $_c8$); + $r3$.ɵi18nAttributes(12, $_c8$); $r3$.ɵtext(13, "Content E"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(14, "div"); - $r3$.ɵi18nAttribute(15, $_c10$); + $r3$.ɵi18nAttributes(15, $_c10$); $r3$.ɵtext(16, "Content F"); $r3$.ɵelementEnd(); } @@ -142,7 +142,7 @@ describe('i18n support in the view compiler', () => { template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵelementStart(0, "div", $_c0$); - $r3$.ɵi18nAttribute(1, $_c2$); + $r3$.ɵi18nAttributes(1, $_c2$); $r3$.ɵelementEnd(); } } @@ -207,10 +207,10 @@ describe('i18n support in the view compiler', () => { if (rf & 1) { $r3$.ɵelementStart(0, "div", $_c0$); $r3$.ɵpipe(1, "uppercase"); - $r3$.ɵi18nAttribute(2, $_c4$); + $r3$.ɵi18nAttributes(2, $_c4$); $r3$.ɵelementEnd(); $r3$.ɵelementStart(3, "div", $_c5$); - $r3$.ɵi18nAttribute(4, $_c8$); + $r3$.ɵi18nAttributes(4, $_c8$); $r3$.ɵelementEnd(); } if (rf & 2) { @@ -265,7 +265,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵelementStart(0, "div"); $r3$.ɵelementStart(1, "div"); $r3$.ɵpipe(2, "uppercase"); - $r3$.ɵi18nAttribute(3, $_c2$); + $r3$.ɵi18nAttributes(3, $_c2$); $r3$.ɵelementEnd(); $r3$.ɵelementEnd(); } @@ -345,10 +345,10 @@ describe('i18n support in the view compiler', () => { if (rf & 1) { $r3$.ɵelementStart(0, "div", $_c0$); $r3$.ɵpipe(1, "uppercase"); - $r3$.ɵi18nAttribute(2, $_c4$); + $r3$.ɵi18nAttributes(2, $_c4$); $r3$.ɵelementEnd(); $r3$.ɵelementStart(3, "div", $_c5$); - $r3$.ɵi18nAttribute(4, $_c8$); + $r3$.ɵi18nAttributes(4, $_c8$); $r3$.ɵelementEnd(); } if (rf & 2) { @@ -403,7 +403,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵelementStart(0, "div"); $r3$.ɵelementStart(1, "div"); $r3$.ɵpipe(2, "uppercase"); - $r3$.ɵi18nAttribute(3, $_c2$); + $r3$.ɵi18nAttributes(3, $_c2$); $r3$.ɵelementEnd(); $r3$.ɵelementEnd(); } @@ -693,7 +693,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵelementStart(0, "div"); $r3$.ɵi18nStart(1, $MSG_APP_SPEC_TS_0$); $r3$.ɵelementStart(2, "span"); - $r3$.ɵi18nAttribute(3, $_c2$); + $r3$.ɵi18nAttributes(3, $_c2$); $r3$.ɵelementEnd(); $r3$.ɵi18nEnd(); $r3$.ɵelementEnd(); @@ -701,7 +701,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵi18nStart(5, $MSG_APP_SPEC_TS_3$); $r3$.ɵpipe(6, "uppercase"); $r3$.ɵelementStart(7, "span"); - $r3$.ɵi18nAttribute(8, $_c5$); + $r3$.ɵi18nAttributes(8, $_c5$); $r3$.ɵelementEnd(); $r3$.ɵi18nEnd(); $r3$.ɵelementEnd(); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 4337f4fefb..ab4bfe494e 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -95,7 +95,7 @@ export class Identifiers { static pipeBind4: o.ExternalReference = {name: 'ɵpipeBind4', moduleName: CORE}; static pipeBindV: o.ExternalReference = {name: 'ɵpipeBindV', moduleName: CORE}; - static i18nAttribute: o.ExternalReference = {name: 'ɵi18nAttribute', moduleName: CORE}; + static i18nAttributes: o.ExternalReference = {name: 'ɵi18nAttributes', moduleName: CORE}; static i18nExp: o.ExternalReference = {name: 'ɵi18nExp', moduleName: CORE}; static i18nStart: o.ExternalReference = {name: 'ɵi18nStart', moduleName: CORE}; static i18nEnd: o.ExternalReference = {name: 'ɵi18nEnd', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 005839beac..1fd2a8584d 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -490,7 +490,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (i18nAttrArgs.length) { const index: o.Expression = o.literal(this.allocateDataSlot()); const args = this.constantPool.getConstLiteral(o.literalArr(i18nAttrArgs), true); - this.creationInstruction(element.sourceSpan, R3.i18nAttribute, [index, args]); + this.creationInstruction(element.sourceSpan, R3.i18nAttributes, [index, args]); if (hasBindings) { this.updateInstruction(element.sourceSpan, R3.i18nApply, [index]); } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index cf66b58e12..7c0952b012 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -108,24 +108,12 @@ export { PipeDef as ɵPipeDef, PipeDefWithMeta as ɵPipeDefWithMeta, whenRendered as ɵwhenRendered, - i18nAttribute as ɵi18nAttribute, + i18nAttributes as ɵi18nAttributes, i18nExp as ɵi18nExp, i18nStart as ɵi18nStart, i18nEnd as ɵi18nEnd, i18nApply as ɵi18nApply, - i18nExpMapping as ɵi18nExpMapping, - i18nInterpolation1 as ɵi18nInterpolation1, - i18nInterpolation2 as ɵi18nInterpolation2, - i18nInterpolation3 as ɵi18nInterpolation3, - i18nInterpolation4 as ɵi18nInterpolation4, - i18nInterpolation5 as ɵi18nInterpolation5, - i18nInterpolation6 as ɵi18nInterpolation6, - i18nInterpolation7 as ɵi18nInterpolation7, - i18nInterpolation8 as ɵi18nInterpolation8, - i18nInterpolationV as ɵi18nInterpolationV, - i18nMapping as ɵi18nMapping, - I18nInstruction as ɵI18nInstruction, - I18nExpInstruction as ɵI18nExpInstruction, + i18nIcuReplaceVars as ɵi18nIcuReplaceVars, WRAP_RENDERER_FACTORY2 as ɵWRAP_RENDERER_FACTORY2, setClassMetadata as ɵsetClassMetadata, } from './render3/index'; diff --git a/packages/core/src/render3/STATUS.md b/packages/core/src/render3/STATUS.md index d4647bb337..c44c7e572b 100644 --- a/packages/core/src/render3/STATUS.md +++ b/packages/core/src/render3/STATUS.md @@ -205,12 +205,12 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S ### I18N | Feature | Runtime | Spec | Compiler | | ----------------------------------- | ------- | -------- | -------- | -| i18nStart | ❌ | ✅ | ✅ | -| i18nEnd | ❌ | ✅ | ✅ | -| i18nAttributes | ❌ | ✅ | ✅ | -| i18nExp | ❌ | ✅ | ✅ | -| i18nApply | ❌ | ✅ | ✅ | -| ICU expressions | ❌ | ✅ | ❌ | +| i18nStart | ✅ | ✅ | ✅ | +| i18nEnd | ✅ | ✅ | ✅ | +| i18nAttributes | ✅ | ✅ | ✅ | +| i18nExp | ✅ | ✅ | ✅ | +| i18nApply | ✅ | ✅ | ✅ | +| ICU expressions | ✅ | ✅ | ❌ | | closure support for g3 | ✅ | ✅ | ❌ | | runtime service for external world | ❌ | ❌ | ❌ | | migration tool | ❌ | ❌ | ❌ | diff --git a/packages/core/src/render3/VIEW_DATA.md b/packages/core/src/render3/VIEW_DATA.md index 20928eca12..a6e8f938ea 100644 --- a/packages/core/src/render3/VIEW_DATA.md +++ b/packages/core/src/render3/VIEW_DATA.md @@ -11,12 +11,12 @@ For example index `123` may point to a component instance in the `LViewData` but The layout is as such: -| Section | `LViewData` | `TView.data` -| ---------- | --------------------------------------------- | -------------------------------------------------- -| `HEADER` | contextual data | mostly `null` -| `CONSTS` | DOM, pipe, and local ref instances | -| `VARS` | binding values | property names -| `EXPANDO` | host bindings; directive instances; providers | host prop names; directive tokens; provider tokens +| Section | `LViewData` | `TView.data` +| ---------- | ------------------------------------------------------------ | -------------------------------------------------- +| `HEADER` | contextual data | mostly `null` +| `CONSTS` | DOM, pipe, and local ref instances | +| `VARS` | binding values | property names +| `EXPANDO` | host bindings; directive instances; providers; dynamic nodes | host prop names; directive tokens; provider tokens; `null` ## `HEADER` diff --git a/packages/core/src/render3/i18n.md b/packages/core/src/render3/i18n.md index d5b93aa4a6..c7fd23e14d 100644 --- a/packages/core/src/render3/i18n.md +++ b/packages/core/src/render3/i18n.md @@ -23,7 +23,7 @@ class MyComponent { ``` NOTE: - There really is only two kinds of i18n text. - 1. In attribute as in `i18n-title`. + 1. In attribute as in `title` (with `i18n-title` present). 2. In element body marked as as `
`. - The element body i18n can have internal DOM structure which may consist of sub-templates. @@ -160,8 +160,8 @@ i18nAttributes(1, MSG_div_attr); ``` The above instruction checks the `TView.data` cache at position `1` and if empty will create `I18nUpdateOpCodes` like so: ```typescript -[ - // The following OpCodes represent: `
` +const i18nUpdateOpCodes = [ + // The following OpCodes represent: `
` // If `changeMask & 0b11` // has changed then execute update OpCodes. // has NOT changed then skip `7` values and start processing next OpCodes. @@ -170,12 +170,13 @@ The above instruction checks the `TView.data` cache at position `1` and if empty 'Hello ', // accumulate('Hello '); -1, // accumulate(-1); '!', // accumulate('!'); - // Update attribute: `elementAttribute(1, 'title', accumulatorFlush(null));` + // Update attribute: `elementAttribute(0, 'title', accumulatorFlush(null));` // NOTE: `null` means don't sanitize - 1 << SHIFT_REF | Attr, 'title', null, + 0 << SHIFT_REF | Attr, 'title', null, ] ``` NOTE: +- `i18nAttributes` updates the attributes of the previous element. - If there is more than one attribute which needs to be internationalized it is added to the array as `[attributeName, translation]` tuple. - Even attributes which don't have bindings must go through `i18nAttributes` so that they correctly work with i18n in server environment. @@ -216,7 +217,7 @@ will generate ```typescript // Text broken down to allow addition of comments (Generated code will not have comments) const MSG_div = - 'List: ' + 'List: ' + '�*2:1�' + // template(2, MyComponent_NgIf_Template_0, ...); '�#1:1�' + // elementStart(1, 'ul'); '�*2:2�' + // template(2, MyComponent_NgIf_NgFor_Template_1, ...); @@ -298,7 +299,7 @@ i18nEnd(); // The instruction which is responsible for inserting text node The `i18nStart` generates these instructions which are cached in the `TView` and then processed by `i18nEnd`. ```typescript -{ +const tI18n = { vars: 2, // Number of slots to allocate in EXPANDO. expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 create: [ // Processed by `i18nEnd` @@ -362,7 +363,7 @@ This case is more complex because it contains an ICU. ICUs are pre-parsed and then stored in the `TVIEW.data` as follows. ```typescript -{ +const tI18n = { vars: 3 + Math.max(4, 3, 3), // Number of slots to allocate in EXPANDO. (Max of all ICUs + fixed) expandoStartIndex: 200, // Assume in this example EXPANDO starts at 200 create: [ @@ -379,16 +380,16 @@ ICUs are pre-parsed and then stored in the `TVIEW.data` as follows. // has NOT changed then skip `2` values and start processing next OpCodes. 0b1, 2, -1, // accumulate(-1); - // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` - 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, + // Switch ICU: `icuSwitchCase(lViewData[200 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` + 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. 0b1, 1, - // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/);` - // SHIFT_ICU: points to: `i18nStart(0, MSG_div, 1);` - // SHIFT_REF: is an index into which ICU is being updated. In our example we only have + // Update ICU: `icuUpdateCase(lViewData[200 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/);` + // SHIFT_REF: points to: `i18nStart(0, MSG_div, 1);` + // SHIFT_ICU: is an index into which ICU is being updated. In our example we only have // one ICU so it is 0-th ICU to update. - 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, + 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, ], icus: [ { @@ -544,7 +545,7 @@ The OpCodes require that offsets for the EXPANDO index for the reference. The question is how do we compute this: ```typescript -{ +const tI18n = { vars: 1, expandoStartIndex: 100, // Retrieved from `tView.blueprint.length` at i18nStart invocation. create: [ @@ -623,7 +624,7 @@ The rules for attribute ICUs should be the same as for normal ICUs. For this reason we would like to reuse as much code as possible for parsing and processing of the ICU for simplicity and consistency. ```typescript -{ +const tI18n = { vars: 0, // Number of slots to allocate in EXPANDO. (Max of all ICUs + fixed) expandoStartIndex: 200, // Assume in this example EXPANDO starts at 200 create: [ @@ -635,18 +636,18 @@ For this reason we would like to reuse as much code as possible for parsing and // has NOT changed then skip `2` values and start processing next OpCodes. 0b1, 2, -1, // accumulate(-1) - // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` - 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, + // Switch ICU: `icuSwitchCase(lViewData[200 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` + 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. 0b1, 4, 'You have ', // accumulate('You have '); - // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/);` - // SHIFT_ICU: points to: `i18nStart(0, MSG_div, 1);` - // SHIFT_REF: is an index into which ICU is being updated. In our example we only have + // Update ICU: `icuUpdateCase(lViewData[200 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/);` + // SHIFT_REF: points to: `i18nStart(0, MSG_div, 1);` + // SHIFT_ICU: is an index into which ICU is being updated. In our example we only have // one ICU so it is 0-th ICU to update. - 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, + 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, '.', // accumulate('.'); @@ -699,7 +700,7 @@ For this reason we would like to reuse as much code as possible for parsing and // has changed then execute update OpCodes. // has NOT changed then skip `1` values and start processing next OpCodes. -1, 2, - -1, // accumulate(lviewData[bindIndex-1]); + -1, // accumulate(lViewData[bindIndex-1]); 'emails', // accumulate('no emails'); ] ] @@ -724,7 +725,7 @@ Given ``` The above needs to be parsed into: ```TypeScript -{ +const icu = { type: 'plural', // or 'select' expressionBindingIndex: 0, // from �0�, cases: [ @@ -757,7 +758,7 @@ NOTE: The updates to attributes with placeholders require that we go through san ## Translation without top level element -Placing `i18n` attribute on an existing elements is easy because the element defines parent and the translated element can be insert synchronously. +Placing `i18n` attribute on an existing elements is easy because the element defines parent and the translated element can be inserted synchronously. For virtual elements such as `` or `` this is more complicated because there is no common root element to insert into. In such a case the `i18nStart` acts as the element to insert into. This is similar to `` behavior. @@ -783,7 +784,7 @@ function MyComponent_Template_0(rf: RenderFlags, ctx: any) { Which would get parsed into: ```typescript -{ +const tI18n = { vars: 2, // Number of slots to allocate in EXPANDO. expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 create: [ // Processed by `i18nEnd` @@ -869,7 +870,7 @@ NOTE: The internal data structure will be: ```typescript -{ +const tI18n = { vars: 2, // Number of slots to allocate in EXPANDO. expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 create: [ // Processed by `i18nEnd` @@ -881,16 +882,16 @@ The internal data structure will be: // has NOT changed then skip `2` values and start processing next OpCodes. 0b1, 2, -1, // accumulate(-1); - // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` - 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, + // Switch ICU: `icuSwitchCase(lViewData[100 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` + 100 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. 0b1, 1, - // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/);` - // SHIFT_ICU: points to: `i18nStart(0, MSG_div, 1);` - // SHIFT_REF: is an index into which ICU is being updated. In our example we only have + // Update ICU: `icuUpdateCase(lViewData[100 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/);` + // SHIFT_REF: points to: `i18nStart(0, MSG_div, 1);` + // SHIFT_ICU: is an index into which ICU is being updated. In our example we only have // one ICU so it is 0-th ICU to update. - 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, + 100 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, ], icus: [ { // {�0�, plural, =0 {zero} other {�0� }} @@ -906,7 +907,7 @@ The internal data structure will be: '', 1 << SHIFT_PARENT | AppendChild, // Expando location: 100 COMMENT_MARKER, '', 0 << SHIFT_PARENT | AppendChild, // Expando location: 101 ], - ] + ], remove: [ [ // Case: `0`: `{zero}` 1 << SHIFT_PARENT | 100 << SHIFT_REF | Remove, @@ -921,16 +922,16 @@ The internal data structure will be: ], [ // Case: `other`: `{�0� }` 0b1, 3, - -2, ' ', 100 << SHIFT_REF | Text // Case: `�0� ` + -2, ' ', 100 << SHIFT_REF | Text, // Case: `�0� ` 0b10, 5, - -1 - // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 1 /*SHIFT_REF*/, accumulatorFlush());` - 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuSwitch, + -1, + // Switch ICU: `icuSwitchCase(lViewData[101 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` + 101 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks int the ICU. 0b10, 1, - // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 1 /*SHIFT_REF*/);` - 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuUpdate, + // Update ICU: `icuUpdateCase(lViewData[101 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/);` + 101 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, ], ] }, @@ -1057,7 +1058,7 @@ Here is a more complete example. Given this Angular's template: ```HTML -
+
{{count}} is rendered as: { count, plural, @@ -1111,7 +1112,7 @@ To generate code where the extracted i18n messages have the same ids, the `ngtsc Given this Angular's template: ```HTML -
+
{{count}} is rendered as: { count, plural, diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts index c92dd51091..7e4ec1e31d 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -6,277 +6,471 @@ * found in the LICENSE file at https://angular.io/license */ -import {NO_CHANGE} from '../../src/render3/tokens'; - -import {assertEqual, assertLessThan} from './assert'; -import {adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createNodeAtIndex, load} from './instructions'; +import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer'; +import {InertBodyHelper} from '../sanitization/inert_body'; +import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; +import {assertDefined, assertEqual, assertGreaterThan} from './assert'; +import {allocExpando, createNodeAtIndex, elementAttribute, load, textBinding} from './instructions'; import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container'; -import {TElementNode, TNode, TNodeType} from './interfaces/node'; +import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n'; +import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node'; import {RComment, RElement} from './interfaces/renderer'; +import {SanitizerFn} from './interfaces/sanitization'; import {StylingContext} from './interfaces/styling'; -import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, TVIEW} from './interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, LViewData, TVIEW, TView} from './interfaces/view'; import {appendChild, createTextNode, removeChild} from './node_manipulation'; -import {getRenderer, getViewData, resetComponentState} from './state'; -import {getNativeByIndex, getNativeByTNode, getTNode, isLContainer, stringify} from './util'; +import {_getViewData, getIsParent, getPreviousOrParentTNode, getRenderer, getTView, setIsParent, setPreviousOrParentTNode} from './state'; +import {NO_CHANGE} from './tokens'; +import {addAllToArray, getNativeByIndex, getNativeByTNode, getTNode, isLContainer, stringify} from './util'; +const MARKER = `�`; +const ICU_BLOCK_REGEX = /^\s*(�\d+�)\s*,\s*(select|plural)\s*,/; +const SUBTEMPLATE_REGEXP = /�\/?\*(\d+:\d+)�/gi; +const PH_REGEXP = /�(\/?[#*]\d+):?\d*�/gi; +const BINDING_REGEXP = /�(\d+):?\d*�/gi; +const ICU_REGEXP = /({\s*�\d+�\s*,\s*\S{6}\s*,[\s\S]*})/gi; +interface IcuExpression { + type: IcuType; + mainBinding: number; + cases: string[]; + values: (string|IcuExpression)[][]; +} -/** - * A list of flags to encode the i18n instructions used to translate the template. - * We shift the flags by 29 so that 30 & 31 & 32 bits contains the instructions. - */ -export const enum I18nInstructions { - Text = 1 << 29, - Element = 2 << 29, - Expression = 3 << 29, - TemplateRoot = 4 << 29, - Any = 5 << 29, - CloseNode = 6 << 29, - RemoveNode = 7 << 29, - /** Used to decode the number encoded with the instruction. */ - IndexMask = (1 << 29) - 1, - /** Used to test the type of instruction. */ - InstructionMask = ~((1 << 29) - 1), +interface IcuCase { + /** + * Number of slots to allocate in expando for this case. + * + * This is the max number of DOM elements which will be created by this i18n + ICU blocks. When + * the DOM elements are being created they are stored in the EXPANDO, so that update OpCodes can + * write into them. + */ + vars: number; + + /** + * An optional array of child/sub ICUs. + */ + childIcus: number[]; + + /** + * A set of OpCodes to apply in order to build up the DOM render tree for the ICU + */ + create: I18nMutateOpCodes; + + /** + * A set of OpCodes to apply in order to destroy the DOM render tree for the ICU. + */ + remove: I18nMutateOpCodes; + + /** + * A set of OpCodes to apply in order to update the DOM render tree for the ICU bindings. + */ + update: I18nUpdateOpCodes; } /** - * Represents the instructions used to translate the template. - * Instructions can be a placeholder index, a static text or a simple bit field (`I18nFlag`). - * When the instruction is the flag `Text`, it is always followed by its text value. + * Breaks pattern into strings and top level {...} blocks. + * Can be used to break a message into text and ICU expressions, or to break an ICU expression into + * keys and cases. + * Original code from closure library, modified for Angular. + * + * @param pattern (sub)Pattern to be broken. + * */ -export type I18nInstruction = number | string; -/** - * Represents the instructions used to translate attributes containing expressions. - * Even indexes contain static strings, while odd indexes contain the index of the expression whose - * value will be concatenated into the final translation. - */ -export type I18nExpInstruction = number | string; -/** Mapping of placeholder names to their absolute indexes in their templates. */ -export type PlaceholderMap = { - [name: string]: number -}; -const i18nTagRegex = /{\$([^}]+)}/g; - -/** - * Takes a translation string, the initial list of placeholders (elements and expressions) and the - * indexes of their corresponding expression nodes to return a list of instructions for each - * template function. - * - * Because embedded templates have different indexes for each placeholder, each parameter (except - * the translation) is an array, where each value corresponds to a different template, by order of - * appearance. - * - * @param translation A translation string where placeholders are represented by `{$name}` - * @param elements An array containing, for each template, the maps of element placeholders and - * their indexes. - * @param expressions An array containing, for each template, the maps of expression placeholders - * and their indexes. - * @param templateRoots An array of template roots whose content should be ignored when - * generating the instructions for their parent template. - * @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is - * an ng-container. - * - * @returns A list of instructions used to translate each template. - */ -export function i18nMapping( - translation: string, elements: (PlaceholderMap | null)[] | null, - expressions?: (PlaceholderMap | null)[] | null, templateRoots?: string[] | null, - lastChildIndex?: number | null): I18nInstruction[][] { - const translationParts = translation.split(i18nTagRegex); - const nbTemplates = templateRoots ? templateRoots.length + 1 : 1; - const instructions: I18nInstruction[][] = (new Array(nbTemplates)).fill(undefined); - - generateMappingInstructions( - 0, 0, translationParts, instructions, elements, expressions, templateRoots, lastChildIndex); - - return instructions; -} - -/** - * Internal function that reads the translation parts and generates a set of instructions for each - * template. - * - * See `i18nMapping()` for more details. - * - * @param tmplIndex The order of appearance of the template. - * 0 for the root template, following indexes match the order in `templateRoots`. - * @param partIndex The current index in `translationParts`. - * @param translationParts The translation string split into an array of placeholders and text - * elements. - * @param instructions The current list of instructions to update. - * @param elements An array containing, for each template, the maps of element placeholders and - * their indexes. - * @param expressions An array containing, for each template, the maps of expression placeholders - * and their indexes. - * @param templateRoots An array of template roots whose content should be ignored when - * generating the instructions for their parent template. - * @param lastChildIndex The index of the last child of the i18n node. Used when the i18n block is - * an ng-container. - * - * @returns the current index in `translationParts` - */ -function generateMappingInstructions( - tmplIndex: number, partIndex: number, translationParts: string[], - instructions: I18nInstruction[][], elements: (PlaceholderMap | null)[] | null, - expressions?: (PlaceholderMap | null)[] | null, templateRoots?: string[] | null, - lastChildIndex?: number | null): number { - const tmplInstructions: I18nInstruction[] = []; - const phVisited: string[] = []; - let openedTagCount = 0; - let maxIndex = 0; - let currentElements: PlaceholderMap|null = - elements && elements[tmplIndex] ? elements[tmplIndex] : null; - let currentExpressions: PlaceholderMap|null = - expressions && expressions[tmplIndex] ? expressions[tmplIndex] : null; - - instructions[tmplIndex] = tmplInstructions; - - for (; partIndex < translationParts.length; partIndex++) { - // The value can either be text or the name of a placeholder (element/template root/expression) - const value = translationParts[partIndex]; - - // Odd indexes are placeholders - if (partIndex & 1) { - let phIndex; - if (currentElements && currentElements[value] !== undefined) { - phIndex = currentElements[value]; - // The placeholder represents a DOM element, add an instruction to move it - let templateRootIndex = templateRoots ? templateRoots.indexOf(value) : -1; - if (templateRootIndex !== -1 && (templateRootIndex + 1) !== tmplIndex) { - // This is a template root, it has no closing tag, not treating it as an element - tmplInstructions.push(phIndex | I18nInstructions.TemplateRoot); - } else { - tmplInstructions.push(phIndex | I18nInstructions.Element); - openedTagCount++; - } - phVisited.push(value); - } else if (currentExpressions && currentExpressions[value] !== undefined) { - phIndex = currentExpressions[value]; - // The placeholder represents an expression, add an instruction to move it - tmplInstructions.push(phIndex | I18nInstructions.Expression); - phVisited.push(value); - } else { - // It is a closing tag - tmplInstructions.push(I18nInstructions.CloseNode); - - if (tmplIndex > 0) { - openedTagCount--; - - // If we have reached the closing tag for this template, exit the loop - if (openedTagCount === 0) { - break; - } - } - } - - if (phIndex !== undefined && phIndex > maxIndex) { - maxIndex = phIndex; - } - - if (templateRoots) { - const newTmplIndex = templateRoots.indexOf(value) + 1; - if (newTmplIndex !== 0 && newTmplIndex !== tmplIndex) { - partIndex = generateMappingInstructions( - newTmplIndex, partIndex, translationParts, instructions, elements, expressions, - templateRoots, lastChildIndex); - } - } - - } else if (value) { - // It's a non-empty string, create a text node - tmplInstructions.push(I18nInstructions.Text, value); - } +function extractParts(pattern: string): (string | IcuExpression)[] { + if (!pattern) { + return []; } - // Add instructions to remove elements that are not used in the translation - if (elements) { - const tmplElements = elements[tmplIndex]; + let prevPos = 0; + const braceStack = []; + const results: (string | IcuExpression)[] = []; + const braces = /[{}]/g; + // lastIndex doesn't get set to 0 so we have to. + braces.lastIndex = 0; - if (tmplElements) { - const phKeys = Object.keys(tmplElements); + let match; + while (match = braces.exec(pattern)) { + const pos = match.index; + if (match[0] == '}') { + braceStack.pop(); - for (let i = 0; i < phKeys.length; i++) { - const ph = phKeys[i]; - - if (phVisited.indexOf(ph) === -1) { - let index = tmplElements[ph]; - // Add an instruction to remove the element - tmplInstructions.push(index | I18nInstructions.RemoveNode); - - if (index > maxIndex) { - maxIndex = index; - } + if (braceStack.length == 0) { + // End of the block. + const block = pattern.substring(prevPos, pos); + if (ICU_BLOCK_REGEX.test(block)) { + results.push(parseICUBlock(block)); + } else if (block) { // Don't push empty strings + results.push(block); } + + prevPos = pos + 1; } - } - } - - // Add instructions to remove expressions that are not used in the translation - if (expressions) { - const tmplExpressions = expressions[tmplIndex]; - - if (tmplExpressions) { - const phKeys = Object.keys(tmplExpressions); - - for (let i = 0; i < phKeys.length; i++) { - const ph = phKeys[i]; - - if (phVisited.indexOf(ph) === -1) { - let index = tmplExpressions[ph]; - if (ngDevMode) { - assertLessThan( - index.toString(2).length, 28, `Index ${index} is too big and will overflow`); - } - // Add an instruction to remove the expression - tmplInstructions.push(index | I18nInstructions.RemoveNode); - - if (index > maxIndex) { - maxIndex = index; - } - } - } - } - } - - if (tmplIndex === 0 && typeof lastChildIndex === 'number') { - // The current parent is an ng-container and it has more children after the translation that we - // need to append to keep the order of the DOM nodes correct - for (let i = maxIndex + 1; i <= lastChildIndex; i++) { - if (ngDevMode) { - assertLessThan(i.toString(2).length, 28, `Index ${i} is too big and will overflow`); - } - tmplInstructions.push(i | I18nInstructions.Any); - } - } - - return partIndex; -} - -function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode): TNode { - if (ngDevMode) { - ngDevMode.rendererMoveNode++; - } - - const viewData = getViewData(); - - // On first pass, re-organize node tree to put this node in the correct position. - const firstTemplatePass = viewData[TVIEW].firstTemplatePass; - if (firstTemplatePass) { - if (previousTNode === parentTNode && tNode !== parentTNode.child) { - tNode.next = parentTNode.child; - parentTNode.child = tNode; - } else if (previousTNode !== parentTNode && tNode !== previousTNode.next) { - tNode.next = previousTNode.next; - previousTNode.next = tNode; } else { - tNode.next = null; + if (braceStack.length == 0) { + const substring = pattern.substring(prevPos, pos); + results.push(substring); + prevPos = pos + 1; + } + braceStack.push('{'); + } + } + + const substring = pattern.substring(prevPos); + if (substring != '') { + results.push(substring); + } + + return results; +} + +/** + * Parses text containing an ICU expression and produces a JSON object for it. + * Original code from closure library, modified for Angular. + * + * @param pattern Text containing an ICU expression that needs to be parsed. + * + */ +function parseICUBlock(pattern: string): IcuExpression { + const cases = []; + const values: (string | IcuExpression)[][] = []; + let icuType = IcuType.plural; + let mainBinding = 0; + pattern = pattern.replace(ICU_BLOCK_REGEX, function(str: string, binding: string, type: string) { + if (type === 'select') { + icuType = IcuType.select; + } else { + icuType = IcuType.plural; + } + mainBinding = parseInt(binding.substr(1), 10); + return ''; + }); + + const parts = extractParts(pattern) as string[]; + // Looking for (key block)+ sequence. One of the keys has to be "other". + for (let pos = 0; pos < parts.length;) { + let key = parts[pos++].trim(); + if (icuType === IcuType.plural) { + // Key can be "=x", we just want "x" + key = key.replace(/\s*(?:=)?(\w+)\s*/, '$1'); + } + if (key.length) { + cases.push(key); } - if (parentTNode !== viewData[HOST_NODE]) { - tNode.parent = parentTNode as TElementNode; + const blocks = extractParts(parts[pos++]) as string[]; + if (blocks.length) { + values.push(blocks); } } + assertGreaterThan(cases.indexOf('other'), -1, 'Missing key "other" in ICU statement.'); + // TODO(ocombe): support ICU expressions in attributes, see #21615 + return {type: icuType, mainBinding: mainBinding, cases, values}; +} + +/** + * Removes everything inside the sub-templates of a message. + */ +function removeInnerTemplateTranslation(message: string): string { + let match; + let res = ''; + let index = 0; + let inTemplate = false; + let tagMatched; + + while ((match = SUBTEMPLATE_REGEXP.exec(message)) !== null) { + if (!inTemplate) { + res += message.substring(index, match.index + match[0].length); + tagMatched = match[1]; + inTemplate = true; + } else { + if (match[0] === `${MARKER}/*${tagMatched}${MARKER}`) { + index = match.index; + inTemplate = false; + } + } + } + + ngDevMode && + assertEqual( + inTemplate, false, + `Tag mismatch: unable to find the end of the sub-template in the translation "${message}"`); + + res += message.substr(index); + return res; +} + +/** + * Extracts a part of a message and removes the rest. + * + * This method is used for extracting a part of the message associated with a template. A translated + * message can span multiple templates. + * + * Example: + * ``` + *
Translate me!
+ * ``` + * + * @param message The message to crop + * @param subTemplateIndex Index of the sub-template to extract. If undefined it returns the + * external template and removes all sub-templates. + */ +export function getTranslationForTemplate(message: string, subTemplateIndex?: number) { + if (typeof subTemplateIndex !== 'number') { + // We want the root template message, ignore all sub-templates + return removeInnerTemplateTranslation(message); + } else { + // We want a specific sub-template + const start = + message.indexOf(`:${subTemplateIndex}${MARKER}`) + 2 + subTemplateIndex.toString().length; + const end = message.search(new RegExp(`${MARKER}\\/\\*\\d+:${subTemplateIndex}${MARKER}`)); + return removeInnerTemplateTranslation(message.substring(start, end)); + } +} + +/** + * Generate the OpCodes to update the bindings of a string. + * + * @param str The string containing the bindings. + * @param destinationNode Index of the destination node which will receive the binding. + * @param attrName Name of the attribute, if the string belongs to an attribute. + * @param sanitizeFn Sanitization function used to sanitize the string after update, if necessary. + */ +function generateBindingUpdateOpCodes( + str: string, destinationNode: number, attrName?: string, + sanitizeFn: SanitizerFn | null = null): I18nUpdateOpCodes { + const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size + const textParts = str.split(BINDING_REGEXP); + let mask = 0; + + for (let j = 0; j < textParts.length; j++) { + const textValue = textParts[j]; + + if (j & 1) { + // Odd indexes are bindings + const bindingIndex = parseInt(textValue, 10); + updateOpCodes.push(-1 - bindingIndex); + mask = mask | toMaskBit(bindingIndex); + } else if (textValue !== '') { + // Even indexes are text + updateOpCodes.push(textValue); + } + } + + updateOpCodes.push( + destinationNode << I18nUpdateOpCode.SHIFT_REF | + (attrName ? I18nUpdateOpCode.Attr : I18nUpdateOpCode.Text)); + if (attrName) { + updateOpCodes.push(attrName, sanitizeFn); + } + updateOpCodes[0] = mask; + updateOpCodes[1] = updateOpCodes.length - 2; + return updateOpCodes; +} + +function getBindingMask(icuExpression: IcuExpression, mask = 0): number { + mask = mask | toMaskBit(icuExpression.mainBinding); + let match; + for (let i = 0; i < icuExpression.values.length; i++) { + const valueArr = icuExpression.values[i]; + for (let j = 0; j < valueArr.length; j++) { + const value = valueArr[j]; + if (typeof value === 'string') { + while (match = BINDING_REGEXP.exec(value)) { + mask = mask | toMaskBit(parseInt(match[1], 10)); + } + } else { + mask = getBindingMask(value as IcuExpression, mask); + } + } + } + return mask; +} + +const i18nIndexStack: number[] = []; +let i18nIndexStackPointer = -1; + +/** + * Convert binding index to mask bit. + * + * Each index represents a single bit on the bit-mask. Because bit-mask only has 32 bits, we make + * the 32nd bit share all masks for all bindings higher than 32. Since it is extremely rare to have + * more than 32 bindings this will be hit very rarely. The downside of hitting this corner case is + * that we will execute binding code more often than necessary. (penalty of performance) + */ +function toMaskBit(bindingIndex: number): number { + return 1 << Math.min(bindingIndex, 31); +} + +const parentIndexStack: number[] = []; + +/** + * Marks a block of text as translatable. + * + * The instructions `i18nStart` and `i18nEnd` mark the translation block in the template. + * The translation `message` is the value which is locale specific. The translation string may + * contain placeholders which associate inner elements and sub-templates within the translation. + * + * The translation `message` placeholders are: + * - `�{index}(:{block})�`: *Binding Placeholder*: Marks a location where an expression will be + * interpolated into. The placeholder `index` points to the expression binding index. An optional + * `block` that matches the sub-template in which it was declared. + * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *Element Placeholder*: Marks the beginning + * and end of DOM element that were embedded in the original translation block. The placeholder + * `index` points to the element index in the template instructions set. An optional `block` that + * matches the sub-template in which it was declared. + * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *Sub-template Placeholder*: Sub-templates must be + * split up and translated separately in each angular template function. The `index` points to the + * `template` instruction index. A `block` that matches the sub-template in which it was declared. + * + * @param index A unique index of the translation in the static block. + * @param message The translation message. + * @param subTemplateIndex Optional sub-template index in the `message`. + */ +export function i18nStart(index: number, message: string, subTemplateIndex?: number): void { + const tView = getTView(); + ngDevMode && assertDefined(tView, `tView should be defined`); + ngDevMode && + assertEqual( + tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`); + if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) { + i18nStartFirstPass(tView, index, message, subTemplateIndex); + } +} + +/** + * See `i18nStart` above. + */ +function i18nStartFirstPass( + tView: TView, index: number, message: string, subTemplateIndex?: number) { + i18nIndexStack[++i18nIndexStackPointer] = index; + const viewData = _getViewData(); + const expandoStartIndex = tView.blueprint.length - HEADER_OFFSET; + const previousOrParentTNode = getPreviousOrParentTNode(); + const parentTNode = getIsParent() ? getPreviousOrParentTNode() : + previousOrParentTNode && previousOrParentTNode.parent; + let parentIndex = parentTNode && parentTNode !== viewData[HOST_NODE] ? + parentTNode.index - HEADER_OFFSET : + index; + let parentIndexPointer = 0; + parentIndexStack[parentIndexPointer] = parentIndex; + const createOpCodes: I18nMutateOpCodes = []; + // If the previous node wasn't the direct parent then we have a translation without top level + // element and we need to keep a reference of the previous element if there is one + if (index > 0 && previousOrParentTNode !== parentTNode) { + // Create an OpCode to select the previous TNode + createOpCodes.push( + previousOrParentTNode.index << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select); + } + const updateOpCodes: I18nUpdateOpCodes = []; + const icuExpressions: TIcu[] = []; + + const templateTranslation = getTranslationForTemplate(message, subTemplateIndex); + const msgParts = templateTranslation.split(PH_REGEXP); + for (let i = 0; i < msgParts.length; i++) { + let value = msgParts[i]; + if (i & 1) { + // Odd indexes are placeholders (elements and sub-templates) + if (value.charAt(0) === '/') { + // It is a closing tag + if (value.charAt(1) === '#') { + const phIndex = parseInt(value.substr(2), 10); + parentIndex = parentIndexStack[--parentIndexPointer]; + createOpCodes.push(phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd); + } + } else { + const phIndex = parseInt(value.substr(1), 10); + // The value represents a placeholder that we move to the designated index + createOpCodes.push( + phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select, + parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); + + if (value.charAt(0) === '#') { + parentIndexStack[++parentIndexPointer] = parentIndex = phIndex; + } + } + } else { + // Even indexes are text (including bindings & ICU expressions) + const parts = value.split(ICU_REGEXP); + for (let j = 0; j < parts.length; j++) { + value = parts[j]; + + if (j & 1) { + // Odd indexes are ICU expressions + // Create the comment node that will anchor the ICU expression + allocExpando(viewData); + const icuNodeIndex = tView.blueprint.length - 1 - HEADER_OFFSET; + createOpCodes.push( + COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', + parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); + + // Update codes for the ICU expression + const icuExpression = parseICUBlock(value.substr(1, value.length - 2)); + const mask = getBindingMask(icuExpression); + icuStart(icuExpressions, icuExpression, icuNodeIndex, icuNodeIndex); + // Since this is recursive, the last TIcu that was pushed is the one we want + const tIcuIndex = icuExpressions.length - 1; + updateOpCodes.push( + toMaskBit(icuExpression.mainBinding), // mask of the main binding + 3, // skip 3 opCodes if not changed + -1 - icuExpression.mainBinding, + icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex, + mask, // mask of all the bindings of this ICU expression + 2, // skip 2 opCodes if not changed + icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex); + } else if (value !== '') { + // Even indexes are text (including bindings) + const hasBinding = value.match(BINDING_REGEXP); + // Create text nodes + allocExpando(viewData); + createOpCodes.push( + // If there is a binding, the value will be set during update + hasBinding ? '' : value, + parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); + + if (hasBinding) { + addAllToArray( + generateBindingUpdateOpCodes(value, tView.blueprint.length - 1 - HEADER_OFFSET), + updateOpCodes); + } + } + } + } + } + + // NOTE: local var needed to properly assert the type of `TI18n`. + const tI18n: TI18n = { + vars: tView.blueprint.length - HEADER_OFFSET - expandoStartIndex, + expandoStartIndex, + create: createOpCodes, + update: updateOpCodes, + icus: icuExpressions.length ? icuExpressions : null, + }; + tView.data[index + HEADER_OFFSET] = tI18n; +} + +function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode | null): TNode { + ngDevMode && ngDevMode.rendererMoveNode++; + const viewData = _getViewData(); + if (!previousTNode) { + previousTNode = parentTNode; + } + // re-organize node tree to put this node in the correct position. + if (previousTNode === parentTNode && tNode !== parentTNode.child) { + tNode.next = parentTNode.child; + parentTNode.child = tNode; + } else if (previousTNode !== parentTNode && tNode !== previousTNode.next) { + tNode.next = previousTNode.next; + previousTNode.next = tNode; + } else { + tNode.next = null; + } + + if (parentTNode !== viewData[HOST_NODE]) { + tNode.parent = parentTNode as TElementNode; + } + appendChild(getNativeByTNode(tNode, viewData), tNode, viewData); const slotValue = viewData[tNode.index]; @@ -288,492 +482,978 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode): return tNode; } -export function i18nAttribute(index: number, attrs: any[]): void { - // placeholder for i18nAttribute function -} - -export function i18nExp(expression: any): void { - // placeholder for i18nExp function -} - -export function i18nStart(index: number, message: string, subTemplateIndex: number = 0): void { - // placeholder for i18nExp function -} - +/** + * Translates a translation block marked by `i18nStart` and `i18nEnd`. It inserts the text/ICU nodes + * into the render tree, moves the placeholder nodes and removes the deleted nodes. + */ export function i18nEnd(): void { - // placeholder for i18nEnd function + const tView = getTView(); + ngDevMode && assertDefined(tView, `tView should be defined`); + ngDevMode && + assertEqual( + tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`); + if (tView.firstTemplatePass) { + i18nEndFirstPass(tView); + } } /** - * Takes a list of instructions generated by `i18nMapping()` to transform the template accordingly. - * - * @param startIndex Index of the first element to translate (for instance the first child of the - * element with the i18n attribute). - * @param instructions The list of instructions to apply on the current view. + * See `i18nEnd` above. */ -export function i18nApply(startIndex: number, instructions: I18nInstruction[]): void { - const viewData = getViewData(); - if (ngDevMode) { - assertEqual( - viewData[BINDING_INDEX], viewData[TVIEW].bindingStartIndex, - 'i18nApply should be called before any binding'); - } +function i18nEndFirstPass(tView: TView) { + const viewData = _getViewData(); + ngDevMode && assertEqual( + viewData[BINDING_INDEX], viewData[TVIEW].bindingStartIndex, + 'i18nEnd should be called before any binding'); - if (!instructions) { - return; - } + const rootIndex = i18nIndexStack[i18nIndexStackPointer--]; + const tI18n = tView.data[rootIndex + HEADER_OFFSET] as TI18n; + ngDevMode && assertDefined(tI18n, `You should call i18nStart before i18nEnd`); + // The last placeholder that was added before `i18nEnd` + const previousOrParentTNode = getPreviousOrParentTNode(); + const visitedPlaceholders = + readCreateOpCodes(rootIndex, tI18n.create, tI18n.expandoStartIndex, viewData); + + // Remove deleted placeholders + // The last placeholder that was added before `i18nEnd` is `previousOrParentTNode` + for (let i = rootIndex + 1; i <= previousOrParentTNode.index - HEADER_OFFSET; i++) { + if (visitedPlaceholders.indexOf(i) === -1) { + removeNode(i, viewData); + } + } +} + +function readCreateOpCodes( + index: number, createOpCodes: I18nMutateOpCodes, expandoStartIndex: number, + viewData: LViewData): number[] { const renderer = getRenderer(); - const startTNode = getTNode(startIndex, viewData); - let localParentTNode: TNode = startTNode.parent || viewData[HOST_NODE] !; - let localPreviousTNode: TNode = localParentTNode; - resetComponentState(); // We don't want to add to the tree with the wrong previous node - - for (let i = 0; i < instructions.length; i++) { - const instruction = instructions[i] as number; - switch (instruction & I18nInstructions.InstructionMask) { - case I18nInstructions.Element: - const elementTNode = getTNode(instruction & I18nInstructions.IndexMask, viewData); - localPreviousTNode = appendI18nNode(elementTNode, localParentTNode, localPreviousTNode); - localParentTNode = elementTNode; - break; - case I18nInstructions.Expression: - case I18nInstructions.TemplateRoot: - case I18nInstructions.Any: - const nodeIndex = instruction & I18nInstructions.IndexMask; - localPreviousTNode = - appendI18nNode(getTNode(nodeIndex, viewData), localParentTNode, localPreviousTNode); - break; - case I18nInstructions.Text: - if (ngDevMode) { - ngDevMode.rendererCreateTextNode++; - } - const value = instructions[++i]; - const textRNode = createTextNode(value, renderer); - // If we were to only create a `RNode` then projections won't move the text. - // Create text node at the current end of viewData. Must subtract header offset because - // createNodeAtIndex takes a raw index (not adjusted by header offset). - adjustBlueprintForNewNode(viewData); - const textTNode = createNodeAtIndex( - viewData.length - 1 - HEADER_OFFSET, TNodeType.Element, textRNode, null, null); - localPreviousTNode = appendI18nNode(textTNode, localParentTNode, localPreviousTNode); - resetComponentState(); - break; - case I18nInstructions.CloseNode: - localPreviousTNode = localParentTNode; - localParentTNode = localParentTNode.parent || viewData[HOST_NODE] !; - break; - case I18nInstructions.RemoveNode: - if (ngDevMode) { - ngDevMode.rendererRemoveNode++; - } - const removeIndex = instruction & I18nInstructions.IndexMask; - const removedElement: RElement|RComment = getNativeByIndex(removeIndex, viewData); - const removedTNode = getTNode(removeIndex, viewData); - removeChild(removedTNode, removedElement || null, viewData); - - const slotValue = load(removeIndex) as RElement | RComment | LContainer | StylingContext; - if (isLContainer(slotValue)) { - const lContainer = slotValue as LContainer; - if (removedTNode.type !== TNodeType.Container) { - removeChild(removedTNode, lContainer[NATIVE] || null, viewData); + let currentTNode: TNode|null = null; + let previousTNode: TNode|null = null; + const visitedPlaceholders: number[] = []; + for (let i = 0; i < createOpCodes.length; i++) { + const opCode = createOpCodes[i]; + if (typeof opCode == 'string') { + const textRNode = createTextNode(opCode, renderer); + ngDevMode && ngDevMode.rendererCreateTextNode++; + previousTNode = currentTNode; + currentTNode = + createNodeAtIndex(expandoStartIndex++, TNodeType.Element, textRNode, null, null); + setIsParent(false); + } else if (typeof opCode == 'number') { + switch (opCode & I18nMutateOpCode.MASK_OPCODE) { + case I18nMutateOpCode.AppendChild: + const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT; + let destinationTNode: TNode; + if (destinationNodeIndex === index) { + // If the destination node is `i18nStart`, we don't have a + // top-level node and we should use the host node instead + destinationTNode = viewData[HOST_NODE] !; + } else { + destinationTNode = getTNode(destinationNodeIndex, viewData); } - removedTNode.detached = true; - lContainer[RENDER_PARENT] = null; + ngDevMode && + assertDefined( + currentTNode !, + `You need to create or select a node before you can insert it into the DOM`); + previousTNode = appendI18nNode(currentTNode !, destinationTNode, previousTNode); + destinationTNode.next = null; + break; + case I18nMutateOpCode.Select: + const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + visitedPlaceholders.push(nodeIndex); + previousTNode = currentTNode; + currentTNode = getTNode(nodeIndex, viewData); + if (currentTNode) { + setPreviousOrParentTNode(currentTNode); + if (currentTNode.type === TNodeType.Element) { + setIsParent(true); + } + } + break; + case I18nMutateOpCode.ElementEnd: + const elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + previousTNode = currentTNode = getTNode(elementIndex, viewData); + setPreviousOrParentTNode(currentTNode); + setIsParent(false); + break; + case I18nMutateOpCode.Attr: + const elementNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; + const attrName = createOpCodes[++i] as string; + const attrValue = createOpCodes[++i] as string; + elementAttribute(elementNodeIndex, attrName, attrValue); + break; + default: + throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); + } + } else { + switch (opCode) { + case COMMENT_MARKER: + const commentValue = createOpCodes[++i] as string; + ngDevMode && assertEqual( + typeof commentValue, 'string', + `Expected "${commentValue}" to be a comment node value`); + const commentRNode = renderer.createComment(commentValue); + ngDevMode && ngDevMode.rendererCreateComment++; + previousTNode = currentTNode; + currentTNode = createNodeAtIndex( + expandoStartIndex++, TNodeType.IcuContainer, commentRNode, null, null); + (currentTNode as TIcuContainerNode).activeCaseIndex = null; + // We will add the case nodes later, during the update phase + setIsParent(false); + break; + case ELEMENT_MARKER: + const tagNameValue = createOpCodes[++i] as string; + ngDevMode && assertEqual( + typeof tagNameValue, 'string', + `Expected "${tagNameValue}" to be an element node tag name`); + const elementRNode = renderer.createElement(tagNameValue); + ngDevMode && ngDevMode.rendererCreateElement++; + previousTNode = currentTNode; + currentTNode = createNodeAtIndex( + expandoStartIndex++, TNodeType.Element, elementRNode, tagNameValue, null); + break; + default: + throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`); + } + } + } + + setIsParent(false); + + return visitedPlaceholders; +} + +function readUpdateOpCodes( + updateOpCodes: I18nUpdateOpCodes, icus: TIcu[] | null, bindingsStartIndex: number, + changeMask: number, viewData: LViewData, bypassCheckBit = false) { + let caseCreated = false; + for (let i = 0; i < updateOpCodes.length; i++) { + // bit code to check if we should apply the next update + const checkBit = updateOpCodes[i] as number; + // Number of opCodes to skip until next set of update codes + const skipCodes = updateOpCodes[++i] as number; + if (bypassCheckBit || (checkBit & changeMask)) { + // The value has been updated since last checked + let value = ''; + for (let j = i + 1; j <= (i + skipCodes); j++) { + const opCode = updateOpCodes[j]; + if (typeof opCode == 'string') { + value += opCode; + } else if (typeof opCode == 'number') { + if (opCode < 0) { + // It's a binding index whose value is negative + value += stringify(viewData[bindingsStartIndex - opCode]); + } else { + const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF; + switch (opCode & I18nUpdateOpCode.MASK_OPCODE) { + case I18nUpdateOpCode.Attr: + const attrName = updateOpCodes[++j] as string; + const sanitizeFn = updateOpCodes[++j] as SanitizerFn | null; + elementAttribute(nodeIndex, attrName, value, sanitizeFn); + break; + case I18nUpdateOpCode.Text: + textBinding(nodeIndex, value); + break; + case I18nUpdateOpCode.IcuSwitch: + let tIcuIndex = updateOpCodes[++j] as number; + let tIcu = icus ![tIcuIndex]; + let icuTNode = getTNode(nodeIndex, viewData) as TIcuContainerNode; + // If there is an active case, delete the old nodes + if (icuTNode.activeCaseIndex !== null) { + const removeCodes = tIcu.remove[icuTNode.activeCaseIndex]; + for (let k = 0; k < removeCodes.length; k++) { + const removeOpCode = removeCodes[k] as number; + switch (removeOpCode & I18nMutateOpCode.MASK_OPCODE) { + case I18nMutateOpCode.Remove: + const nodeIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF; + removeNode(nodeIndex, viewData); + break; + case I18nMutateOpCode.RemoveNestedIcu: + const nestedIcuNodeIndex = + removeCodes[k + 1] as number >>> I18nMutateOpCode.SHIFT_REF; + const nestedIcuTNode = + getTNode(nestedIcuNodeIndex, viewData) as TIcuContainerNode; + const activeIndex = nestedIcuTNode.activeCaseIndex; + if (activeIndex !== null) { + const nestedIcuTIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF; + const nestedTIcu = icus ![nestedIcuTIndex]; + addAllToArray(nestedTIcu.remove[activeIndex], removeCodes); + } + break; + } + } + } + + // Update the active caseIndex + const caseIndex = getCaseIndex(tIcu, value); + icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null; + + // Add the nodes for the new case + readCreateOpCodes(-1, tIcu.create[caseIndex], tIcu.expandoStartIndex, viewData); + caseCreated = true; + break; + case I18nUpdateOpCode.IcuUpdate: + tIcuIndex = updateOpCodes[++j] as number; + tIcu = icus ![tIcuIndex]; + icuTNode = getTNode(nodeIndex, viewData) as TIcuContainerNode; + readUpdateOpCodes( + tIcu.update[icuTNode.activeCaseIndex !], icus, bindingsStartIndex, changeMask, + viewData, caseCreated); + break; + } + } + } + } + } + i += skipCodes; + } +} + +function removeNode(index: number, viewData: LViewData) { + const removedPhTNode = getTNode(index, viewData); + const removedPhRNode = getNativeByIndex(index, viewData); + removeChild(removedPhTNode, removedPhRNode || null, viewData); + removedPhTNode.detached = true; + ngDevMode && ngDevMode.rendererRemoveNode++; + + const slotValue = load(index) as RElement | RComment | LContainer | StylingContext; + if (isLContainer(slotValue)) { + const lContainer = slotValue as LContainer; + if (removedPhTNode.type !== TNodeType.Container) { + removeChild(removedPhTNode, lContainer[NATIVE] || null, viewData); + } + lContainer[RENDER_PARENT] = null; + } +} + +/** + * + * Use this instruction to create a translation block that doesn't contain any placeholder. + * It calls both {@link i18nStart} and {@link i18nEnd} in one instruction. + * + * The translation `message` is the value which is locale specific. The translation string may + * contain placeholders which associate inner elements and sub-templates within the translation. + * + * The translation `message` placeholders are: + * - `�{index}(:{block})�`: *Binding Placeholder*: Marks a location where an expression will be + * interpolated into. The placeholder `index` points to the expression binding index. An optional + * `block` that matches the sub-template in which it was declared. + * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *Element Placeholder*: Marks the beginning + * and end of DOM element that were embedded in the original translation block. The placeholder + * `index` points to the element index in the template instructions set. An optional `block` that + * matches the sub-template in which it was declared. + * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *Sub-template Placeholder*: Sub-templates must be + * split up and translated separately in each angular template function. The `index` points to the + * `template` instruction index. A `block` that matches the sub-template in which it was declared. + * + * @param index A unique index of the translation in the static block. + * @param message The translation message. + * @param subTemplateIndex Optional sub-template index in the `message`. + */ +export function i18n(index: number, message: string, subTemplateIndex?: number): void { + i18nStart(index, message, subTemplateIndex); + i18nEnd(); +} + +/** + * Marks a list of attributes as translatable. + * + * @param index A unique index in the static block + * @param values + */ +export function i18nAttributes(index: number, values: string[]): void { + const tView = getTView(); + ngDevMode && assertDefined(tView, `tView should be defined`); + ngDevMode && + assertEqual( + tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`); + if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) { + i18nAttributesFirstPass(tView, index, values); + } +} + +/** + * See `i18nAttributes` above. + */ +function i18nAttributesFirstPass(tView: TView, index: number, values: string[]) { + const previousElement = getPreviousOrParentTNode(); + const previousElementIndex = previousElement.index - HEADER_OFFSET; + const updateOpCodes: I18nUpdateOpCodes = []; + for (let i = 0; i < values.length; i += 2) { + const attrName = values[i]; + const message = values[i + 1]; + const parts = message.split(ICU_REGEXP); + for (let j = 0; j < parts.length; j++) { + const value = parts[j]; + + if (j & 1) { + // Odd indexes are ICU expressions + // TODO(ocombe): support ICU expressions in attributes + } else if (value !== '') { + // Even indexes are text (including bindings) + const hasBinding = !!value.match(BINDING_REGEXP); + if (hasBinding) { + addAllToArray( + generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes); + } else { + elementAttribute(previousElementIndex, attrName, value); + } + } + } + } + + tView.data[index + HEADER_OFFSET] = updateOpCodes; +} + +let changeMask = 0b0; +let shiftsCounter = 0; + +/** + * Stores the values of the bindings during each update cycle in order to determine if we need to + * update the translated nodes. + * + * @param expression The binding's new value or NO_CHANGE + */ +export function i18nExp(expression: T | NO_CHANGE): void { + if (expression !== NO_CHANGE) { + changeMask = changeMask | (1 << shiftsCounter); + } + shiftsCounter++; +} + +/** + * Updates a translation block or an i18n attribute when the bindings have changed. + * + * @param index Index of either {@link i18nStart} (translation block) or {@link i18nAttributes} + * (i18n attribute) on which it should update the content. + */ +export function i18nApply(index: number) { + if (shiftsCounter) { + const tView = getTView(); + ngDevMode && assertDefined(tView, `tView should be defined`); + const viewData = _getViewData(); + const tI18n = tView.data[index + HEADER_OFFSET]; + let updateOpCodes: I18nUpdateOpCodes; + let icus: TIcu[]|null = null; + if (Array.isArray(tI18n)) { + updateOpCodes = tI18n as I18nUpdateOpCodes; + } else { + updateOpCodes = (tI18n as TI18n).update; + icus = (tI18n as TI18n).icus; + } + const bindingsStartIndex = viewData[BINDING_INDEX] - shiftsCounter - 1; + readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, viewData); + + // Reset changeMask & maskBit to default for the next update cycle + changeMask = 0b0; + shiftsCounter = 0; + } +} + +enum Plural { + Zero = 0, + One = 1, + Two = 2, + Few = 3, + Many = 4, + Other = 5, +} + +/** + * Returns the plural case based on the locale. + * This is a copy of the deprecated function that we used in Angular v4. + * // TODO(ocombe): remove this once we can the real getPluralCase function + * + * @deprecated from v5 the plural case function is in locale data files common/locales/*.ts + */ +function getPluralCase(locale: string, nLike: number | string): Plural { + if (typeof nLike === 'string') { + nLike = parseInt(nLike, 10); + } + const n: number = nLike as number; + const nDecimal = n.toString().replace(/^[^.]*\.?/, ''); + const i = Math.floor(Math.abs(n)); + const v = nDecimal.length; + const f = parseInt(nDecimal, 10); + const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0; + + const lang = locale.split('-')[0].toLowerCase(); + + switch (lang) { + case 'af': + case 'asa': + case 'az': + case 'bem': + case 'bez': + case 'bg': + case 'brx': + case 'ce': + case 'cgg': + case 'chr': + case 'ckb': + case 'ee': + case 'el': + case 'eo': + case 'es': + case 'eu': + case 'fo': + case 'fur': + case 'gsw': + case 'ha': + case 'haw': + case 'hu': + case 'jgo': + case 'jmc': + case 'ka': + case 'kk': + case 'kkj': + case 'kl': + case 'ks': + case 'ksb': + case 'ky': + case 'lb': + case 'lg': + case 'mas': + case 'mgo': + case 'ml': + case 'mn': + case 'nb': + case 'nd': + case 'ne': + case 'nn': + case 'nnh': + case 'nyn': + case 'om': + case 'or': + case 'os': + case 'ps': + case 'rm': + case 'rof': + case 'rwk': + case 'saq': + case 'seh': + case 'sn': + case 'so': + case 'sq': + case 'ta': + case 'te': + case 'teo': + case 'tk': + case 'tr': + case 'ug': + case 'uz': + case 'vo': + case 'vun': + case 'wae': + case 'xog': + if (n === 1) return Plural.One; + return Plural.Other; + case 'ak': + case 'ln': + case 'mg': + case 'pa': + case 'ti': + if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One; + return Plural.Other; + case 'am': + case 'as': + case 'bn': + case 'fa': + case 'gu': + case 'hi': + case 'kn': + case 'mr': + case 'zu': + if (i === 0 || n === 1) return Plural.One; + return Plural.Other; + case 'ar': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many; + return Plural.Other; + case 'ast': + case 'ca': + case 'de': + case 'en': + case 'et': + case 'fi': + case 'fy': + case 'gl': + case 'it': + case 'nl': + case 'sv': + case 'sw': + case 'ur': + case 'yi': + if (i === 1 && v === 0) return Plural.One; + return Plural.Other; + case 'be': + if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One; + if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 && + !(n % 100 >= 12 && n % 100 <= 14)) + return Plural.Few; + if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 || + n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'br': + if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One; + if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two; + if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) && + !(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 || + n % 100 >= 90 && n % 100 <= 99)) + return Plural.Few; + if (!(n === 0) && n % 1e6 === 0) return Plural.Many; + return Plural.Other; + case 'bs': + case 'hr': + case 'sr': + if (v === 0 && i % 10 === 1 && !(i % 100 === 11) || f % 10 === 1 && !(f % 100 === 11)) + return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14) || + f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 && + !(f % 100 >= 12 && f % 100 <= 14)) + return Plural.Few; + return Plural.Other; + case 'cs': + case 'sk': + if (i === 1 && v === 0) return Plural.One; + if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few; + if (!(v === 0)) return Plural.Many; + return Plural.Other; + case 'cy': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n === 3) return Plural.Few; + if (n === 6) return Plural.Many; + return Plural.Other; + case 'da': + if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One; + return Plural.Other; + case 'dsb': + case 'hsb': + if (v === 0 && i % 100 === 1 || f % 100 === 1) return Plural.One; + if (v === 0 && i % 100 === 2 || f % 100 === 2) return Plural.Two; + if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || + f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4) + return Plural.Few; + return Plural.Other; + case 'ff': + case 'fr': + case 'hy': + case 'kab': + if (i === 0 || i === 1) return Plural.One; + return Plural.Other; + case 'fil': + if (v === 0 && (i === 1 || i === 2 || i === 3) || + v === 0 && !(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) || + !(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9)) + return Plural.One; + return Plural.Other; + case 'ga': + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few; + if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many; + return Plural.Other; + case 'gd': + if (n === 1 || n === 11) return Plural.One; + if (n === 2 || n === 12) return Plural.Two; + if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few; + return Plural.Other; + case 'gv': + if (v === 0 && i % 10 === 1) return Plural.One; + if (v === 0 && i % 10 === 2) return Plural.Two; + if (v === 0 && + (i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80)) + return Plural.Few; + if (!(v === 0)) return Plural.Many; + return Plural.Other; + case 'he': + if (i === 1 && v === 0) return Plural.One; + if (i === 2 && v === 0) return Plural.Two; + if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many; + return Plural.Other; + case 'is': + if (t === 0 && i % 10 === 1 && !(i % 100 === 11) || !(t === 0)) return Plural.One; + return Plural.Other; + case 'ksh': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + return Plural.Other; + case 'kw': + case 'naq': + case 'se': + case 'smn': + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + return Plural.Other; + case 'lag': + if (n === 0) return Plural.Zero; + if ((i === 0 || i === 1) && !(n === 0)) return Plural.One; + return Plural.Other; + case 'lt': + if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One; + if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 && + !(n % 100 >= 11 && n % 100 <= 19)) + return Plural.Few; + if (!(f === 0)) return Plural.Many; + return Plural.Other; + case 'lv': + case 'prg': + if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 || + v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19) + return Plural.Zero; + if (n % 10 === 1 && !(n % 100 === 11) || v === 2 && f % 10 === 1 && !(f % 100 === 11) || + !(v === 2) && f % 10 === 1) + return Plural.One; + return Plural.Other; + case 'mk': + if (v === 0 && i % 10 === 1 || f % 10 === 1) return Plural.One; + return Plural.Other; + case 'mt': + if (n === 1) return Plural.One; + if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10) + return Plural.Few; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many; + return Plural.Other; + case 'pl': + if (i === 1 && v === 0) return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14)) + return Plural.Few; + if (v === 0 && !(i === 1) && i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 || + v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || + v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'pt': + if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One; + return Plural.Other; + case 'ro': + if (i === 1 && v === 0) return Plural.One; + if (!(v === 0) || n === 0 || + !(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19) + return Plural.Few; + return Plural.Other; + case 'ru': + case 'uk': + if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14)) + return Plural.Few; + if (v === 0 && i % 10 === 0 || + v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || + v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && i % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'shi': + if (i === 0 || n === 1) return Plural.One; + if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few; + return Plural.Other; + case 'si': + if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One; + return Plural.Other; + case 'sl': + if (v === 0 && i % 100 === 1) return Plural.One; + if (v === 0 && i % 100 === 2) return Plural.Two; + if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0)) + return Plural.Few; + return Plural.Other; + case 'tzm': + if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99) + return Plural.One; + return Plural.Other; + // When there is no specification, the default is always "other" + // Spec: http://cldr.unicode.org/index/cldr-spec/plural-rules + // > other (required—general plural form — also used if the language only has a single form) + default: + return Plural.Other; + } +} + +function getPluralCategory(value: any, locale: string): string { + const plural = getPluralCase(locale, value); + + switch (plural) { + case Plural.Zero: + return 'zero'; + case Plural.One: + return 'one'; + case Plural.Two: + return 'two'; + case Plural.Few: + return 'few'; + case Plural.Many: + return 'many'; + default: + return 'other'; + } +} + +/** + * Returns the index of the current case of an ICU expression depending on the main binding value + * + * @param icuExpression + * @param bindingValue The value of the main binding used by this ICU expression + */ +function getCaseIndex(icuExpression: TIcu, bindingValue: string): number { + let index = icuExpression.cases.indexOf(bindingValue); + if (index === -1) { + switch (icuExpression.type) { + case IcuType.plural: { + // TODO(ocombe): replace this hard-coded value by the real LOCALE_ID value + const locale = 'en-US'; + const resolvedCase = getPluralCategory(bindingValue, locale); + index = icuExpression.cases.indexOf(resolvedCase); + if (index === -1 && resolvedCase !== 'other') { + index = icuExpression.cases.indexOf('other'); } break; + } + case IcuType.select: { + index = icuExpression.cases.indexOf('other'); + break; + } } } + return index; } /** - * Takes a translation string and the initial list of expressions and returns a list of instructions - * that will be used to translate an attribute. - * Even indexes contain static strings, while odd indexes contain the index of the expression whose - * value will be concatenated into the final translation. + * Generate the OpCodes for ICU expressions. + * + * @param tIcus + * @param icuExpression + * @param startIndex + * @param expandoStartIndex */ -export function i18nExpMapping( - translation: string, placeholders: PlaceholderMap): I18nExpInstruction[] { - const staticText: I18nExpInstruction[] = translation.split(i18nTagRegex); - // odd indexes are placeholders - for (let i = 1; i < staticText.length; i += 2) { - staticText[i] = placeholders[staticText[i]]; +function icuStart( + tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number, + expandoStartIndex: number): void { + const createCodes = []; + const removeCodes = []; + const updateCodes = []; + const vars = []; + const childIcus: number[][] = []; + for (let i = 0; i < icuExpression.values.length; i++) { + // Each value is an array of strings & other ICU expressions + const valueArr = icuExpression.values[i]; + const nestedIcus: IcuExpression[] = []; + for (let j = 0; j < valueArr.length; j++) { + const value = valueArr[j]; + if (typeof value !== 'string') { + // It is an nested ICU expression + const icuIndex = nestedIcus.push(value as IcuExpression) - 1; + // Replace nested ICU expression by a comment node + valueArr[j] = ``; + } + } + const icuCase: IcuCase = + parseIcuCase(valueArr.join(''), startIndex, nestedIcus, tIcus, expandoStartIndex); + createCodes.push(icuCase.create); + removeCodes.push(icuCase.remove); + updateCodes.push(icuCase.update); + vars.push(icuCase.vars); + childIcus.push(icuCase.childIcus); + } + const tIcu: TIcu = { + type: icuExpression.type, + vars, + expandoStartIndex: expandoStartIndex + 1, childIcus, + cases: icuExpression.cases, + create: createCodes, + remove: removeCodes, + update: updateCodes + }; + tIcus.push(tIcu); + const lViewData = _getViewData(); + const worstCaseSize = Math.max(...vars); + for (let i = 0; i < worstCaseSize; i++) { + allocExpando(lViewData); } - return staticText; } /** - * Checks if the value of an expression has changed and replaces it by its value in a translation, - * or returns NO_CHANGE. + * Transforms a string template into an HTML template and a list of instructions used to update + * attributes or nodes that contain bindings. * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. + * @param unsafeHtml The string to parse + * @param parentIndex + * @param nestedIcus + * @param tIcus + * @param expandoStartIndex */ -export function i18nInterpolation1(instructions: I18nExpInstruction[], v0: any): string|NO_CHANGE { - const different = bindingUpdated(getViewData()[BINDING_INDEX]++, v0); - - if (!different) { - return NO_CHANGE; +function parseIcuCase( + unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[], + expandoStartIndex: number): IcuCase { + const inertBodyHelper = new InertBodyHelper(document); + const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); + if (!inertBodyElement) { + throw new Error('Unable to generate inert body element'); } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - res += stringify(v0); - } else { - res += instructions[i]; - } - } - - return res; + const wrapper = getTemplateContent(inertBodyElement !) as Element || inertBodyElement; + const opCodes: IcuCase = {vars: 0, childIcus: [], create: [], remove: [], update: []}; + parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex); + return opCodes; } +const NESTED_ICU = /�(\d+)�/; + /** - * Checks if the values of up to 2 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. + * Parses a node, its children and its siblings, and generates the mutate & update OpCodes. * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. + * @param currentNode The first node to parse + * @param icuCase The data for the ICU expression case that contains those nodes + * @param parentIndex Index of the current node's parent + * @param nestedIcus Data for the nested ICU expressions that this case contains + * @param tIcus Data for all ICU expressions of the current message + * @param expandoStartIndex Expando start index for the current ICU expression */ -export function i18nInterpolation2(instructions: I18nExpInstruction[], v0: any, v1: any): string| - NO_CHANGE { - const viewData = getViewData(); - const different = bindingUpdated2(viewData[BINDING_INDEX], v0, v1); - viewData[BINDING_INDEX] += 2; +function parseNodes( + currentNode: Node | null, icuCase: IcuCase, parentIndex: number, nestedIcus: IcuExpression[], + tIcus: TIcu[], expandoStartIndex: number) { + if (currentNode) { + const nestedIcusToCreate: [IcuExpression, number][] = []; + while (currentNode) { + const nextNode: Node|null = currentNode.nextSibling; + const newIndex = expandoStartIndex + ++icuCase.vars; + switch (currentNode.nodeType) { + case Node.ELEMENT_NODE: + const element = currentNode as Element; + const tagName = element.tagName.toLowerCase(); + if (!VALID_ELEMENTS.hasOwnProperty(tagName)) { + // This isn't a valid element, we won't create an element for it + icuCase.vars--; + } else { + icuCase.create.push( + ELEMENT_MARKER, tagName, + parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); + const elAttrs = element.attributes; + for (let i = 0; i < elAttrs.length; i++) { + const attr = elAttrs.item(i) !; + const lowerAttrName = attr.name.toLowerCase(); + const hasBinding = !!attr.value.match(BINDING_REGEXP); + // we assume the input string is safe, unless it's using a binding + if (hasBinding) { + if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) { + if (URI_ATTRS[lowerAttrName]) { + addAllToArray( + generateBindingUpdateOpCodes(attr.value, newIndex, attr.name, _sanitizeUrl), + icuCase.update); + } else if (SRCSET_ATTRS[lowerAttrName]) { + addAllToArray( + generateBindingUpdateOpCodes( + attr.value, newIndex, attr.name, sanitizeSrcset), + icuCase.update); + } else { + addAllToArray( + generateBindingUpdateOpCodes(attr.value, newIndex, attr.name), + icuCase.update); + } + } else { + ngDevMode && + console.warn( + `WARNING: ignoring unsafe attribute value ${lowerAttrName} on element ${tagName} (see http://g.co/ng/security#xss)`); + } + } else { + icuCase.create.push( + newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, attr.name, + attr.value); + } + } + // Parse the children of this node (if any) + parseNodes( + currentNode.firstChild, icuCase, newIndex, nestedIcus, tIcus, expandoStartIndex); + // Remove the parent node after the children + icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove); + } + break; + case Node.TEXT_NODE: + const value = currentNode.textContent || ''; + const hasBinding = value.match(BINDING_REGEXP); + icuCase.create.push( + hasBinding ? '' : value, + parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); + icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove); + if (hasBinding) { + addAllToArray(generateBindingUpdateOpCodes(value, newIndex), icuCase.update); + } + break; + case Node.COMMENT_NODE: + // Check if the comment node is a placeholder for a nested ICU + const match = NESTED_ICU.exec(currentNode.textContent || ''); + if (match) { + const nestedIcuIndex = parseInt(match[1], 10); + const newLocal = ngDevMode ? `nested ICU ${nestedIcuIndex}` : ''; + // Create the comment node that will anchor the ICU expression + icuCase.create.push( + COMMENT_MARKER, newLocal, + parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild); + const nestedIcu = nestedIcus[nestedIcuIndex]; + nestedIcusToCreate.push([nestedIcu, newIndex]); + } else { + // We do not handle any other type of comment + icuCase.vars--; + } + break; + default: + // We do not handle any other type of element + icuCase.vars--; + } + currentNode = nextNode !; + } - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = b1 ? v1 : v0; - - res += stringify(value); - } else { - res += instructions[i]; + for (let i = 0; i < nestedIcusToCreate.length; i++) { + const nestedIcu = nestedIcusToCreate[i][0]; + const nestedIcuNodeIndex = nestedIcusToCreate[i][1]; + icuStart(tIcus, nestedIcu, nestedIcuNodeIndex, expandoStartIndex + icuCase.vars); + // Since this is recursive, the last TIcu that was pushed is the one we want + const nestTIcuIndex = tIcus.length - 1; + icuCase.vars += Math.max(...tIcus[nestTIcuIndex].vars); + icuCase.childIcus.push(nestTIcuIndex); + const mask = getBindingMask(nestedIcu); + icuCase.update.push( + toMaskBit(nestedIcu.mainBinding), // mask of the main binding + 3, // skip 3 opCodes if not changed + -1 - nestedIcu.mainBinding, + nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, + nestTIcuIndex, + mask, // mask of all the bindings of this ICU expression + 2, // skip 2 opCodes if not changed + nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, + nestTIcuIndex); + icuCase.remove.push( + nestTIcuIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu, + nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove); } } - - return res; } +const RAW_ICU_REGEXP = /{\s*(\S*)\s*,\s*\S{6}\s*,[\s\S]*}/gi; + /** - * Checks if the values of up to 3 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. + * Replaces the variable parameter (main binding) of an ICU by a given value. * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * @param v2 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. + * Example: + * ``` + * const MSG_APP_1_RAW = "{VAR_SELECT, select, male {male} female {female} other {other}}"; + * const MSG_APP_1 = i18nIcuReplaceVars(MSG_APP_1_RAW, { VAR_SELECT: "�0�" }); + * // --> MSG_APP_1 = "{�0�, select, male {male} female {female} other {other}}" + * ``` */ -export function i18nInterpolation3( - instructions: I18nExpInstruction[], v0: any, v1: any, v2: any): string|NO_CHANGE { - const viewData = getViewData(); - const different = bindingUpdated3(viewData[BINDING_INDEX], v0, v1, v2); - viewData[BINDING_INDEX] += 3; - - if (!different) { - return NO_CHANGE; +export function i18nIcuReplaceVars(message: string, replacements: {[key: string]: string}): string { + const keys = Object.keys(replacements); + function replaceFn(replacement: string) { + return (str: string, varMatch: string) => { return str.replace(varMatch, replacement); }; } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b2 = idx & 2; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = b2 ? v2 : (b1 ? v1 : v0); - - res += stringify(value); - } else { - res += instructions[i]; - } + for (let i = 0; i < keys.length; i++) { + message = message.replace(RAW_ICU_REGEXP, replaceFn(replacements[keys[i]])); } - - return res; -} - -/** - * Checks if the values of up to 4 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. - * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * @param v2 value checked for change. - * @param v3 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - */ -export function i18nInterpolation4( - instructions: I18nExpInstruction[], v0: any, v1: any, v2: any, v3: any): string|NO_CHANGE { - const viewData = getViewData(); - const different = bindingUpdated4(viewData[BINDING_INDEX], v0, v1, v2, v3); - viewData[BINDING_INDEX] += 4; - - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b2 = idx & 2; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = b2 ? (b1 ? v3 : v2) : (b1 ? v1 : v0); - - res += stringify(value); - } else { - res += instructions[i]; - } - } - - return res; -} - -/** - * Checks if the values of up to 5 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. - * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * @param v2 value checked for change. - * @param v3 value checked for change. - * @param v4 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - */ -export function i18nInterpolation5( - instructions: I18nExpInstruction[], v0: any, v1: any, v2: any, v3: any, v4: any): string| - NO_CHANGE { - const viewData = getViewData(); - let different = bindingUpdated4(viewData[BINDING_INDEX], v0, v1, v2, v3); - different = bindingUpdated(viewData[BINDING_INDEX] + 4, v4) || different; - viewData[BINDING_INDEX] += 5; - - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b4 = idx & 4; - const b2 = idx & 2; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = b4 ? v4 : (b2 ? (b1 ? v3 : v2) : (b1 ? v1 : v0)); - - res += stringify(value); - } else { - res += instructions[i]; - } - } - - return res; -} - -/** - * Checks if the values of up to 6 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. - * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * @param v2 value checked for change. - * @param v3 value checked for change. - * @param v4 value checked for change. - * @param v5 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - */ export function -i18nInterpolation6( - instructions: I18nExpInstruction[], v0: any, v1: any, v2: any, v3: any, v4: any, v5: any): - string|NO_CHANGE { - const viewData = getViewData(); - let different = bindingUpdated4(viewData[BINDING_INDEX], v0, v1, v2, v3); - different = bindingUpdated2(viewData[BINDING_INDEX] + 4, v4, v5) || different; - viewData[BINDING_INDEX] += 6; - - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b4 = idx & 4; - const b2 = idx & 2; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = b4 ? (b1 ? v5 : v4) : (b2 ? (b1 ? v3 : v2) : (b1 ? v1 : v0)); - - res += stringify(value); - } else { - res += instructions[i]; - } - } - - return res; -} - -/** - * Checks if the values of up to 7 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. - * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * @param v2 value checked for change. - * @param v3 value checked for change. - * @param v4 value checked for change. - * @param v5 value checked for change. - * @param v6 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - */ -export function i18nInterpolation7( - instructions: I18nExpInstruction[], v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, - v6: any): string|NO_CHANGE { - const viewData = getViewData(); - let different = bindingUpdated4(viewData[BINDING_INDEX], v0, v1, v2, v3); - different = bindingUpdated3(viewData[BINDING_INDEX] + 4, v4, v5, v6) || different; - viewData[BINDING_INDEX] += 7; - - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b4 = idx & 4; - const b2 = idx & 2; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = b4 ? (b2 ? v6 : (b1 ? v5 : v4)) : (b2 ? (b1 ? v3 : v2) : (b1 ? v1 : v0)); - - res += stringify(value); - } else { - res += instructions[i]; - } - } - - return res; -} - -/** - * Checks if the values of up to 8 expressions have changed and replaces them by their values in a - * translation, or returns NO_CHANGE. - * - * @param instructions A list of instructions that will be used to translate an attribute. - * @param v0 value checked for change. - * @param v1 value checked for change. - * @param v2 value checked for change. - * @param v3 value checked for change. - * @param v4 value checked for change. - * @param v5 value checked for change. - * @param v6 value checked for change. - * @param v7 value checked for change. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - */ -export function i18nInterpolation8( - instructions: I18nExpInstruction[], v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, - v6: any, v7: any): string|NO_CHANGE { - const viewData = getViewData(); - let different = bindingUpdated4(viewData[BINDING_INDEX], v0, v1, v2, v3); - different = bindingUpdated4(viewData[BINDING_INDEX] + 4, v4, v5, v6, v7) || different; - viewData[BINDING_INDEX] += 8; - - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are bindings - if (i & 1) { - // Extract bits - const idx = instructions[i] as number; - const b4 = idx & 4; - const b2 = idx & 2; - const b1 = idx & 1; - // Get the value from the argument vx where x = idx - const value = - b4 ? (b2 ? (b1 ? v7 : v6) : (b1 ? v5 : v4)) : (b2 ? (b1 ? v3 : v2) : (b1 ? v1 : v0)); - - res += stringify(value); - } else { - res += instructions[i]; - } - } - - return res; -} - -/** - * Create a translated interpolation binding with a variable number of expressions. - * - * If there are 1 to 8 expressions then `i18nInterpolation()` should be used instead. It is faster - * because there is no need to create an array of expressions and iterate over it. - * - * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. - */ -export function i18nInterpolationV(instructions: I18nExpInstruction[], values: any[]): string| - NO_CHANGE { - const viewData = getViewData(); - let different = false; - for (let i = 0; i < values.length; i++) { - // Check if bindings have changed - bindingUpdated(viewData[BINDING_INDEX]++, values[i]) && (different = true); - } - - if (!different) { - return NO_CHANGE; - } - - let res = ''; - for (let i = 0; i < instructions.length; i++) { - // Odd indexes are placeholders - if (i & 1) { - res += stringify(values[instructions[i] as number]); - } else { - res += instructions[i]; - } - } - - return res; + return message; } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index c441025c40..db46134731 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -87,24 +87,12 @@ export { } from './state'; export { - i18nAttribute, + i18nAttributes, i18nExp, i18nStart, i18nEnd, i18nApply, - i18nMapping, - i18nInterpolation1, - i18nInterpolation2, - i18nInterpolation3, - i18nInterpolation4, - i18nInterpolation5, - i18nInterpolation6, - i18nInterpolation7, - i18nInterpolation8, - i18nInterpolationV, - i18nExpMapping, - I18nInstruction, - I18nExpInstruction + i18nIcuReplaceVars, } from './i18n'; export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 91eea29ae0..c4b34fab27 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -7,7 +7,6 @@ */ import './ng_dev_mode'; - import {resolveForwardRef} from '../di/forward_ref'; import {InjectionToken} from '../di/injection_token'; import {InjectFlags} from '../di/injector_compatibility'; @@ -16,7 +15,6 @@ import {Sanitizer} from '../sanitization/security'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {Type} from '../type'; import {noop} from '../util/noop'; - import {assertDefined, assertEqual, assertLessThan, assertNotEqual} from './assert'; import {attachPatchData, getComponentViewByInstance} from './context_discovery'; import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di'; @@ -25,11 +23,12 @@ import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} fro import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, InitialStylingFlags, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {INJECTOR_SIZE, NodeInjectorFactory} from './interfaces/injector'; -import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; +import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; import {PlayerFactory} from './interfaces/player'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; +import {SanitizerFn} from './interfaces/sanitization'; import {StylingIndex} from './interfaces/styling'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LViewData, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; @@ -42,7 +41,6 @@ import {getStylingContext} from './styling/util'; import {NO_CHANGE} from './tokens'; import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isDifferent, loadInternal, readPatchedLViewData, stringify} from './util'; - /** * A permanent marker promise which signifies that the current CD tree is * clean. @@ -54,11 +52,6 @@ const enum BindingDirection { Output, } -/** - * Function used to sanitize the value before writing it into the renderer. - */ -type SanitizerFn = (value: any) => string; - /** * Refreshes the view, executing the following steps in that order: * triggers init hooks, refreshes dynamic embedded views, triggers content hooks, sets host @@ -197,9 +190,13 @@ export function createNodeAtIndex( export function createNodeAtIndex( index: number, type: TNodeType.ElementContainer, native: RComment, name: null, attrs: TAttributes | null): TElementContainerNode; +export function createNodeAtIndex( + index: number, type: TNodeType.IcuContainer, native: RComment, name: null, + attrs: TAttributes | null): TElementContainerNode; export function createNodeAtIndex( index: number, type: TNodeType, native: RText | RElement | RComment | null, name: string | null, - attrs: TAttributes | null): TElementNode&TContainerNode&TElementContainerNode&TProjectionNode { + attrs: TAttributes | null): TElementNode&TContainerNode&TElementContainerNode&TProjectionNode& + TIcuContainerNode { const viewData = getViewData(); const tView = getTView(); const adjustedIndex = index + HEADER_OFFSET; @@ -233,7 +230,7 @@ export function createNodeAtIndex( setPreviousOrParentTNode(tNode); setIsParent(true); return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode & - TProjectionNode; + TProjectionNode & TIcuContainerNode; } export function createViewNode(index: number, view: LViewData) { @@ -255,11 +252,12 @@ export function createViewNode(index: number, view: LViewData) { * i18nApply() or ComponentFactory.create), we need to adjust the blueprint for future * template passes. */ -export function adjustBlueprintForNewNode(view: LViewData) { +export function allocExpando(view: LViewData) { const tView = view[TVIEW]; if (tView.firstTemplatePass) { tView.expandoStartIndex++; tView.blueprint.push(null); + tView.data.push(null); view.push(null); } } @@ -914,7 +912,7 @@ export function elementEnd(): void { * @param sanitizer An optional function used to sanitize the value. */ export function elementAttribute( - index: number, name: string, value: any, sanitizer?: SanitizerFn): void { + index: number, name: string, value: any, sanitizer?: SanitizerFn | null): void { if (value !== NO_CHANGE) { const viewData = getViewData(); const renderer = getRenderer(); @@ -947,7 +945,7 @@ export function elementAttribute( */ export function elementProperty( - index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): void { + index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null): void { if (value === NO_CHANGE) return; const viewData = getViewData(); const element = getNativeByIndex(index, viewData) as RElement | RComment; diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts index 2c39b244ff..f50b9a4c4e 100644 --- a/packages/core/src/render3/interfaces/i18n.ts +++ b/packages/core/src/render3/interfaces/i18n.ts @@ -16,24 +16,32 @@ * * See: `I18nCreateOpCodes` for example of usage. */ +import {SanitizerFn} from './sanitization'; + export const enum I18nMutateOpCode { - /// Stores shift amount for bits 17-2 that contain reference index. - SHIFT_REF = 2, + /// Stores shift amount for bits 17-3 that contain reference index. + SHIFT_REF = 3, /// Stores shift amount for bits 31-17 that contain parent index. SHIFT_PARENT = 17, /// Mask for OpCode - MASK_OPCODE = 0b11, + MASK_OPCODE = 0b111, /// Mask for reference index. MASK_REF = ((2 ^ 16) - 1) << SHIFT_REF, /// OpCode to select a node. (next OpCode will contain the operation.) - Select = 0b00, + Select = 0b000, /// OpCode to append the current node to `PARENT`. - AppendChild = 0b01, + AppendChild = 0b001, /// OpCode to insert the current node to `PARENT` before `REF`. - InsertBefore = 0b10, + InsertBefore = 0b010, /// OpCode to remove the `REF` node from `PARENT`. - Remove = 0b11, + Remove = 0b011, + /// OpCode to set the attribute of a node. + Attr = 0b100, + /// OpCode to simulate elementEnd() + ElementEnd = 0b101, + /// OpCode to read the remove OpCodes for the nested ICU + RemoveNestedIcu = 0b110, } /** @@ -114,8 +122,8 @@ export interface COMMENT_MARKER { marker: 'comment'; } * // For removing existing nodes * // -------------------------------------------------- * // const node = lViewData[1]; - * // lViewData[2].remove(node); - * 2 << SHIFT_PARENT | 1 << SHIFT_REF | Remove, + * // removeChild(tView.data(1), node, lViewData); + * 1 << SHIFT_REF | Remove, * * // For writing attributes * // -------------------------------------------------- @@ -178,7 +186,7 @@ export const enum I18nUpdateOpCode { * } * ``` * We can assume that each call to `i18nExp` sets an internal `changeMask` bit depending on the - * index of `i18nExp` index. + * index of `i18nExp`. * * OpCodes * ``` @@ -222,7 +230,7 @@ export const enum I18nUpdateOpCode { * ``` * */ -export interface I18nUpdateOpCodes extends Array string | null)> {} +export interface I18nUpdateOpCodes extends Array {} /** * Store information for the i18n translation block. @@ -355,10 +363,6 @@ export interface TIcu { update: I18nUpdateOpCodes[]; } -/** - * Stores currently selected case in each ICU. - * - * For each ICU in translation, the `Li18n` stores the currently selected case for the current - * `LView`. For perf reasons this array is only created if a translation block has an ICU. - */ -export interface LI18n extends Array {} +// Note: This hack is necessary so we don't erroneously get a circular dependency +// failure based on types. +export const unusedValueExportToPlacateAjd = 1; diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index c190e6cb54..5dcb059148 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -21,6 +21,7 @@ export const enum TNodeType { Element = 0b011, ViewOrElement = 0b010, ElementContainer = 0b100, + IcuContainer = 0b101, } /** @@ -255,7 +256,7 @@ export interface TNode { parent: TElementNode|TContainerNode|null; /** - * If this node is part of an i18n block, it indicates whether this container is part of the DOM + * If this node is part of an i18n block, it indicates whether this node is part of the DOM. * If this node is not part of an i18n block, this field is null. */ detached: boolean|null; @@ -358,7 +359,6 @@ export interface TContainerNode extends TNode { projection: null; } - /** Static data for an */ export interface TElementContainerNode extends TNode { /** Index in the LViewData[] array. */ @@ -369,6 +369,21 @@ export interface TElementContainerNode extends TNode { projection: null; } +/** Static data for an ICU expression */ +export interface TIcuContainerNode extends TNode { + /** Index in the LViewData[] array. */ + index: number; + child: TElementNode|TTextNode|null; + parent: TElementNode|TElementContainerNode|null; + tViews: null; + projection: null; + /** + * Indicates the current active case for an ICU expression. + * It is null when there is no active case. + */ + activeCaseIndex: number|null; +} + /** Static data for a view */ export interface TViewNode extends TNode { /** If -1, it's a dynamically created view. Otherwise, it is the view block ID. */ diff --git a/packages/core/src/render3/interfaces/sanitization.ts b/packages/core/src/render3/interfaces/sanitization.ts new file mode 100644 index 0000000000..24970f93c9 --- /dev/null +++ b/packages/core/src/render3/interfaces/sanitization.ts @@ -0,0 +1,12 @@ +/** + * @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 + */ + +/** + * Function used to sanitize the value before writing it into the renderer. + */ +export type SanitizerFn = (value: any) => string; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 61a4c00e8d..1b2a6162ed 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -11,9 +11,9 @@ import {Injector} from '../../di/injector'; import {QueryList} from '../../linker'; import {Sanitizer} from '../../sanitization/security'; import {Type} from '../../type'; - import {LContainer} from './container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList} from './definition'; +import {I18nUpdateOpCodes, TI18n} from './i18n'; import {TElementNode, TNode, TViewNode} from './node'; import {PlayerHandler} from './player'; import {LQueries} from './query'; @@ -297,7 +297,7 @@ export interface TView { /** Whether or not this template has been processed. */ firstTemplatePass: boolean; - /** Static data equivalent of LView.data[]. Contains TNodes. */ + /** Static data equivalent of LView.data[]. Contains TNodes, PipeDefInternal or TI18n. */ data: TData; /** @@ -535,7 +535,7 @@ export type HookData = (number | (() => void))[]; */ export type TData = (TNode | PipeDef| DirectiveDef| ComponentDef| number | Type| - InjectionToken| null)[]; + InjectionToken| TI18n | I18nUpdateOpCodes | null)[]; // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index ec6ff92af4..006020cf9f 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -97,7 +97,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'ɵtextBinding': r3.textBinding, 'ɵembeddedViewStart': r3.embeddedViewStart, 'ɵembeddedViewEnd': r3.embeddedViewEnd, - 'ɵi18nAttribute': r3.i18nAttribute, + 'ɵi18nAttributes': r3.i18nAttributes, 'ɵi18nExp': r3.i18nExp, 'ɵi18nStart': r3.i18nStart, 'ɵi18nEnd': r3.i18nEnd, diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index ceb11abd7a..8637c107fc 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -21,8 +21,23 @@ const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; /** Retrieves the parent element of a given node. */ export function getParentNative(tNode: TNode, currentView: LViewData): RElement|RComment|null { - return tNode.parent == null ? getHostNative(currentView) : - getNativeByTNode(tNode.parent, currentView); + if (tNode.parent == null) { + return getHostNative(currentView); + } else { + const parentTNode = getFirstParentNative(tNode); + return getNativeByTNode(parentTNode, currentView); + } +} + +/** + * Get the first parent of a node that isn't an IcuContainer TNode + */ +function getFirstParentNative(tNode: TNode): TNode { + let parent = tNode.parent; + while (parent && parent.type === TNodeType.IcuContainer) { + parent = parent.parent; + } + return parent !; } /** @@ -578,17 +593,22 @@ function canInsertNativeChildOfView(viewTNode: TViewNode, view: LViewData): bool * * - * @param parent The parent where the child will be inserted into. + * @param tNode The tNode of the node that we want to insert. * @param currentView Current LView being processed. - * @return boolean Whether the child should be inserted now (or delayed until later). + * @return boolean Whether the node should be inserted now (or delayed until later). */ export function canInsertNativeNode(tNode: TNode, currentView: LViewData): boolean { let currentNode = tNode; let parent: TNode|null = tNode.parent; - if (tNode.parent && tNode.parent.type === TNodeType.ElementContainer) { - currentNode = getHighestElementContainer(tNode); - parent = currentNode.parent; + if (tNode.parent) { + if (tNode.parent.type === TNodeType.ElementContainer) { + currentNode = getHighestElementContainer(tNode); + parent = currentNode.parent; + } else if (tNode.parent.type === TNodeType.IcuContainer) { + currentNode = getFirstParentNative(currentNode); + parent = currentNode.parent; + } } if (parent === null) parent = currentView[HOST_NODE]; @@ -639,7 +659,7 @@ export function nativeNextSibling(renderer: Renderer3, node: RNode): RNode|null * @returns Whether or not the child was appended */ export function appendChild( - childEl: RNode | null, childTNode: TNode, currentView: LViewData): boolean { + childEl: RNode | null = null, childTNode: TNode, currentView: LViewData): boolean { if (childEl !== null && canInsertNativeNode(childTNode, currentView)) { const renderer = currentView[RENDERER]; const parentEl = getParentNative(childTNode, currentView); @@ -655,6 +675,9 @@ export function appendChild( } else if (parentTNode.type === TNodeType.ElementContainer) { const renderParent = getRenderParent(childTNode, currentView) !; nativeInsertBefore(renderer, renderParent, childEl, parentEl); + } else if (parentTNode.type === TNodeType.IcuContainer) { + const icuAnchorNode = getNativeByTNode(childTNode.parent !, currentView) !as RElement; + nativeInsertBefore(renderer, parentEl as RElement, childEl, icuAnchorNode); } else { isProceduralRenderer(renderer) ? renderer.appendChild(parentEl !as RElement, childEl) : parentEl !.appendChild(childEl); diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index baa02dfac7..310e041c04 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -157,6 +157,10 @@ export function getCurrentView(): OpaqueViewState { return viewData as any as OpaqueViewState; } +export function _getViewData(): LViewData { + return viewData; +} + /** * Restores `contextViewData` to the given OpaqueViewState instance. * diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 2ae48084dc..4af3f93426 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -252,4 +252,16 @@ export function getParentInjectorTNode( export const defaultScheduler = (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame || // browser only setTimeout // everything else - ).bind(global); \ No newline at end of file + ).bind(global); + +/** + * Equivalent to ES6 spread, add each item to an array. + * + * @param items The items to add + * @param arr The array to which you want to add the items + */ +export function addAllToArray(items: any[], arr: any[]) { + for (let i = 0; i < items.length; i++) { + arr.push(items[i]); + } +} diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts index 6dd295f135..4b69587fb4 100644 --- a/packages/core/src/sanitization/html_sanitizer.ts +++ b/packages/core/src/sanitization/html_sanitizer.ts @@ -57,14 +57,14 @@ const INLINE_ELEMENTS = merge( 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,' + 'samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video')); -const VALID_ELEMENTS = +export const VALID_ELEMENTS = merge(VOID_ELEMENTS, BLOCK_ELEMENTS, INLINE_ELEMENTS, OPTIONAL_END_TAG_ELEMENTS); // Attributes that have href and hence need to be sanitized -const URI_ATTRS = tagSet('background,cite,href,itemtype,longdesc,poster,src,xlink:href'); +export const URI_ATTRS = tagSet('background,cite,href,itemtype,longdesc,poster,src,xlink:href'); // Attributes that have special href set hence need to be sanitized -const SRCSET_ATTRS = tagSet('srcset'); +export const SRCSET_ATTRS = tagSet('srcset'); const HTML_ATTRS = tagSet( 'abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,' + @@ -81,7 +81,7 @@ const HTML_ATTRS = tagSet( // can be sanitized, but they increase security surface area without a legitimate use case, so they // are left out here. -const VALID_ATTRS = merge(URI_ATTRS, SRCSET_ATTRS, HTML_ATTRS); +export const VALID_ATTRS = merge(URI_ATTRS, SRCSET_ATTRS, HTML_ATTRS); /** * SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe @@ -265,7 +265,7 @@ export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string } } -function getTemplateContent(el: Node): Node|null { +export function getTemplateContent(el: Node): Node|null { return 'content' in (el as any /** Microsoft/TypeScript#21517 */) && isTemplateElement(el) ? el.content : null; diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index 01ee1e9f0a..ec84f12fa5 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -626,6 +626,9 @@ { "name": "getElementDepthCount" }, + { + "name": "getFirstParentNative" + }, { "name": "getFirstTemplatePass" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 4874171e6b..57a5183818 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -260,6 +260,9 @@ { "name": "getDirectiveDef" }, + { + "name": "getFirstParentNative" + }, { "name": "getFirstTemplatePass" }, diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index d5bba26b9a..f66d9efc0d 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -854,6 +854,9 @@ { "name": "getErrorLogger" }, + { + "name": "getFirstParentNative" + }, { "name": "getFirstTemplatePass" }, @@ -1113,7 +1116,7 @@ "name": "markViewDirty" }, { - "name": "merge" + "name": "merge$1" }, { "name": "mergeAll" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8857a5ca90..6a2f61b698 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -668,6 +668,9 @@ { "name": "getElementDepthCount" }, + { + "name": "getFirstParentNative" + }, { "name": "getFirstTemplatePass" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index e5630d71fc..d5a236d5fb 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -681,7 +681,7 @@ "name": "PlatformRef" }, { - "name": "Plural" + "name": "Plural$1" }, { "name": "QUERIES" @@ -1697,6 +1697,9 @@ { "name": "getErrorLogger" }, + { + "name": "getFirstParentNative" + }, { "name": "getFirstTemplatePass" }, @@ -1860,7 +1863,7 @@ "name": "getPlayerContext" }, { - "name": "getPluralCategory" + "name": "getPluralCategory$1" }, { "name": "getPointers" diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index 610911c96d..cc2bcb9f83 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -6,13 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgForOfContext} from '@angular/common'; +import {noop} from '../../../compiler/src/render3/view/util'; import {Component as _Component} from '../../src/core'; import {defineComponent} from '../../src/render3/definition'; -import {I18nExpInstruction, I18nInstruction, i18nApply, i18nExpMapping, i18nInterpolation1, i18nInterpolation2, i18nInterpolation3, i18nInterpolation4, i18nInterpolation5, i18nInterpolation6, i18nInterpolation7, i18nInterpolation8, i18nInterpolationV, i18nMapping} from '../../src/render3/i18n'; -import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, nextContext, projection, projectionDef, template, text, textBinding} from '../../src/render3/instructions'; +import {getTranslationForTemplate, i18n, i18nApply, i18nAttributes, i18nEnd, i18nExp, i18nIcuReplaceVars, i18nStart} from '../../src/render3/i18n'; import {RenderFlags} from '../../src/render3/interfaces/definition'; -import {NgForOf} from './common_with_def'; +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 { @@ -21,1786 +24,1486 @@ const Component: typeof _Component = function(...args: any[]): any { } as any; describe('Runtime i18n', () => { - it('should support html elements', () => { - // Html tags are replaced by placeholders. - // Open tag placeholders are never re-used (closing tag placeholders can be). - const MSG_DIV_SECTION_1 = - `{$START_C}trad 1{$END_C}{$START_A}trad 2{$START_B}trad 3{$END_B}{$END_A}`; - let i18n_1: I18nInstruction[][]; - // Initial template: - //
- // - // - // - // - // - //
+ describe('getTranslationForTemplate', () => { + it('should crop messages for the selected template', () => { + let message = `simple text`; + expect(getTranslationForTemplate(message)).toEqual(message); - // Translated to: - //
- // trad 1 - // - // trad 2 - // trad 3 - // - //
- function createTemplate() { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, [{'START_A': 1, 'START_B': 2, 'START_REMOVE_ME': 3, 'START_C': 4}]); - } + message = `Hello �0�!`; + expect(getTranslationForTemplate(message)).toEqual(message); - elementStart(0, 'div'); - { // Start of translated section 1 - // - i18n sections do not contain any text() instruction - elementStart(1, 'a'); // START_A + 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/); + }); + }); + + describe('i18nIcuReplaceVars', () => { + it('should replace var names', () => { + const MSG_APP_1_RAW = '{VAR_SELECT, select, male {male} female {female} other {other}}'; + const MSG_APP_1 = i18nIcuReplaceVars(MSG_APP_1_RAW, {VAR_SELECT: '\uFFFD0\uFFFD'}); + expect(MSG_APP_1).toEqual('{�0�, select, male {male} female {female} other {other}}'); + }); + }); + + 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'); { - element(2, 'b'); // START_B - element(3, 'remove-me'); // START_REMOVE_ME + i18nStart(1, MSG_DIV); + { + element(2, 'div'); // Will be removed + element(3, 'span'); + } + i18nEnd(); } elementEnd(); - element(4, 'c'); // START_C - } // End of translated section 1 - elementEnd(); - i18nApply(1, i18n_1[0]); - } + elementStart(4, 'div'); + { text(5, '!'); } + elementEnd(); + }, null, 6); - const fixture = new TemplateFixture(createTemplate, () => {}, 5); - expect(fixture.html).toEqual(''); - }); + expect(fixture.html).toEqual('
Hello world
!
'); + }); - it('should support expressions', () => { - const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; - let i18n_1: I18nInstruction[][]; + 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�!`; - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 3, - vars: 2, - // Initial template: - //
- // {{exp1}} {{exp2}} - //
- - // Translated to: - //
- // start {{exp2}} middle {{exp1}} end - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 1, 'EXP_2': 2}]); - } - - elementStart(0, 'div'); - { - // Start of translated section 1 - // One text node is added per expression in the interpolation - text(1); // EXP_1 - text(2); // EXP_2 - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - textBinding(1, bind(ctx.exp1)); - textBinding(2, bind(ctx.exp2)); - } + 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(); } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
start 2 middle 1 end
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
start 2 middle 1 end
'); - - // Change the expressions - fixture.component.exp1 = 'expr 1'; - fixture.component.exp2 = 'expr 2'; - fixture.update(); - expect(fixture.html).toEqual('
start expr 2 middle expr 1 end
'); - }); - - it('should support expressions on removed nodes', () => { - const MSG_DIV_SECTION_1 = `message`; - let i18n_1: I18nInstruction[][]; - - class MyApp { - exp1 = '1'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - consts: 2, - vars: 1, - selectors: [['my-app']], - // Initial template: - //
- // {{exp1}} - //
- - // Translated to: - //
- // message - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 1}]); - } - - elementStart(0, 'div'); - { - // Start of translated section 1 - text(1); // EXP_1 will be removed - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - textBinding(1, bind(ctx.exp1)); - } + if (rf & RenderFlags.Update) { + elementProperty(2, 'ngIf', bind(true)); } - }); - } + } - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
message
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
message
'); - - // Change the expressions - fixture.component.exp1 = 'expr 1'; - fixture.update(); - expect(fixture.html).toEqual('
message
'); - }); - - it('should support expressions in attributes', () => { - const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; - const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1}); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 2, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty(0, 'title', i18nInterpolation2(i18n_1, ctx.exp1, ctx.exp2)); - } + function subTemplate_2(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_DIV, 2); + element(1, 'span'); + i18nEnd(); } - }); - } + } - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
'); - - // Change the expressions - fixture.component.exp1 = function test() {}; - fixture.component.exp2 = null; - fixture.update(); - expect(fixture.html).toEqual('
'); - }); - - it('should support both html elements, expressions and expressions in attributes', () => { - const MSG_DIV_SECTION_1 = `{$EXP_1} {$START_P}trad {$EXP_2}{$END_P}`; - const MSG_ATTR_1 = `start {$EXP_2} middle {$EXP_1} end`; - let i18n_1: I18nInstruction[][]; - let i18n_2: I18nExpInstruction[]; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - exp3 = '3'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 8, - vars: 4, - // Initial template: - //
- // {{exp1}} - // - // - // - // - //

- // {{exp2}} - //

- // {{exp3}} - //
- - // Translated to: - //
- // {{exp1}} - //

- // trad {{exp2}} - //

- //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, [{ - 'START_REMOVE_ME_1': 2, - 'START_REMOVE_ME_2': 3, - 'START_REMOVE_ME_3': 4, - 'START_P': 5 - }], - [{'EXP_1': 1, 'EXP_2': 6, 'EXP_3': 7}]); - } - if (!i18n_2) { - i18n_2 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0, 'EXP_2': 1}); - } - - elementStart(0, 'div'); - { - // Start of translated section 1 - text(1); // EXP_1 - elementStart(2, 'remove-me-1'); // START_REMOVE_ME_1 - { - element(3, 'remove-me-2'); // START_REMOVE_ME_2 - element(4, 'remove-me-3'); // START_REMOVE_ME_3 - } - elementEnd(); - elementStart(5, 'p'); // START_P - { text(6); } // EXP_2 - elementEnd(); - text(7); // EXP_3 - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - textBinding(1, bind(ctx.exp1)); - textBinding(6, bind(ctx.exp2)); - textBinding(7, bind(ctx.exp3)); - elementProperty(0, 'title', i18nInterpolation2(i18n_2, ctx.exp1, ctx.exp2)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
1

trad 2

'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
1

trad 2

'); - - // Change the expressions - fixture.component.exp1 = 'expr 1'; - fixture.component.exp2 = 'expr 2'; - fixture.update(); - expect(fixture.html) - .toEqual('
expr 1

trad expr 2

'); - }); - - it('should support multiple i18n elements', () => { - const MSG_DIV_SECTION_1 = `trad {$EXP_1}`; - const MSG_DIV_SECTION_2 = `{$START_C}trad{$END_C}`; - const MSG_ATTR_1 = `start {$EXP_2} middle {$EXP_1} end`; - let i18n_1: I18nInstruction[][]; - let i18n_2: I18nInstruction[][]; - let i18n_3: I18nExpInstruction[]; - - class MyApp { - exp1 = '1'; - exp2 = '2'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 6, - vars: 2, - // Initial template: - //
- // - // {{exp1}} - // - // hello - // - // - // - //
- - // Translated to: - //
- // - // trad {{exp1}} - // - // hello - // - // trad - // - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 2}]); - } - if (!i18n_2) { - i18n_2 = i18nMapping(MSG_DIV_SECTION_2, [{'START_C': 5}]); - } - if (!i18n_3) { - i18n_3 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0, 'EXP_2': 1}); - } - - elementStart(0, 'div'); - { - elementStart(1, 'a'); - { - // Start of translated section 1 - text(2); // EXP_1 - // End of translated section 1 - } - elementEnd(); - text(3, 'hello'); - elementStart(4, 'b'); - { - // Start of translated section 2 - element(5, 'c'); // START_C - // End of translated section 2 - } + 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(); } - elementEnd(); - i18nApply(2, i18n_1[0]); - i18nApply(5, i18n_2[0]); + if (rf & RenderFlags.Update) { + elementProperty(2, 'ngIf', true); + } } - if (rf & RenderFlags.Update) { - textBinding(2, bind(ctx.exp1)); - elementProperty(4, 'title', i18nInterpolation2(i18n_3, ctx.exp1, ctx.exp2)); - } - } - }); - } + }); + } - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
trad 1hellotrad
'); + const fixture = new ComponentFixture(MyApp); + expect(fixture.html) + .toEqual('
Content:
beforemiddleafter
!
'); + }); - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual('
trad 1hellotrad
'); + 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); - // Change the expressions - fixture.component.exp1 = 'expr 1'; - fixture.component.exp2 = 'expr 2'; - fixture.update(); - expect(fixture.html) - .toEqual( - '
trad expr 1hellotrad
'); + // 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('view containers / embedded templates', () => { - it('should support containers', () => { - const MSG_DIV_SECTION_1 = `valeur: {$EXP_1}`; - // The indexes are based on the main template function - let i18n_1: I18nInstruction[][]; + 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: + //
+ // + // trad {{exp1}} + // + // hello + // + // + // trad + // + //
+ + 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, - factory: () => new MyApp(), selectors: [['my-app']], - consts: 3, - vars: 0, - // Initial template: - // before ( - // % if (condition) { // with i18n - // value: {{exp1}} - // % } - // ) after - - // Translated : - // before ( - // % if (condition) { // with i18n - // valeur: {{exp1}} - // % } - // ) after - template: (rf: RenderFlags, myApp: MyApp) => { + factory: () => new MyApp(), + consts: 10, + vars: 2, + template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 0}]); + 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(); } - - text(0, 'before ('); - container(1); - text(2, ') after'); + elementEnd(); } if (rf & RenderFlags.Update) { - containerRefreshStart(1); - { - let rf0 = embeddedViewStart(0, 1, 1); - if (rf0 & RenderFlags.Create) { - // Start of translated section 1 - text(0); // EXP_1 - // End of translated section 1 - i18nApply(0, i18n_1[0]); - } - if (rf0 & RenderFlags.Update) { - textBinding(0, bind(myApp.exp1)); - } - embeddedViewEnd(); - } - containerRefreshEnd(); + 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('before (valeur: 1) after'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('before (valeur: 1) after'); + expect(fixture.html) + .toEqual( + `
trad 1hellotrad
`); }); - it('should support ng-container', () => { - const MSG_DIV_SECTION_1 = `{$START_B}{$END_B}`; - // With ng-container the i18n node doesn't create any element at runtime which means that - // its children are not the only children of their parent, some nodes which are not - // translated might also be the children of the same parent. - // This is why we need to pass the `lastChildIndex` to `i18nMapping` - let i18n_1: I18nInstruction[][]; - // Initial template: - //
- // - // - // - // - // - // + it('should support attribute translations on removed elements', () => { + // Translated template: + //
+ // trad {{exp1}} //
- // Translated to: - //
- // - // - // - // - // - //
- function createTemplate() { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{'START_B': 2, 'START_C': 3}], null, null, 4); - } - - elementStart(0, 'div'); - { - element(1, 'a'); - { - // Start of translated section 1 - element(2, 'b'); // START_B - element(3, 'c'); // START_C - // End of translated section 1 - } - element(4, 'd'); - } - elementEnd(); - i18nApply(2, i18n_1[0]); - } - - const fixture = new TemplateFixture(createTemplate, () => {}, 5); - expect(fixture.html).toEqual('
'); - }); - - it('should support embedded templates', () => { - const MSG_DIV_SECTION_1 = `{$START_LI}valeur: {$EXP_1}!{$END_LI}`; - // The indexes are based on each template function - let i18n_1: I18nInstruction[][]; - - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 2 - elementStart(0, 'li'); // START_LI - { text(1); } // EXP_1 - elementEnd(); - // End of translated section 2 - i18nApply(0, i18n_1[1]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } + const MSG_DIV_1 = `trad �0�`; + const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; class MyApp { - items: string[] = ['1', '2']; + exp1 = '1'; + exp2 = '2'; static ngComponentDef = defineComponent({ type: MyApp, - factory: () => new MyApp(), selectors: [['my-app']], - consts: 2, - vars: 1, - // Initial template: - //
    - //
  • value: {{item}}
  • - //
- - // Translated to: - //
    - //
  • valeur: {{item}}!
  • - //
- template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, [{'START_LI': 1}, {'START_LI': 0}], [null, {'EXP_1': 1}], - ['START_LI']); - } - - elementStart(0, 'ul'); - { - // Start of translated section 1 - template(1, liTemplate, 2, 1, null, ['ngForOf', '']); // START_LI - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(1, 'ngForOf', bind(myApp.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
  • valeur: 1!
  • valeur: 2!
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: 1!
  • valeur: 2!
'); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: 1!
'); - - // Change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: one!
'); - - // Add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: one!
  • valeur: two!
'); - }); - - it('should support sibling embedded templates', () => { - const MSG_DIV_SECTION_1 = - `{$START_LI_0}valeur: {$EXP_1}!{$END_LI_0}{$START_LI_1}valeur bis: {$EXP_2}!{$END_LI_1}`; - // The indexes are based on each template function - let i18n_1: I18nInstruction[][]; - - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 2 - elementStart(0, 'li'); // START_LI_0 - { text(1); } // EXP_1 - elementEnd(); - // End of translated section 2 - i18nApply(0, i18n_1[1]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - function liTemplateBis(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 3 - elementStart(0, 'li'); // START_LI_1 - { text(1); } // EXP_2 - elementEnd(); - // End of translated section 3 - i18nApply(0, i18n_1[2]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - class MyApp { - items: string[] = ['1', '2']; - - static ngComponentDef = defineComponent({ - type: MyApp, factory: () => new MyApp(), - selectors: [['my-app']], - consts: 3, - vars: 2, - // Initial template: - //
    - //
  • value: {{item}}
  • - //
  • value bis: {{item}}
  • - //
- - // Translated to: - //
    - //
  • valeur: {{item}}!
  • - //
  • valeur bis: {{item}}!
  • - //
- template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, - [{'START_LI_0': 1, 'START_LI_1': 2}, {'START_LI_0': 0}, {'START_LI_1': 0}], - [null, {'EXP_1': 1}, {'EXP_2': 1}], ['START_LI_0', 'START_LI_1']); - } - - elementStart(0, 'ul'); - { - // Start of translated section 1 - template(1, liTemplate, 2, 1, null, ['ngForOf', '']); // START_LI_0 - template(2, liTemplateBis, 2, 1, null, ['ngForOf', '']); // START_LI_1 - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(1, 'ngForOf', bind(myApp.items)); - elementProperty(2, 'ngForOf', bind(myApp.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - '
  • valeur: 1!
  • valeur: 2!
  • valeur bis: 1!
  • valeur bis: 2!
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
  • valeur: 1!
  • valeur: 2!
  • valeur bis: 1!
  • valeur bis: 2!
'); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: 1!
  • valeur bis: 1!
'); - - // Change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: one!
  • valeur bis: one!
'); - - // Add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html) - .toEqual( - '
  • valeur: one!
  • valeur: two!
  • valeur bis: one!
  • valeur bis: two!
'); - }); - - it('should support changing the order of multiple template roots in the same template', () => { - const MSG_DIV_SECTION_1 = - `{$START_LI_1}valeur bis: {$EXP_2}!{$END_LI_1}{$START_LI_0}valeur: {$EXP_1}!{$END_LI_0}`; - // The indexes are based on each template function - let i18n_1: I18nInstruction[][]; - - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 2 - elementStart(0, 'li'); // START_LI_0 - { text(1); } // EXP_1 - elementEnd(); - // End of translated section 2 - i18nApply(0, i18n_1[1]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - function liTemplateBis(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 3 - elementStart(0, 'li'); // START_LI_1 - { text(1); } // EXP_2 - elementEnd(); - // End of translated section 3 - i18nApply(0, i18n_1[2]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - class MyApp { - items: string[] = ['1', '2']; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 3, - vars: 2, - // Initial template: - //
    - //
  • value: {{item}}
  • - //
  • value bis: {{item}}
  • - //
- - // Translated to: - //
    - //
  • valeur bis: {{item}}!
  • - //
  • valeur: {{item}}!
  • - //
- template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, - [{'START_LI_0': 1, 'START_LI_1': 2}, {'START_LI_0': 0}, {'START_LI_1': 0}], - [null, {'EXP_1': 1}, {'EXP_2': 1}], ['START_LI_0', 'START_LI_1']); - } - - elementStart(0, 'ul'); - { - // Start of translated section 1 - template(1, liTemplate, 2, 1, null, ['ngForOf', '']); // START_LI_0 - template(2, liTemplateBis, 2, 1, null, ['ngForOf', '']); // START_LI_1 - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(1, 'ngForOf', bind(myApp.items)); - elementProperty(2, 'ngForOf', bind(myApp.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - '
  • valeur bis: 1!
  • valeur bis: 2!
  • valeur: 1!
  • valeur: 2!
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
  • valeur bis: 1!
  • valeur bis: 2!
  • valeur: 1!
  • valeur: 2!
'); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur bis: 1!
  • valeur: 1!
'); - - // Change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur bis: one!
  • valeur: one!
'); - - // Add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html) - .toEqual( - '
  • valeur bis: one!
  • valeur bis: two!
  • valeur: one!
  • valeur: two!
'); - }); - - it('should support nested embedded templates', () => { - const MSG_DIV_SECTION_1 = `{$START_LI}{$START_SPAN}valeur: {$EXP_1}!{$END_SPAN}{$END_LI}`; - // The indexes are based on each template function - let i18n_1: I18nInstruction[][]; - - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 2 - elementStart(0, 'li'); // START_LI - { - template(1, spanTemplate, 2, 1, null, ['ngForOf', '']); // START_SPAN - } - elementEnd(); - // End of translated section 2 - i18nApply(0, i18n_1[1]); - } - if (rf1 & RenderFlags.Update) { - const myApp = nextContext(); - elementProperty(1, 'ngForOf', bind(myApp.items)); - } - } - - function spanTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 3 - elementStart(0, 'span'); // START_SPAN - { text(1); } // EXP_1 - elementEnd(); - // End of translated section 3 - i18nApply(0, i18n_1[2]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - class MyApp { - items: string[] = ['1', '2']; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - // Initial template: - //
    - //
  • - // value: {{item}} - //
  • - //
- - // Translated to: - //
    - //
  • - // valeur: {{item}}! - //
  • - //
- template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, - [{'START_LI': 1}, {'START_LI': 0, 'START_SPAN': 1}, {'START_SPAN': 0}], - [null, null, {'EXP_1': 1}], ['START_LI', 'START_SPAN']); - } - - elementStart(0, 'ul'); - { - // Start of translated section 1 - template(1, liTemplate, 2, 1, null, ['ngForOf', '']); // START_LI - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(1, 'ngForOf', bind(myApp.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual( - '
  • valeur: 1!valeur: 2!
  • valeur: 1!valeur: 2!
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual( - '
  • valeur: 1!valeur: 2!
  • valeur: 1!valeur: 2!
'); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: 1!
'); - - // Change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
  • valeur: one!
'); - - // Add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html) - .toEqual( - '
  • valeur: one!valeur: two!
  • valeur: one!valeur: two!
'); - }); - - it('should be able to move template roots around', () => { - const MSG_DIV_SECTION_1 = - `{$START_LI_0}début{$END_LI_0}{$START_LI_1}valeur: {$EXP_1}{$END_LI_1}fin`; - // The indexes are based on each template function - let i18n_1: I18nInstruction[][]; - - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 2 - elementStart(0, 'li'); // START_LI_1 - { text(1); } // EXP_1 - elementEnd(); - // End of translated section 2 - i18nApply(0, i18n_1[1]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], consts: 5, - vars: 1, - // Initial template: - //
    - //
  • start
  • - //
  • value: {{item}}
  • - //
  • delete me
  • - //
- - // Translated to: - //
    - //
  • début
  • - //
  • valeur: {{item}}
  • - // fin - //
- template: (rf: RenderFlags, myApp: MyApp) => { + vars: 5, + template: (rf: RenderFlags, ctx: MyApp) => { if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, - [{'START_LI_0': 1, 'START_LI_1': 2, 'START_LI_2': 3}, {'START_LI_1': 0}], - [null, {'EXP_1': 1}], ['START_LI_1']); - } - - elementStart(0, 'ul'); - { - // Start of translated section 1 - element(1, 'li'); // START_LI_0 - template(2, liTemplate, 2, 1, null, ['ngForOf', '']); // START_LI_1 - elementStart(3, 'li'); // START_LI_2 - { text(4, 'delete me'); } - elementEnd(); - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(2, 'ngForOf', bind(myApp.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
  • début
  • valeur: first
  • valeur: second
  • fin
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html) - .toEqual('
  • début
  • valeur: first
  • valeur: second
  • fin
'); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
  • début
  • valeur: first
  • fin
'); - - // Change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
  • début
  • valeur: one
  • fin
'); - - // Add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html) - .toEqual('
  • début
  • valeur: one
  • valeur: two
  • fin
'); - }); - - it('should be able to remove template roots', () => { - const MSG_DIV_SECTION_1 = `loop`; - // The indexes are based on each template function - let i18n_1: I18nInstruction[][]; - function liTemplate(rf1: RenderFlags, row: NgForOfContext) { - if (rf1 & RenderFlags.Create) { - // This is a container so the whole template is a translated section - // Start of translated section 2 - elementStart(0, 'li'); // START_LI - { text(1); } // EXP_1 - elementEnd(); - // End of translated section 2 - i18nApply(0, i18n_1[1]); - } - if (rf1 & RenderFlags.Update) { - textBinding(1, bind(row.$implicit)); - } - } - - class MyApp { - items: string[] = ['first', 'second']; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 2, - vars: 1, - // Initial template: - //
    - //
  • value: {{item}}
  • - //
- - // Translated to: - //
    - // loop - //
- template: (rf: RenderFlags, myApp: MyApp) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, [{'START_LI': 1}, {'START_LI': 0}], [null, {'EXP_1': 1}], - ['START_LI']); - } - - elementStart(0, 'ul'); - { - // Start of translated section 1 - template(1, liTemplate, 2, 1, undefined, ['ngForOf', '']); // START_LI - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(1, 'ngForOf', bind(myApp.items)); - } - }, - directives: () => [NgForOf] - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
    loop
'); - - // Change detection cycle, no model changes - fixture.update(); - expect(fixture.html).toEqual('
    loop
'); - - // Remove the last item - fixture.component.items.length = 1; - fixture.update(); - expect(fixture.html).toEqual('
    loop
'); - - // Change an item - fixture.component.items[0] = 'one'; - fixture.update(); - expect(fixture.html).toEqual('
    loop
'); - - // Add an item - fixture.component.items.push('two'); - fixture.update(); - expect(fixture.html).toEqual('
    loop
'); - }); - }); - - 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 = - `{$START_CHILD}Je suis projeté depuis {$START_B}{$EXP_1}{$END_B}{$END_CHILD}`; - let i18n_1: I18nInstruction[][]; - const MSG_ATTR_1 = `Enfant de {$EXP_1}`; - let i18n_2: I18nExpInstruction[]; - - @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) { - if (!i18n_1) { - i18n_1 = i18nMapping( - MSG_DIV_SECTION_1, [{ - 'START_CHILD': 1, - 'START_B': 2, - 'START_REMOVE_ME_1': 4, - 'START_REMOVE_ME_2': 5, - 'START_REMOVE_ME_3': 6 - }], - [{'EXP_1': 3}]); - } - if (!i18n_2) { - i18n_2 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0}); - } - elementStart(0, 'div'); { - // Start of translated section 1 - elementStart(1, 'child'); // START_CHILD + i18nAttributes(1, MSG_DIV_1_ATTR_1); + i18nStart(2, MSG_DIV_1); { - elementStart(2, 'b'); // START_B - { - text(3); // EXP_1 - element(4, 'remove-me-1'); // START_REMOVE_ME_1 - } + elementStart(3, 'b'); // Will be removed + { i18nAttributes(4, MSG_DIV_1_ATTR_1); } elementEnd(); - element(5, 'remove-me-2'); // START_REMOVE_ME_2 } - elementEnd(); - element(6, 'remove-me-3'); // START_REMOVE_ME_3 - // End of translated section 1 + i18nEnd(); } elementEnd(); - i18nApply(1, i18n_1[0]); } if (rf & RenderFlags.Update) { - elementProperty(2, 'title', i18nInterpolation1(i18n_2, cmp.name)); - textBinding(3, bind(cmp.name)); + 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(Parent); - expect(fixture.html) - .toEqual( - '

Je suis projeté depuis Parent

'); + const fixture = new ComponentFixture(MyApp); + expect(fixture.html).toEqual(`
trad 1
`); }); - 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(); + 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 = `Je suis projeté depuis {$EXP_1}`; - let i18n_1: I18nInstruction[][]; - const MSG_ATTR_1 = `Enfant de {$EXP_1}`; - let i18n_2: I18nExpInstruction[]; + 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: ` + @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

'); + //

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}} + + + 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: 6, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 4}]); - } - if (!i18n_2) { - i18n_2 = i18nExpMapping(MSG_ATTR_1, {'EXP_1': 0}); - } - - elementStart(0, 'div'); - { - elementStart(1, 'child'); + // 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'); { - element(2, 'any'); - elementStart(3, 'b'); + elementStart(1, 'child'); { - // Start of translated section 1 - text(4); // EXP_1 - // End of translated section 1 + element(2, 'any'); + elementStart(3, 'b'); + { + i18nAttributes(4, MSG_ATTR_1); + i18n(5, MSG_DIV_SECTION_1); + } + elementEnd(); + element(6, 'any'); } elementEnd(); - element(5, 'any'); } elementEnd(); } - elementEnd(); - i18nApply(4, i18n_1[0]); - } - if (rf & RenderFlags.Update) { - elementProperty(3, 'title', i18nInterpolation1(i18n_2, cmp.name)); - textBinding(4, bind(cmp.name)); - } - } - }); - } - - 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 = `{$START_B}Bonjour{$END_B} Monde!`; - let i18n_1: I18nInstruction[][]; - - @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) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{'START_B': 1}]); + if (rf & RenderFlags.Update) { + i18nExp(bind(cmp.name)); + i18nApply(4); + i18nExp(bind(cmp.name)); + i18nApply(5); } - - elementStart(0, 'child'); - { - // Start of translated section 1 - element(1, 'b'); // START_B - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); } - } - }); - } + }); + } - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
Bonjour Monde!
'); - }); + const fixture = new ComponentFixture(Parent); + expect(fixture.html) + .toEqual( + '

Je suis projeté depuis Parent

'); + }); - it('should project translations with selectors', () => { - @Component({ - selector: 'child', - template: ` + 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); + }) + 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 = `{$START_SPAN_0}Contenu{$END_SPAN_0}`; - let i18n_1: I18nInstruction[][]; + const MSG_DIV_SECTION_1 = `�#2�Contenu�/#2�`; - @Component({ - selector: 'parent', - template: ` + @Component({ + selector: 'parent', + template: ` ` - // Translated to: - // Contenu - }) - class 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) { - if (!i18n_1) { - i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{'START_SPAN_0': 1, 'START_SPAN_1': 2}]); + // 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(); } - - elementStart(0, 'child'); - { - // Start of translated section 1 - element(1, 'span', ['title', 'keepMe']); // START_SPAN_0 - element(2, 'span', ['title', 'deleteMe']); // START_SPAN_1 - // End of translated section 1 - } - elementEnd(); - i18nApply(1, i18n_1[0]); } - } - }); - } + }); + } - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('Contenu'); - }); - }); - - describe('i18nInterpolation', () => { - it('i18nInterpolation should return the same value as i18nInterpolationV', () => { - const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; - const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1}); - let interpolation; - let interpolationV; - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 4, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - interpolation = i18nInterpolation2(i18n_1, ctx.exp1, ctx.exp2); - interpolationV = i18nInterpolationV(i18n_1, [ctx.exp1, ctx.exp2]); - elementProperty(0, 'title', interpolation); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(interpolation).toBeDefined(); - expect(interpolation).toEqual(interpolationV); - }); - - it('i18nInterpolation3 should work', () => { - const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} end`; - const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2}); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - exp3: any = '3'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 3, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty(0, 'title', i18nInterpolation3(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); - }); - - it('i18nInterpolation4 should work', () => { - const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} end`; - const i18n_1 = - i18nExpMapping(MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3}); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - exp3: any = '3'; - exp4: any = '4'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 4, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty( - 0, 'title', i18nInterpolation4(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); - }); - - it('i18nInterpolation5 should work', () => { - const MSG_DIV_SECTION_1 = `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} end`; - const i18n_1 = i18nExpMapping( - MSG_DIV_SECTION_1, {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4}); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - exp3: any = '3'; - exp4: any = '4'; - exp5: any = '5'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 5, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty( - 0, 'title', - i18nInterpolation5(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); - }); - - it('i18nInterpolation6 should work', () => { - const MSG_DIV_SECTION_1 = - `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} _ {$EXP_6} end`; - const i18n_1 = i18nExpMapping( - MSG_DIV_SECTION_1, - {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4, 'EXP_6': 5}); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - exp3: any = '3'; - exp4: any = '4'; - exp5: any = '5'; - exp6: any = '6'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 6, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty( - 0, 'title', - i18nInterpolation6( - i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5, ctx.exp6)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); - }); - - it('i18nInterpolation7 should work', () => { - const MSG_DIV_SECTION_1 = - `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} _ {$EXP_6} _ {$EXP_7} end`; - const i18n_1 = i18nExpMapping( - MSG_DIV_SECTION_1, - {'EXP_1': 0, 'EXP_2': 1, 'EXP_3': 2, 'EXP_4': 3, 'EXP_5': 4, 'EXP_6': 5, 'EXP_7': 6}); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - exp3: any = '3'; - exp4: any = '4'; - exp5: any = '5'; - exp6: any = '6'; - exp7: any = '7'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 7, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty( - 0, 'title', i18nInterpolation7( - i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5, - ctx.exp6, ctx.exp7)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); - }); - - it('i18nInterpolation8 should work', () => { - const MSG_DIV_SECTION_1 = - `start {$EXP_1} _ {$EXP_2} _ {$EXP_3} _ {$EXP_4} _ {$EXP_5} _ {$EXP_6} _ {$EXP_7} _ {$EXP_8} end`; - const i18n_1 = i18nExpMapping(MSG_DIV_SECTION_1, { - 'EXP_1': 0, - 'EXP_2': 1, - 'EXP_3': 2, - 'EXP_4': 3, - 'EXP_5': 4, - 'EXP_6': 5, - 'EXP_7': 6, - 'EXP_8': 7 + const fixture = new ComponentFixture(Parent); + expect(fixture.html).toEqual('Contenu'); }); - - class MyApp { - exp1: any = '1'; - exp2: any = '2'; - exp3: any = '3'; - exp4: any = '4'; - exp5: any = '5'; - exp6: any = '6'; - exp7: any = '7'; - exp8: any = '8'; - - static ngComponentDef = defineComponent({ - type: MyApp, - factory: () => new MyApp(), - selectors: [['my-app']], - consts: 1, - vars: 8, - // Initial template: - //
- - // Translated to: - //
- template: (rf: RenderFlags, ctx: MyApp) => { - if (rf & RenderFlags.Create) { - element(0, 'div'); // translated section 1 - } - if (rf & RenderFlags.Update) { - elementProperty( - 0, 'title', i18nInterpolation8( - i18n_1, ctx.exp1, ctx.exp2, ctx.exp3, ctx.exp4, ctx.exp5, - ctx.exp6, ctx.exp7, ctx.exp8)); - } - } - }); - } - - const fixture = new ComponentFixture(MyApp); - expect(fixture.html).toEqual('
'); }); - }); });