From f8f1168fa61b04568e6cc4a7d54b62a6ebac3ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Wed, 14 Nov 2018 10:23:21 -0800 Subject: [PATCH] Revert "feat(ivy): support for i18n & ICU expressions (#26275)" This reverts commit a63fd2d0f5f867ee17bf991a72ccbe6d6fa76566. --- .../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 | 2081 ++++------- 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 | 3121 +++++++++-------- 25 files changed, 2563 insertions(+), 3005 deletions(-) delete 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 e15a714944..4b748509e5 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$.ɵi18nAttributes(3, $_c2$); + $r3$.ɵi18nAttribute(3, $_c2$); $r3$.ɵtext(4, "Content B"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(5, "div"); - $r3$.ɵi18nAttributes(6, $_c4$); + $r3$.ɵi18nAttribute(6, $_c4$); $r3$.ɵtext(7, "Content C"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(8, "div"); - $r3$.ɵi18nAttributes(9, $_c6$); + $r3$.ɵi18nAttribute(9, $_c6$); $r3$.ɵtext(10, "Content D"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(11, "div"); - $r3$.ɵi18nAttributes(12, $_c8$); + $r3$.ɵi18nAttribute(12, $_c8$); $r3$.ɵtext(13, "Content E"); $r3$.ɵelementEnd(); $r3$.ɵelementStart(14, "div"); - $r3$.ɵi18nAttributes(15, $_c10$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(1, $_c2$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(2, $_c4$); + $r3$.ɵi18nAttribute(2, $_c4$); $r3$.ɵelementEnd(); $r3$.ɵelementStart(3, "div", $_c5$); - $r3$.ɵi18nAttributes(4, $_c8$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(3, $_c2$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(2, $_c4$); + $r3$.ɵi18nAttribute(2, $_c4$); $r3$.ɵelementEnd(); $r3$.ɵelementStart(3, "div", $_c5$); - $r3$.ɵi18nAttributes(4, $_c8$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(3, $_c2$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(3, $_c2$); + $r3$.ɵi18nAttribute(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$.ɵi18nAttributes(8, $_c5$); + $r3$.ɵi18nAttribute(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 ab4bfe494e..4337f4fefb 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 i18nAttributes: o.ExternalReference = {name: 'ɵi18nAttributes', moduleName: CORE}; + static i18nAttribute: o.ExternalReference = {name: 'ɵi18nAttribute', 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 1fd2a8584d..005839beac 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.i18nAttributes, [index, args]); + this.creationInstruction(element.sourceSpan, R3.i18nAttribute, [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 7c0952b012..cf66b58e12 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -108,12 +108,24 @@ export { PipeDef as ɵPipeDef, PipeDefWithMeta as ɵPipeDefWithMeta, whenRendered as ɵwhenRendered, - i18nAttributes as ɵi18nAttributes, + i18nAttribute as ɵi18nAttribute, i18nExp as ɵi18nExp, i18nStart as ɵi18nStart, i18nEnd as ɵi18nEnd, i18nApply as ɵi18nApply, - i18nIcuReplaceVars as ɵi18nIcuReplaceVars, + 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, 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 c44c7e572b..d4647bb337 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 a6e8f938ea..20928eca12 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; dynamic nodes | host prop names; directive tokens; provider tokens; `null` +| 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 ## `HEADER` diff --git a/packages/core/src/render3/i18n.md b/packages/core/src/render3/i18n.md index c7fd23e14d..d5b93aa4a6 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 `title` (with `i18n-title` present). + 1. In attribute as in `i18n-title`. 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 -const i18nUpdateOpCodes = [ - // The following OpCodes represent: `
` +[ + // 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,13 +170,12 @@ const i18nUpdateOpCodes = [ 'Hello ', // accumulate('Hello '); -1, // accumulate(-1); '!', // accumulate('!'); - // Update attribute: `elementAttribute(0, 'title', accumulatorFlush(null));` + // Update attribute: `elementAttribute(1, 'title', accumulatorFlush(null));` // NOTE: `null` means don't sanitize - 0 << SHIFT_REF | Attr, 'title', null, + 1 << 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. @@ -217,7 +216,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, ...); @@ -299,7 +298,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` @@ -363,7 +362,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: [ @@ -380,16 +379,16 @@ const tI18n = { // has NOT changed then skip `2` values and start processing next OpCodes. 0b1, 2, -1, // accumulate(-1); - // Switch ICU: `icuSwitchCase(lViewData[200 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` - 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. 0b1, 1, - // 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 + // 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 // one ICU so it is 0-th ICU to update. - 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, ], icus: [ { @@ -545,7 +544,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: [ @@ -624,7 +623,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: [ @@ -636,18 +635,18 @@ const tI18n = { // has NOT changed then skip `2` values and start processing next OpCodes. 0b1, 2, -1, // accumulate(-1) - // Switch ICU: `icuSwitchCase(lViewData[200 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` - 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 0 << SHIFT_REF | 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[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 + // 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 // one ICU so it is 0-th ICU to update. - 200 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, '.', // accumulate('.'); @@ -700,7 +699,7 @@ const tI18n = { // 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'); ] ] @@ -725,7 +724,7 @@ Given ``` The above needs to be parsed into: ```TypeScript -const icu = { +{ type: 'plural', // or 'select' expressionBindingIndex: 0, // from �0�, cases: [ @@ -758,7 +757,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 inserted synchronously. +Placing `i18n` attribute on an existing elements is easy because the element defines parent and the translated element can be insert 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. @@ -784,7 +783,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` @@ -870,7 +869,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` @@ -882,16 +881,16 @@ const tI18n = { // has NOT changed then skip `2` values and start processing next OpCodes. 0b1, 2, -1, // accumulate(-1); - // Switch ICU: `icuSwitchCase(lViewData[100 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` - 100 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. 0b1, 1, - // 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 + // 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 // one ICU so it is 0-th ICU to update. - 100 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, ], icus: [ { // {�0�, plural, =0 {zero} other {�0� }} @@ -907,7 +906,7 @@ const tI18n = { '', 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, @@ -922,16 +921,16 @@ const tI18n = { ], [ // 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[101 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/, accumulatorFlush());` - 101 << SHIFT_REF | 0 << SHIFT_ICU | IcuSwitch, + -1 + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 1 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuSwitch, // NOTE: the bit mask here is the logical OR of all of the masks int the ICU. 0b10, 1, - // Update ICU: `icuUpdateCase(lViewData[101 /*SHIFT_REF*/], 0 /*SHIFT_ICU*/);` - 101 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate, + // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 1 /*SHIFT_REF*/);` + 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuUpdate, ], ] }, @@ -1058,7 +1057,7 @@ Here is a more complete example. Given this Angular's template: ```HTML -
+
{{count}} is rendered as: { count, plural, @@ -1112,7 +1111,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 6b0c35f722..c92dd51091 100644 --- a/packages/core/src/render3/i18n.ts +++ b/packages/core/src/render3/i18n.ts @@ -6,469 +6,275 @@ * found in the LICENSE file at https://angular.io/license */ -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 {NO_CHANGE} from '../../src/render3/tokens'; + +import {assertEqual, assertLessThan} from './assert'; +import {adjustBlueprintForNewNode, bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, createNodeAtIndex, load} from './instructions'; import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n'; -import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node'; +import {TElementNode, 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, LViewData, TVIEW, TView} from './interfaces/view'; +import {BINDING_INDEX, HEADER_OFFSET, HOST_NODE, TVIEW} from './interfaces/view'; import {appendChild, createTextNode, removeChild} from './node_manipulation'; -import {_getViewData, getIsParent, getPreviousOrParentTNode, getRenderer, getTView, setIsParent, setPreviousOrParentTNode} from './state'; -import {NO_CHANGE} from './tokens'; -import {addAllToArray, getNativeByIndex, getNativeByTNode, getTNode, isLContainer, stringify} from './util'; +import {getRenderer, getViewData, resetComponentState} from './state'; +import {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)[][]; -} -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; +/** + * 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), } /** - * 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. - * + * 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. */ -function extractParts(pattern: string): (string | IcuExpression)[] { - if (!pattern) { - return []; - } +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; - 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; +/** + * 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); - let match; - while (match = braces.exec(pattern)) { - const pos = match.index; - if (match[0] == '}') { - braceStack.pop(); + generateMappingInstructions( + 0, 0, translationParts, instructions, elements, expressions, templateRoots, lastChildIndex); - 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; - } - } else { - 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); - } - - 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)); + 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 { - 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); + tmplInstructions.push(I18nInstructions.CloseNode); - if (value.charAt(0) === '#') { - parentIndexStack[++parentIndexPointer] = parentIndex = phIndex; + if (tmplIndex > 0) { + openedTagCount--; + + // If we have reached the closing tag for this template, exit the loop + if (openedTagCount === 0) { + break; + } } } - } 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); + if (phIndex !== undefined && phIndex > maxIndex) { + maxIndex = phIndex; + } - // 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 (templateRoots) { + const newTmplIndex = templateRoots.indexOf(value) + 1; + if (newTmplIndex !== 0 && newTmplIndex !== tmplIndex) { + partIndex = generateMappingInstructions( + newTmplIndex, partIndex, translationParts, instructions, elements, expressions, + templateRoots, lastChildIndex); + } + } - if (hasBinding) { - addAllToArray( - generateBindingUpdateOpCodes(value, tView.blueprint.length - 1 - HEADER_OFFSET), - updateOpCodes); + } else if (value) { + // It's a non-empty string, create a text node + tmplInstructions.push(I18nInstructions.Text, value); + } + } + + // Add instructions to remove elements that are not used in the translation + if (elements) { + const tmplElements = elements[tmplIndex]; + + if (tmplElements) { + const phKeys = Object.keys(tmplElements); + + 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; } } } } } - // 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; + // 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 | 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; +function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode): TNode { + if (ngDevMode) { + ngDevMode.rendererMoveNode++; } - if (parentTNode !== viewData[HOST_NODE]) { - tNode.parent = parentTNode as TElementNode; + 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 (parentTNode !== viewData[HOST_NODE]) { + tNode.parent = parentTNode as TElementNode; + } } appendChild(getNativeByTNode(tNode, viewData), tNode, viewData); @@ -482,977 +288,492 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode | return tNode; } -/** - * 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 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 +} + export function i18nEnd(): 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) { - i18nEndFirstPass(tView); - } + // placeholder for i18nEnd function } /** - * See `i18nEnd` above. + * 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. */ -function i18nEndFirstPass(tView: TView) { - const viewData = _getViewData(); - ngDevMode && assertEqual( - viewData[BINDING_INDEX], viewData[TVIEW].bindingStartIndex, - 'i18nEnd should be called before any binding'); - - 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); - } +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'); + } + + if (!instructions) { + return; } -} -function readCreateOpCodes( - index: number, createOpCodes: I18nMutateOpCodes, expandoStartIndex: number, - viewData: LViewData): number[] { const renderer = getRenderer(); - 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); - } - 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}"`); - } - } - } + 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 - 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; - } - } + 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++; } - } - } - 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); + 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); - 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'); + 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); + } + removedTNode.detached = true; + lContainer[RENDER_PARENT] = null; } break; - } - case IcuType.select: { - index = icuExpression.cases.indexOf('other'); - break; - } - } - } - return index; -} - -/** - * Generate the OpCodes for ICU expressions. - * - * @param tIcus - * @param icuExpression - * @param startIndex - * @param expandoStartIndex - */ -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); - } -} - -/** - * Transforms a string template into an HTML template and a list of instructions used to update - * attributes or nodes that contain bindings. - * - * @param unsafeHtml The string to parse - * @param parentIndex - * @param nestedIcus - * @param tIcus - * @param expandoStartIndex - */ -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'); - } - 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+)�/; - -/** - * Parses a node, its children and its siblings, and generates the mutate & update OpCodes. - * - * @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 - */ -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 !; - } - - 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); } } } -const RAW_ICU_REGEXP = /{\s*(\S*)\s*,\s*\S{6}\s*,[\s\S]*}/gi; +/** + * 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. + */ +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]]; + } + return staticText; +} /** - * Replaces the variable parameter (main binding) of an ICU by a given value. + * Checks if the value of an expression has changed and replaces it by its value in a translation, + * or returns NO_CHANGE. * - * 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}}" - * ``` + * @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. */ -export function i18nIcuReplaceVars(message: string, replacements: {[key: string]: string}): string { - const keys = Object.keys(replacements); - for (let i = 0; i < keys.length; i++) { - message = message.replace(RAW_ICU_REGEXP, (str: string, varMatch: string) => { - return str.replace(varMatch, replacements[keys[i]]); - }); +export function i18nInterpolation1(instructions: I18nExpInstruction[], v0: any): string|NO_CHANGE { + const different = bindingUpdated(getViewData()[BINDING_INDEX]++, v0); + + if (!different) { + return NO_CHANGE; } - return message; + + 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; +} + +/** + * Checks if the values of up to 2 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. + * + * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. + */ +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; + + 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]; + } + } + + return res; +} + +/** + * Checks if the values of up to 3 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. + * + * @returns The concatenated string when any of the arguments changes, `NO_CHANGE` otherwise. + */ +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; + } + + 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]; + } + } + + 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; } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index db46134731..c441025c40 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -87,12 +87,24 @@ export { } from './state'; export { - i18nAttributes, + i18nAttribute, i18nExp, i18nStart, i18nEnd, i18nApply, - i18nIcuReplaceVars, + i18nMapping, + i18nInterpolation1, + i18nInterpolation2, + i18nInterpolation3, + i18nInterpolation4, + i18nInterpolation5, + i18nInterpolation6, + i18nInterpolation7, + i18nInterpolation8, + i18nInterpolationV, + i18nExpMapping, + I18nInstruction, + I18nExpInstruction } 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 c4b34fab27..91eea29ae0 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -7,6 +7,7 @@ */ import './ng_dev_mode'; + import {resolveForwardRef} from '../di/forward_ref'; import {InjectionToken} from '../di/injection_token'; import {InjectFlags} from '../di/injector_compatibility'; @@ -15,6 +16,7 @@ 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'; @@ -23,12 +25,11 @@ 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, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from './interfaces/node'; +import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, 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'; @@ -41,6 +42,7 @@ 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. @@ -52,6 +54,11 @@ 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 @@ -190,13 +197,9 @@ 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& - TIcuContainerNode { + attrs: TAttributes | null): TElementNode&TContainerNode&TElementContainerNode&TProjectionNode { const viewData = getViewData(); const tView = getTView(); const adjustedIndex = index + HEADER_OFFSET; @@ -230,7 +233,7 @@ export function createNodeAtIndex( setPreviousOrParentTNode(tNode); setIsParent(true); return tNode as TElementNode & TViewNode & TContainerNode & TElementContainerNode & - TProjectionNode & TIcuContainerNode; + TProjectionNode; } export function createViewNode(index: number, view: LViewData) { @@ -252,12 +255,11 @@ export function createViewNode(index: number, view: LViewData) { * i18nApply() or ComponentFactory.create), we need to adjust the blueprint for future * template passes. */ -export function allocExpando(view: LViewData) { +export function adjustBlueprintForNewNode(view: LViewData) { const tView = view[TVIEW]; if (tView.firstTemplatePass) { tView.expandoStartIndex++; tView.blueprint.push(null); - tView.data.push(null); view.push(null); } } @@ -912,7 +914,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 | null): void { + index: number, name: string, value: any, sanitizer?: SanitizerFn): void { if (value !== NO_CHANGE) { const viewData = getViewData(); const renderer = getRenderer(); @@ -945,7 +947,7 @@ export function elementAttribute( */ export function elementProperty( - index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null): void { + index: number, propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn): 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 f50b9a4c4e..2c39b244ff 100644 --- a/packages/core/src/render3/interfaces/i18n.ts +++ b/packages/core/src/render3/interfaces/i18n.ts @@ -16,32 +16,24 @@ * * See: `I18nCreateOpCodes` for example of usage. */ -import {SanitizerFn} from './sanitization'; - export const enum I18nMutateOpCode { - /// Stores shift amount for bits 17-3 that contain reference index. - SHIFT_REF = 3, + /// Stores shift amount for bits 17-2 that contain reference index. + SHIFT_REF = 2, /// Stores shift amount for bits 31-17 that contain parent index. SHIFT_PARENT = 17, /// Mask for OpCode - MASK_OPCODE = 0b111, + MASK_OPCODE = 0b11, /// Mask for reference index. MASK_REF = ((2 ^ 16) - 1) << SHIFT_REF, /// OpCode to select a node. (next OpCode will contain the operation.) - Select = 0b000, + Select = 0b00, /// OpCode to append the current node to `PARENT`. - AppendChild = 0b001, + AppendChild = 0b01, /// OpCode to insert the current node to `PARENT` before `REF`. - InsertBefore = 0b010, + InsertBefore = 0b10, /// OpCode to remove the `REF` node from `PARENT`. - 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, + Remove = 0b11, } /** @@ -122,8 +114,8 @@ export interface COMMENT_MARKER { marker: 'comment'; } * // For removing existing nodes * // -------------------------------------------------- * // const node = lViewData[1]; - * // removeChild(tView.data(1), node, lViewData); - * 1 << SHIFT_REF | Remove, + * // lViewData[2].remove(node); + * 2 << SHIFT_PARENT | 1 << SHIFT_REF | Remove, * * // For writing attributes * // -------------------------------------------------- @@ -186,7 +178,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 of `i18nExp` index. * * OpCodes * ``` @@ -230,7 +222,7 @@ export const enum I18nUpdateOpCode { * ``` * */ -export interface I18nUpdateOpCodes extends Array {} +export interface I18nUpdateOpCodes extends Array string | null)> {} /** * Store information for the i18n translation block. @@ -363,6 +355,10 @@ export interface TIcu { update: I18nUpdateOpCodes[]; } -// Note: This hack is necessary so we don't erroneously get a circular dependency -// failure based on types. -export const unusedValueExportToPlacateAjd = 1; +/** + * 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 {} diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 5dcb059148..c190e6cb54 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -21,7 +21,6 @@ export const enum TNodeType { Element = 0b011, ViewOrElement = 0b010, ElementContainer = 0b100, - IcuContainer = 0b101, } /** @@ -256,7 +255,7 @@ export interface TNode { parent: TElementNode|TContainerNode|null; /** - * If this node is part of an i18n block, it indicates whether this node is part of the DOM. + * If this node is part of an i18n block, it indicates whether this container is part of the DOM * If this node is not part of an i18n block, this field is null. */ detached: boolean|null; @@ -359,6 +358,7 @@ export interface TContainerNode extends TNode { projection: null; } + /** Static data for an */ export interface TElementContainerNode extends TNode { /** Index in the LViewData[] array. */ @@ -369,21 +369,6 @@ 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 deleted file mode 100644 index 24970f93c9..0000000000 --- a/packages/core/src/render3/interfaces/sanitization.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @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 1b2a6162ed..61a4c00e8d 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, PipeDefInternal or TI18n. */ + /** Static data equivalent of LView.data[]. Contains TNodes. */ data: TData; /** @@ -535,7 +535,7 @@ export type HookData = (number | (() => void))[]; */ export type TData = (TNode | PipeDef| DirectiveDef| ComponentDef| number | Type| - InjectionToken| TI18n | I18nUpdateOpCodes | null)[]; + InjectionToken| 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 006020cf9f..ec6ff92af4 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, - 'ɵi18nAttributes': r3.i18nAttributes, + 'ɵi18nAttribute': r3.i18nAttribute, 'ɵ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 8637c107fc..ceb11abd7a 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -21,23 +21,8 @@ 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 { - 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 !; + return tNode.parent == null ? getHostNative(currentView) : + getNativeByTNode(tNode.parent, currentView); } /** @@ -593,22 +578,17 @@ function canInsertNativeChildOfView(viewTNode: TViewNode, view: LViewData): bool * * - * @param tNode The tNode of the node that we want to insert. + * @param parent The parent where the child will be inserted into. * @param currentView Current LView being processed. - * @return boolean Whether the node should be inserted now (or delayed until later). + * @return boolean Whether the child 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) { - 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 (tNode.parent && tNode.parent.type === TNodeType.ElementContainer) { + currentNode = getHighestElementContainer(tNode); + parent = currentNode.parent; } if (parent === null) parent = currentView[HOST_NODE]; @@ -659,7 +639,7 @@ export function nativeNextSibling(renderer: Renderer3, node: RNode): RNode|null * @returns Whether or not the child was appended */ export function appendChild( - childEl: RNode | null = null, childTNode: TNode, currentView: LViewData): boolean { + childEl: RNode | null, childTNode: TNode, currentView: LViewData): boolean { if (childEl !== null && canInsertNativeNode(childTNode, currentView)) { const renderer = currentView[RENDERER]; const parentEl = getParentNative(childTNode, currentView); @@ -675,9 +655,6 @@ 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 310e041c04..baa02dfac7 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -157,10 +157,6 @@ 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 243089edc7..9c0a9599ab 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -249,16 +249,4 @@ export function getParentInjectorTNode( export const defaultScheduler = (typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame || // browser only setTimeout // everything else - ).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]); - } -} + ).bind(global); \ No newline at end of file diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts index 4b69587fb4..6dd295f135 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')); -export const VALID_ELEMENTS = +const VALID_ELEMENTS = merge(VOID_ELEMENTS, BLOCK_ELEMENTS, INLINE_ELEMENTS, OPTIONAL_END_TAG_ELEMENTS); // Attributes that have href and hence need to be sanitized -export const URI_ATTRS = tagSet('background,cite,href,itemtype,longdesc,poster,src,xlink:href'); +const URI_ATTRS = tagSet('background,cite,href,itemtype,longdesc,poster,src,xlink:href'); // Attributes that have special href set hence need to be sanitized -export const SRCSET_ATTRS = tagSet('srcset'); +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. -export const VALID_ATTRS = merge(URI_ATTRS, SRCSET_ATTRS, HTML_ATTRS); +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 } } -export function getTemplateContent(el: Node): Node|null { +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 e0fec896fe..ac48a8cfd4 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -623,9 +623,6 @@ { "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 57a5183818..4874171e6b 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -260,9 +260,6 @@ { "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 4a03d58ac7..3efa3d8617 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 @@ -845,9 +845,6 @@ { "name": "getErrorLogger" }, - { - "name": "getFirstParentNative" - }, { "name": "getFirstTemplatePass" }, @@ -1107,7 +1104,7 @@ "name": "markViewDirty" }, { - "name": "merge$1" + "name": "merge" }, { "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 8ece166571..8d17e05d2b 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -665,9 +665,6 @@ { "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 236ff9d64f..cecfc71b4e 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -678,7 +678,7 @@ "name": "PlatformRef" }, { - "name": "Plural$1" + "name": "Plural" }, { "name": "QUERIES" @@ -1688,9 +1688,6 @@ { "name": "getErrorLogger" }, - { - "name": "getFirstParentNative" - }, { "name": "getFirstTemplatePass" }, @@ -1854,7 +1851,7 @@ "name": "getPlayerContext" }, { - "name": "getPluralCategory$1" + "name": "getPluralCategory" }, { "name": "getPointers" diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts index cc2bcb9f83..610911c96d 100644 --- a/packages/core/test/render3/i18n_spec.ts +++ b/packages/core/test/render3/i18n_spec.ts @@ -6,16 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {noop} from '../../../compiler/src/render3/view/util'; +import {NgForOfContext} from '@angular/common'; import {Component as _Component} from '../../src/core'; import {defineComponent} from '../../src/render3/definition'; -import {getTranslationForTemplate, i18n, i18nApply, i18nAttributes, i18nEnd, i18nExp, i18nIcuReplaceVars, i18nStart} from '../../src/render3/i18n'; +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 {RenderFlags} from '../../src/render3/interfaces/definition'; -import {getNativeByIndex} from '../../src/render3/util'; -import {NgIf} from './common_with_def'; -import {element, elementEnd, elementStart, template, text, bind, elementProperty, projectionDef, projection} from '../../src/render3/instructions'; -import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n'; -import {HEADER_OFFSET, LViewData, TVIEW} from '../../src/render3/interfaces/view'; +import {NgForOf} from './common_with_def'; import {ComponentFixture, TemplateFixture} from './render_util'; const Component: typeof _Component = function(...args: any[]): any { @@ -24,1486 +21,1786 @@ const Component: typeof _Component = function(...args: any[]): any { } as any; describe('Runtime i18n', () => { - describe('getTranslationForTemplate', () => { - it('should crop messages for the selected template', () => { - let message = `simple text`; - expect(getTranslationForTemplate(message)).toEqual(message); + 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: + //
+ // + // + // + // + // + //
- message = `Hello �0�!`; - 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 �#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'); + elementStart(0, 'div'); + { // Start of translated section 1 + // - i18n sections do not contain any text() instruction + elementStart(1, 'a'); // START_A { - i18nStart(1, MSG_DIV); - { - element(2, 'div'); // Will be removed - element(3, 'span'); + element(2, 'b'); // START_B + element(3, 'remove-me'); // START_REMOVE_ME + } + elementEnd(); + element(4, 'c'); // START_C + } // End of translated section 1 + elementEnd(); + i18nApply(1, i18n_1[0]); + } + + const fixture = new TemplateFixture(createTemplate, () => {}, 5); + expect(fixture.html).toEqual(''); + }); + + it('should support expressions', () => { + const MSG_DIV_SECTION_1 = `start {$EXP_2} middle {$EXP_1} end`; + let i18n_1: I18nInstruction[][]; + + 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)); } - i18nEnd(); } - elementEnd(); - elementStart(4, 'div'); - { text(5, '!'); } - elementEnd(); - }, null, 6); + }); + } - expect(fixture.html).toEqual('
Hello world
!
'); - }); + const fixture = new ComponentFixture(MyApp); + expect(fixture.html).toEqual('
start 2 middle 1 end
'); - 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�!`; + // Change detection cycle, no model changes + fixture.update(); + expect(fixture.html).toEqual('
start 2 middle 1 end
'); - 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(); + // 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)); - } - } + }); + } - 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('
message
'); - 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(); + // 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)); + } + } + }); + } + + 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 + } elementEnd(); } + elementEnd(); + i18nApply(2, i18n_1[0]); + i18nApply(5, i18n_2[0]); + } + 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
'); + + // Change detection cycle, no model changes + fixture.update(); + expect(fixture.html) + .toEqual('
trad 1hellotrad
'); + + // Change the expressions + fixture.component.exp1 = 'expr 1'; + fixture.component.exp2 = 'expr 2'; + fixture.update(); + expect(fixture.html) + .toEqual( + '
trad expr 1hellotrad
'); + }); + + 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[][]; + + class MyApp { + exp1 = '1'; + + 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) => { + if (rf & RenderFlags.Create) { + if (!i18n_1) { + i18n_1 = i18nMapping(MSG_DIV_SECTION_1, null, [{'EXP_1': 0}]); + } + + text(0, 'before ('); + container(1); + text(2, ') after'); + } if (rf & RenderFlags.Update) { - elementProperty(2, 'ngIf', true); + 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(); } } }); } const fixture = new ComponentFixture(MyApp); - expect(fixture.html) - .toEqual('
Content:
beforemiddleafter
!
'); - }); - - it('for ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {no emails!} - =1 {one email} - other {�0� emails} - }`; - const fixture = prepareFixture(() => { - elementStart(0, 'div'); - i18n(1, MSG_DIV); - elementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
'); - }); - - it('for nested ICU expressions', () => { - const MSG_DIV = `{�0�, plural, - =0 {zero} - other {�0� {�1�, select, - cat {cats} - dog {dogs} - other {animals} - }!} - }`; - const fixture = prepareFixture(() => { - elementStart(0, 'div'); - i18n(1, MSG_DIV); - elementEnd(); - }, null, 2); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
'); - }); - }); - - describe(`i18nAttribute`, () => { - it('for text', () => { - const MSG_title = `Hello world!`; - const MSG_div_attr = ['title', MSG_title]; - const nbConsts = 2; - const index = 1; - const fixture = prepareFixture(() => { - elementStart(0, 'div'); - i18nAttributes(index, MSG_div_attr); - elementEnd(); - }, null, nbConsts, index); - const tView = fixture.hostView[TVIEW]; - const opCodes = tView.data[index + HEADER_OFFSET] as I18nUpdateOpCodes; - - expect(opCodes).toEqual([]); - expect((getNativeByIndex(0, fixture.hostView as LViewData) as any as Element) - .getAttribute('title')) - .toEqual(MSG_title); - }); - - it('for simple bindings', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const nbConsts = 2; - const index = 1; - const opCodes = - getOpCodes(() => { i18nAttributes(index, MSG_div_attr); }, null, nbConsts, index); - - expect(opCodes).toEqual([ - 0b1, // bindings mask - 6, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null - ]); - }); - - it('for multiple bindings', () => { - const MSG_title = `Hello �0� and �1�, again �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const nbConsts = 2; - const index = 1; - const opCodes = - getOpCodes(() => { i18nAttributes(index, MSG_div_attr); }, null, nbConsts, index); - - expect(opCodes).toEqual([ - 0b11, // bindings mask - 10, // size - 'Hello ', -1, ' and ', -2, ', again ', -1, '!', - (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null - ]); - }); - - it('for multiple attributes', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title]; - const nbConsts = 2; - const index = 1; - const opCodes = - getOpCodes(() => { i18nAttributes(index, MSG_div_attr); }, null, nbConsts, index); - - expect(opCodes).toEqual([ - 0b1, // bindings mask - 6, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'title', null, - 0b1, // bindings mask - 6, // if no update, skip 4 - 'Hello ', - -1, // binding index - '!', (index - 1) << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.Attr, 'aria-label', null - ]); - }); - }); - - describe(`i18nExp & i18nApply`, () => { - it('for text bindings', () => { - const MSG_DIV = `Hello �0�!`; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - elementStart(0, 'div'); - i18n(1, MSG_DIV); - elementEnd(); - }, - () => { - i18nExp(bind(ctx.value)); - i18nApply(1); - }, - 2, 1); - - // Template should be empty because there is no update template function - expect(fixture.html).toEqual('
Hello world!
'); - }); - - it('for attribute bindings', () => { - const MSG_title = `Hello �0�!`; - const MSG_div_attr = ['title', MSG_title]; - const ctx = {value: 'world'}; - - const fixture = prepareFixture( - () => { - elementStart(0, 'div'); - i18nAttributes(1, MSG_div_attr); - elementEnd(); - }, - () => { - i18nExp(bind(ctx.value)); - i18nApply(1); - }, - 2, 1); - - expect(fixture.html).toEqual('
'); + expect(fixture.html).toEqual('before (valeur: 1) after'); // Change detection cycle, no model changes fixture.update(); - expect(fixture.html).toEqual('
'); - - ctx.value = 'universe'; - fixture.update(); - expect(fixture.html).toEqual('
'); + expect(fixture.html).toEqual('before (valeur: 1) after'); }); - 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 - // + 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: + //
+ // + // + // + // + // + // //
- 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�`; + // 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)); + } + } class MyApp { - exp1 = '1'; - exp2 = '2'; + items: string[] = ['1', '2']; static ngComponentDef = defineComponent({ type: MyApp, - selectors: [['my-app']], factory: () => new MyApp(), - consts: 10, - vars: 2, - template: (rf: RenderFlags, ctx: 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) { - elementStart(0, 'div'); + 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'); { - 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(); + // 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) { - i18nExp(bind(ctx.exp1)); - i18nApply(2); - i18nExp(bind(ctx.exp1)); - i18nExp(bind(ctx.exp2)); - i18nApply(5); + 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( - `
trad 1hellotrad
`); + '
  • 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 attribute translations on removed elements', () => { - // Translated template: - //
- // trad {{exp1}} - //
+ 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[][]; - const MSG_DIV_1 = `trad �0�`; - const MSG_DIV_1_ATTR_1 = ['title', `start �1� middle �0� end`]; + 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 { - exp1 = '1'; - exp2 = '2'; + items: string[] = ['1', '2']; static ngComponentDef = defineComponent({ type: MyApp, - selectors: [['my-app']], factory: () => new MyApp(), - consts: 5, - vars: 5, - template: (rf: RenderFlags, ctx: 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) { - elementStart(0, 'div'); + 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'); { - i18nAttributes(1, MSG_DIV_1_ATTR_1); - i18nStart(2, MSG_DIV_1); - { - elementStart(3, 'b'); // Will be removed - { i18nAttributes(4, MSG_DIV_1_ATTR_1); } - elementEnd(); - } - i18nEnd(); + // 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) { - 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); + 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) => { + 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 + { + elementStart(2, 'b'); // START_B + { + text(3); // EXP_1 + element(4, 'remove-me-1'); // START_REMOVE_ME_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 + } + elementEnd(); + i18nApply(1, i18n_1[0]); + } + if (rf & RenderFlags.Update) { + elementProperty(2, 'title', i18nInterpolation1(i18n_2, cmp.name)); + textBinding(3, bind(cmp.name)); + } + } + }); + } + + const fixture = new ComponentFixture(Parent); + expect(fixture.html) + .toEqual( + '

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 {$EXP_1}`; + 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: 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'); + { + element(2, 'any'); + elementStart(3, 'b'); + { + // Start of translated section 1 + text(4); // EXP_1 + // End of translated section 1 + } + 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}]); + } + + 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!
'); + }); + + it('should project translations with selectors', () => { + @Component({ + selector: 'child', + template: ` + + ` + }) + class Child { + static ngComponentDef = defineComponent({ + type: Child, + selectors: [['child']], + factory: () => new Child(), + consts: 1, + vars: 0, + template: (rf: RenderFlags, cmp: Child) => { + if (rf & RenderFlags.Create) { + projectionDef([[['span']]], ['span']); + projection(0, 1); + } + } + }); + } + + const MSG_DIV_SECTION_1 = `{$START_SPAN_0}Contenu{$END_SPAN_0}`; + let i18n_1: I18nInstruction[][]; + + @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}]); + } + + 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(fixture.html).toEqual(`
trad 1
`); + expect(interpolation).toBeDefined(); + expect(interpolation).toEqual(interpolationV); }); - 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(); - } - } - }); - } + 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}); - const MSG_DIV_SECTION_1 = `�#2�Je suis projeté depuis �#3��0��/#3��/#2�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; + 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: + //
- @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); - } + //
+ template: (rf: RenderFlags, ctx: MyApp) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); // translated section 1 } - }); - } - - 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(); - } + if (rf & RenderFlags.Update) { + elementProperty(0, 'title', i18nInterpolation3(i18n_1, ctx.exp1, ctx.exp2, ctx.exp3)); } - }); - } + } + }); + } - const MSG_DIV_SECTION_1 = `Je suis projeté depuis �0�`; - const MSG_ATTR_1 = ['title', `Enfant de �0�`]; - - @Component({ - selector: 'parent', - template: ` -
- - - I am projected from {{name}} - - -
` - // Translated to: - //
- // - // - // Je suis projeté depuis {{name}} - // - // - //
- }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = defineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 7, - vars: 2, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'div'); - { - elementStart(1, 'child'); - { - element(2, 'any'); - elementStart(3, 'b'); - { - i18nAttributes(4, MSG_ATTR_1); - i18n(5, MSG_DIV_SECTION_1); - } - elementEnd(); - element(6, 'any'); - } - elementEnd(); - } - elementEnd(); - } - if (rf & RenderFlags.Update) { - i18nExp(bind(cmp.name)); - i18nApply(4); - i18nExp(bind(cmp.name)); - i18nApply(5); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual( - '

Je suis projeté depuis Parent

'); - }); - - it('should re-project translations when multiple projections', () => { - @Component({selector: 'grand-child', template: '
'}) - class GrandChild { - static ngComponentDef = defineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - projectionDef(); - elementStart(0, 'div'); - { projection(1); } - elementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = defineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - projectionDef(); - elementStart(0, 'grand-child'); - { projection(1); } - elementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Bonjour�/#2� Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
Bonjour Monde!
- }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = defineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'child'); - { - i18nStart(1, MSG_DIV_SECTION_1); - { element(2, 'b'); } - i18nEnd(); - } - elementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
Bonjour Monde!
'); - //
Bonjour
- //
Bonjour Monde!
- }); - - xit('should re-project translations when removed placeholders', () => { - @Component({selector: 'grand-child', template: '
'}) - class GrandChild { - static ngComponentDef = defineComponent({ - type: GrandChild, - selectors: [['grand-child']], - factory: () => new GrandChild(), - consts: 3, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - projectionDef(); - elementStart(0, 'div'); - { projection(1); } - elementEnd(); - } - } - }); - } - - @Component( - {selector: 'child', template: ''}) - class Child { - static ngComponentDef = defineComponent({ - type: Child, - selectors: [['child']], - directives: [GrandChild], - factory: () => new Child(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - projectionDef(); - elementStart(0, 'grand-child'); - { projection(1); } - elementEnd(); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `Bonjour Monde!`; - - @Component({ - selector: 'parent', - template: `Hello World!` - // Translated to: - //
Bonjour Monde!
- }) - class Parent { - name: string = 'Parent'; - static ngComponentDef = defineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 2, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'child'); - { - i18nStart(1, MSG_DIV_SECTION_1); - { - element(2, 'b'); // will be removed - } - i18nEnd(); - } - elementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html) - .toEqual('
Bonjour Monde!
'); - }); - - it('should project translations with selectors', () => { - @Component({ - selector: 'child', - template: ` - - ` - }) - class Child { - static ngComponentDef = defineComponent({ - type: Child, - selectors: [['child']], - factory: () => new Child(), - consts: 1, - vars: 0, - template: (rf: RenderFlags, cmp: Child) => { - if (rf & RenderFlags.Create) { - projectionDef([[['span']]], ['span']); - projection(0, 1); - } - } - }); - } - - const MSG_DIV_SECTION_1 = `�#2�Contenu�/#2�`; - - @Component({ - selector: 'parent', - template: ` - - - - - ` - // Translated to: - // Contenu - }) - class Parent { - static ngComponentDef = defineComponent({ - type: Parent, - selectors: [['parent']], - directives: [Child], - factory: () => new Parent(), - consts: 4, - vars: 0, - template: (rf: RenderFlags, cmp: Parent) => { - if (rf & RenderFlags.Create) { - elementStart(0, 'child'); - { - i18nStart(1, MSG_DIV_SECTION_1); - { - element(2, 'span', ['title', 'keepMe']); - element(3, 'span', ['title', 'deleteMe']); - } - i18nEnd(); - } - elementEnd(); - } - } - }); - } - - const fixture = new ComponentFixture(Parent); - expect(fixture.html).toEqual('Contenu'); - }); + 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 + }); + + 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('
'); + }); + }); });