From 7d8205311730bd0ebf8c28684ba6f315c42ce86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Mon, 24 Sep 2018 14:47:56 -0700 Subject: [PATCH] docs(ivy): i18n design (#26091) PR Close #26091 --- packages/core/src/render3/i18n.md | 1203 ++++++++++++++++++ packages/core/src/render3/interfaces/i18n.ts | 364 ++++++ 2 files changed, 1567 insertions(+) create mode 100644 packages/core/src/render3/i18n.md create mode 100644 packages/core/src/render3/interfaces/i18n.ts diff --git a/packages/core/src/render3/i18n.md b/packages/core/src/render3/i18n.md new file mode 100644 index 0000000000..d5b93aa4a6 --- /dev/null +++ b/packages/core/src/render3/i18n.md @@ -0,0 +1,1203 @@ +# I18N + +## Example of i18n message + +Given an i18n component: +```typescript +@Component({ + template: ` +
+ {{count}} is rendered as: + + { count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } + . +
+ ` +}) +class MyComponent { +} +``` +NOTE: +- There really is only two kinds of i18n text. + 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. + +## I18n Requirements +The translation: +- Must preserve DOM structure in i18n blocks because those DOM structures may have components and directives. +- The parsed instructions must stay in `TView.data`. + This is because in case of SSR we need to be able to execute multiple locales in the same VM. + (If instructions would be at a top level we could not have more than one parsed instruction.) + The plan is to cache `TView.data` per locale, hence different instructions would get cached into different `TView.data` associated with a given locale. + - NOTE: in SSR `goog.getMsg` will return an object literal of all of the locale translations. + + + +The compiler generates: +```typescript +// These messages need to be retrieved from some localization service described later +const MSG_title = 'Hello �0�!'; +const MSG_div_attr = ['title', MSG_title]; +const MSG_div = `�0� is rendered as: �*3:1��#1:1�{�0:1�, plural, + =0 {no emails!} + =1 {one email} + other {�0:1� emails} +}�/#1:1��/*3:1�.`; + +function MyComponent_NgIf_Template_0(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_div, 1); + element(1, 'b'); + i18nEnd(); + } + if (rf & RenderFlags.Update) { + i18nExp(bind(ctx.count)); // referenced by `�0:1�` + i18nApply(0); + } +} + +class MyComponent { + static ngComponentDef = defineComponent({ + ..., + template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + i18nAttributes(1, MSG_div_attr); + i18nStart(2, MSG_div); + template(3, MyComponent_NgIf_Template_0, ...); + i18nEnd(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + i18nExp(bind(ctx.count)); // referenced by `�0�` + i18nApply(1); // Updates the `i18n-title` binding + i18nExp(bind(ctx.count)); // referenced by `�0�` + i18nApply(2); // Updates the `
...
` + } + } + }); +} +``` +The translated message can contain i18n placeholders (denoted by `�...�`) which tell the translation how to interpolate the text. +The [�](https://www.fileformat.info/info/unicode/char/fffd/index.htm) character was chosen because it is extremely unlikely to collide with existing text, and because it is generated, the developer should never encounter it. +Each i18n placeholder contains a number (and sub-template index) which provide binding information for the placeholder. +The i18n placeholders are: +- `�{index}(:{block})�`: *Binding Place Holder*: + Marks a location where an expression will be interpolated into. + The place holder `index` points to the expression binding index. + On optional `block` that matches the sub-template in which it was declared. +- `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *Element Place Holder*: + Marks the beginning and end of DOM element that were embedded in the original translation block. + The place holder `index` points to the element index in the template instructions set. + On optional `block` that matches the sub-template in which it was declared. +- `�*{index}:{block}�`/`�/*{index}:{block}�`: *Sub-template Place Holder*: + 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. + +No other place holder format is supported. + +NOTE: +- Notice that the closing placeholder has the same information as the opening placeholder. + This is so that the parser can verify that opening and closing placeholders are properly nested. + Failure to properly nest the placeholders implies that the translator change the order of translation incorrectly and should be a runtime error. +- Note that the `block` id may be added to non-root templates. + Block must be properly nested. + It is an error for the translator to move a placeholder outside of its block, and will result in runtime error. +- Notice that all placeholders are globally unique within the translation string. + +## Accumulator + +For concatenating strings we use an accumulator. +This is best explained through pseudo code: + +```typescript +const accumulator:string[] = []; + +/** + * Collect intermediate interpolation values. + */ +function accumulate(value: string|number): void { + if (typeof value == 'number') { + // if the value is a number then look it up in previous `i18nBind` location. + value = lviewData[bindIndex + value]; + } + accumulator.push(stringify(value)); +} + +/** + * Flush final interpolation value. + */ +function accumulatorFlush(sanitizer: null|((text: string)=>string) = null): string { + let interpolation = accumulator.join(''); + if (sanitizer != null) { + interpolation = sanitizer(interpolation); + } + accumulator.length = 0; + return interpolation; +} +``` + +## i18n Attributes + +Let's look at the simpler case of i18n and attribute interpolation. + +```typescript +// These messages need to be retrieved from some localization service described later +const MSG_title = 'Hello �0�!'; +``` +Next notice the `i18nAttributes` instruction inside the `RenderFlags.Create` block. + +```typescript +const MSG_title = 'Hello �0�!'; +const MSG_div_attr = ['title', MSG_title]; +i18nAttributes(1, MSG_div_attr); +``` +The above instruction checks the `TView.data` cache at position `1` and if empty will create `I18nUpdateOpCodes` like so: +```typescript +[ + // The following OpCodes represent: `
` + // If `changeMask & 0b11` + // has changed then execute update OpCodes. + // has NOT changed then skip `7` values and start processing next OpCodes. + 0b1, 7, + // Concatenate `newValue = 'Hello ' + lViewData[bindIndex-1] + '!';`. + 'Hello ', // accumulate('Hello '); + -1, // accumulate(-1); + '!', // accumulate('!'); + // Update attribute: `elementAttribute(1, 'title', accumulatorFlush(null));` + // NOTE: `null` means don't sanitize + 1 << SHIFT_REF | Attr, 'title', null, +] +``` +NOTE: +- 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. + +## i18n Elements + +Generating text inside existing elements is a bit more complicated but follows the same philosophy as attributes. + +First we define the message and mapping (which placeholders map to which expressions) as so: + +```typescript +// These messages need to be retrieved from some localization service described later +const MSG_div = `�0� is rendered as: �*3:1��#1:1�{�0:1�, plural, + =0 {no emails!} + =1 {one email} + other {�0:1� emails} +}�/#1:1��/*3:1�.`; +``` + +### Exclusion Zones / Sub-Templates + +Most i18n translations do not have sub-templates. +For the rare case where a translation has a sub-template the sub-array describes an exclusion zone defined by `�*{index}:{block}�` and `�/*{index}:{block}�` marker. +The exclusion zone is removed from the translation so in our case it is as if we had two separate translations for parent and sub-template. + +Given nested template: +```html +
+ List: +
    +
  • item
  • +
+ Summary: + +
+``` + +will generate +```typescript +// Text broken down to allow addition of comments (Generated code will not have comments) +const MSG_div = + 'List: ' + '�*2:1�' + // template(2, MyComponent_NgIf_Template_0, ...); + '�#1:1�' + // elementStart(1, 'ul'); + '�*2:2�' + // template(2, MyComponent_NgIf_NgFor_Template_1, ...); + '�#1:2�' + // element(1, 'li'); + 'item' + + '�/#1:2�' + + '�/*2:2�' + + '�/#1:1�' + + '�*2:1�' + + 'Summary: ' + + '�*3:3�' + // template(3, MyComponent_NgIf_Template_2, ...); + '�#1:3�' + // element(1, 'span'); + '�#1:3�' + + '�*3:3�'; + +function MyComponent_NgIf_NgFor_Template_1(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_div, 2); // 2nd `*` content: `�#1:2�item�/#1:2�` + element(1, 'li'); + i18nEnd(); + } + ... +} + +function MyComponent_NgIf_Template_0(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_div, 1); // 1st `*` content: `�#1:1��*2:2��/*2:2��/#1:1�` + elementStart(1, 'ul'); + template(2, MyComponent_NgIf_NgFor_Template_1, ...); + elementEnd(); + i18nEnd(); + } + ... +} + +function MyComponent_NgIf_Template_2(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_div, 3); // 3rd `*` content: `�#1:3��/#1:3�` + element(1, 'span'); + i18nEnd(); + } + ... +} + +class MyComponent { + static ngComponentDef = defineComponent({ + ..., + template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); // Outer content: `List : �*2:1��/*2:1�Summary: �*3:3��/*3:3�` + i18nStart(1, MSG_div); + template(2, MyComponent_NgIf_Template_0, ...); + template(3, MyComponent_NgIf_Template_2, ...); + i18nEnd(); + elementEnd(); + } + ... + } + }); +} +``` + +### `i18nStart` + +It is the job of the instruction `i18nStart` to parse the messages and to fill in the translation blocks with text. +(Notice that in i18n-block the DOM element instructions are retained, but the text instructions have been stripped.) + +```typescript +i18nStart( + 2, // storage of the parsed message instructions + MSG_div, // The message to parse which has been translated + // Optional sub-template index. Empty implies `0` (most common) +); +... +i18nEnd(); // The instruction which is responsible for inserting text nodes into + // the render tree based on translation. +``` + +The `i18nStart` generates these instructions which are cached in the `TView` and then processed by `i18nEnd`. + +```typescript +{ + vars: 2, // Number of slots to allocate in EXPANDO. + expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 + create: [ // Processed by `i18nEnd` + // Equivalent to: + // // Assume expandoIndex = 100; + // const node = lViewData[expandoIndex++] = document.createTextNode(''); + // lViewData[2].insertBefore(node, lViewData[3]); + "", 2 << SHIFT_PARENT | 3 << SHIFT_REF | InsertBefore, + // Equivalent to: + // // Assume expandoIndex = 101; + // const node = lViewData[expandoIndex++] = document.createTextNode('.'); + // lViewData[0].appendChild(node); + '.', 2 << SHIFT_PARENT | AppendChild, + ], + update: [ // Processed by `i18nApply` + // Header which consists of change mask and block size. + // If `changeMask & 0b1` + // has changed then execute update OpCodes. + // has NOT changed then skip `3` values and start processing next OpCodes. + 0b1, 3, + -1, // accumulate(-1); + 'is rendered as: ', // accumulate('is rendered as: '); + // Flush the concatenated string to text node at position 100. + 100 << SHIFT_REF | Text, // lViewData[100].textContent = accumulatorFlush(); + ], + icus: null, +} +``` + +NOTE: + - position `2` has `i18nStart` and so it is not a real DOM element, but it should act as if it was a DOM element. + +### `i18nStart` in sub-template + +```typescript +i18nStart( + 0, // storage of the parsed message instructions + MSG_div, // The message to parse which has been translated + 1 // Optional sub-template (block) index. +); +``` + +Notice that in sub-template the `i18nStart` instruction takes `1` as the last argument. +This means that the instruction has to extract out 1st sub-block from the root-template translation. + +Starting with +```typescript +// These messages need to be retrieved from some localization service described later +const MSG_div = `�0� is rendered as: �*3:1��#1:1�{�0:1�, plural, + =0 {no emails!} + =1 {one email} + other {�0:1� emails} +}�/#1:1��/*3:1�.`; +``` + +The `i18nStart` instruction traverses `MSG_div` and looks for 1st sub-template marked with `�*3:1�`. +Notice that the `�*3:1�` contains index to the DOM element `3`. +The rest of the code should work same as described above. + +This case is more complex because it contains an ICU. +ICUs are pre-parsed and then stored in the `TVIEW.data` as follows. + +```typescript +{ + 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: [ + // Equivalent to: + // // Assume expandoIndex = 200; + // const node = lViewData[expandoIndex++] = document.createComment(''); + // lViewData[1].appendChild(node); + COMMENT_MARKER, '', 1 << SHIFT_PARENT | AppendChild, + ], + update: [ + // The following OpCodes represent: `{count, plural, ... }">` + // If `changeMask & 0b1` + // has changed then execute update OpCodes. + // has NOT changed then skip `2` values and start processing next OpCodes. + 0b1, 2, + -1, // accumulate(-1); + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, + + // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. + 0b1, 1, + // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/);` + // SHIFT_ICU: points to: `i18nStart(0, MSG_div, 1);` + // SHIFT_REF: is an index into which ICU is being updated. In our example we only have + // one ICU so it is 0-th ICU to update. + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, + ], + icus: [ + { + cases: [0, 1, 'other'], + vars: [4, 3, 3], + expandoStartIndex: 203, // Assume in this example EXPANDO starts at 203 + childIcus: [], + create: [ + // Case: `0`: `{no emails!}` + [ + // // assume expandoIndex == 203 + // const node = lViewData[expandoIndex++] = document.createTextNode('no '); + // lViewData[1].appendChild(node); + 'no ', 1 << SHIFT_PARENT | AppendChild, + // Equivalent to: + // // assume expandoIndex == 204 + // const node = lViewData[expandoIndex++] = document.createElement('b'); + // lViewData[1].appendChild(node); + ELEMENT_MARKER, 'b', 1 << SHIFT_PARENT | AppendChild, + // const node = lViewData[204]; + // node.setAttribute('title', 'none'); + 204 << SHIFT_REF | Select, 'title', 'none' + // // assume expandoIndex == 205 + // const node = lViewData[expandoIndex++] = document.createTextNode('email'); + // lViewData[1].appendChild(node); + 'email', 204 << SHIFT_PARENT | AppendChild, + ] + // Case: `1`: `{one email}` + [ + // // assume expandoIndex == 203 + // const node = lViewData[expandoIndex++] = document.createTextNode('no '); + // lViewData[1].appendChild(node, lViewData[2]); + 'one ', 1 << SHIFT_PARENT | AppendChild, + // Equivalent to: + // // assume expandoIndex == 204 + // const node = lViewData[expandoIndex++] = document.createElement('b'); + // lViewData[1].appendChild(node); + ELEMENT_MARKER, 'i', 1 << SHIFT_PARENT | AppendChild, + // // assume expandoIndex == 205 + // const node = lViewData[expandoIndex++] = document.createTextNode('email'); + // lViewData[1].appendChild(node); + 'email', 204 << SHIFT_PARENT | AppendChild, + ] + // Case: `"other"`: `{�0� emails}` + [ + // // assume expandoIndex == 203 + // const node = lViewData[expandoIndex++] = document.createTextNode(''); + // lViewData[1].appendChild(node); + '', 1 << SHIFT_PARENT | AppendChild, + // Equivalent to: + // // assume expandoIndex == 204 + // const node = lViewData[expandoIndex++] = document.createComment('span'); + // lViewData[1].appendChild(node); + ELEMENT_MARKER, 'span', 1 << SHIFT_PARENT | AppendChild, + // // assume expandoIndex == 205 + // const node = lViewData[expandoIndex++] = document.createTextNode('emails'); + // lViewData[1].appendChild(node); + 'emails', 204 << SHIFT_PARENT | AppendChild, + ] + ], + remove: [ + // Case: `0`: `{no emails!}` + [ + // lViewData[1].remove(lViewData[203]); + 1 << SHIFT_PARENT | 203 << SHIFT_REF | Remove, + // lViewData[1].remove(lViewData[204]); + 1 << SHIFT_PARENT | 204 << SHIFT_REF | Remove, + ] + // Case: `1`: `{one email}` + [ + // lViewData[1].remove(lViewData[203]); + 1 << SHIFT_PARENT | 203 << SHIFT_REF | Remove, + // lViewData[1].remove(lViewData[204]); + 1 << SHIFT_PARENT | 204 << SHIFT_REF | Remove, + ] + // Case: `"other"`: `{�0� emails}` + [ + // lViewData[1].remove(lViewData[203]); + 1 << SHIFT_PARENT | 203 << SHIFT_REF | Remove, + // lViewData[1].remove(lViewData[204]); + 1 << SHIFT_PARENT | 204 << SHIFT_REF | Remove, + ] + ], + update: [ + // Case: `0`: `{no emails!}` + [ + // no bindings + ] + // Case: `1`: `{one email}` + [ + // no bindings + ] + // Case: `"other"`: `{�0� emails}` + [ + // If `changeMask & 0b1` + // has changed then execute update OpCodes. + // has NOT changed then skip `5` values and start processing next OpCodes. + 0b1, 5, + -1, // accumulate(-1); + ' ', // accumulate(' '); + // Update attribute: `lviewData[203].textValue = accumulatorFlush();` + 203 << SHIFT_REF | Text, + // If `changeMask & 0b1` + // has changed then execute update OpCodes. + // has NOT changed then skip `4` values and start processing next OpCodes. + 0b1, 4, + // Concatenate `newValue = '' + lViewData[bindIndex -1];`. + -1, // accumulate(-1); + // Update attribute: `lViewData[204].setAttribute(204, 'title', 0b1, 2,(null));` + // NOTE: `null` implies no sanitization. + 204 << SHIFT_REF | Attr, 'title', null + ] + ] + } + ] +} +``` + +## Sanitization + +Any text coming from translators is considered safe and has no sanitization applied to it. +(This is why create blocks don't need sanitization) +Any text coming from user (interpolation of bindings to attributes) are consider unsafe and may need to be passed through sanitizer if the attribute is considered dangerous. +For this reason the update OpCodes of attributes take sanitization function as part of the attribute update. +If the sanitization function is present then we pass the interpolated value to the sanitization function before assigning the result to the attribute. +During the parsing of the translated text the parser determines if the attribute is potentially dangerous and if it contains user interpolation, if so it adds an appropriate sanitization function. + +## Computing the expando positions + +Assume we have translation like so: `
Hello {{name}}!
`. +The above calls generates the following template instruction code: + +```typescript +// These messages need to be retrieved from some localization service described later +const MSG_div = 'Hello �0�!'; + +template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + i18nStart(1, MSG_div); + i18nEnd(); + elementEnd(); + } + if (rf & RenderFlags.Update) { + i18nExp(bind(ctx.count)); + i18nApply(1); + } +} +``` + +This requires that the `i18nStart` instruction generates the OpCodes for creation as well as update. +The OpCodes require that offsets for the EXPANDO index for the reference. +The question is how do we compute this: + +```typescript +{ + vars: 1, + expandoStartIndex: 100, // Retrieved from `tView.blueprint.length` at i18nStart invocation. + create: [ + // let expandoIndex = this.expandoStartIndex; // Initialize + + // const node = document.createTextNode(''); + // if (first_execution_for_tview) { + // ngDevMode && assertEquals(tView.blueprint.length, expandoIndex); + // tView.blueprint.push(null); + // ngDevMode && assertEquals(lViewData.length, expandoIndex); + // lViewData.push(node); + // } else { + // lViewData[expandoIndex] = node; // save expandoIndex == 100; + // } + // lViewData[0].appendChild(node); + // expandoIndex++; + "", 0 << SHIFT_PARENT | AppendChild, + ], + update: [ + 0b1, 3, + 'Hello ', -1, '!', + // The `100` position refers to empty text node created above. + 100 << SHIFT_REF | Text, + ], +} +``` + +## ICU in attributes bindings + +Given an i18n component: +```typescript +@Component({ + template: ` +
+
+ ` +}) +class MyComponent { +} +``` + +The compiler generates: +```typescript +// These messages need to be retrieved from some localization service described later +const MSG_title = `You have {�0�, plural, + =0 {no emails} + =1 {one email} + other {�0� emails} + }.`; +const MSG_div_attr = ['title', MSG_title, optionalSanitizerFn]; + +class MyComponent { + static ngComponentDef = defineComponent({ + ..., + template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div'); + i18nAttributes(1, MSG_div_attr); + elementEnd(); + } + if (rf & RenderFlags.Update) { + i18nExp(bind(ctx.count)); // referenced by `�0�` + i18nApply(1); // Updates the `i18n-title` binding + } + } + }); +} +``` + +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 +{ + 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: [ + // attributes have no create block + ], + update: [ + // If `changeMask & 0b1` + // has changed then execute update OpCodes. + // has NOT changed then skip `2` values and start processing next OpCodes. + 0b1, 2, + -1, // accumulate(-1) + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, + + // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. + 0b1, 4, + 'You have ', // accumulate('You have '); + + // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/);` + // SHIFT_ICU: points to: `i18nStart(0, MSG_div, 1);` + // SHIFT_REF: is an index into which ICU is being updated. In our example we only have + // one ICU so it is 0-th ICU to update. + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, + + '.', // accumulate('.'); + + // Update attribute: `elementAttribute(1, 'title', accumulatorFlush(null));` + // NOTE: `null` means don't sanitize + 1 << SHIFT_REF | Attr, 'title', null, + ], + icus: [ + { + cases: [0, 1, 'other'], + vars: [0, 0, 0], + expandoStartIndex: 200, // Assume in this example EXPANDO starts at 200 + childIcus: [], + create: [ + // Case: `0`: `{no emails}` + [ ] + // Case: `1`: `{one email}` + [ ] + // Case: `"other"`: `{�0� emails}` + [ ] + ], + remove: [ + // Case: `0`: `{no emails}` + [ ] + // Case: `1`: `{one email}` + [ ] + // Case: `"other"`: `{�0� emails}` + [ ] + ], + update: [ + // Case: `0`: `{no emails}` + [ + // If `changeMask & -1` // always true + // has changed then execute update OpCodes. + // has NOT changed then skip `1` values and start processing next OpCodes. + -1, 1, + 'no emails', // accumulate('no emails'); + ] + // Case: `1`: `{one email}` + [ + // If `changeMask & -1` // always true + // has changed then execute update OpCodes. + // has NOT changed then skip `1` values and start processing next OpCodes. + -1, 1, + 'one email', // accumulate('no emails'); + ] + // Case: `"other"`: `{�0� emails}` + [ + // If `changeMask & -1` // always true + // 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]); + 'emails', // accumulate('no emails'); + ] + ] + } + ] +} +``` + + +## ICU Parsing + +ICUs need to be parsed, and they may contain HTML. +First part of ICU parsing is breaking down the ICU into cases. + +Given +``` +{�0�, plural, + =0 {no emails!} + =1 {one email} + other {�0� emails} +} +``` +The above needs to be parsed into: +```TypeScript +{ + type: 'plural', // or 'select' + expressionBindingIndex: 0, // from �0�, + cases: [ + 'no emails!', + 'one email', + '�0� emails', + ] +} +``` + +Once the ICU is parsed into its components it needs to be translated into OpCodes. The translation from the `case` to OpCode depends on whether the ICU is located in DOM or in attribute. +Attributes OpCode generation is simple since it only requires breaking the string at placeholder boundaries and generating a single attribute update OpCode with interpolation. +(See ICUs and Attributes for discussion of how ICUs get updated with attributes) +The DOM mode is more complicated as it may involve creation of new DOM elements. + +1. Create a temporary `
` element. +2. `innerHTML` the `case` into the `
` element. +3. Walk the `
`: + 1. If Text node create OpCode to create/destroy the text node. + - If Text node with placeholders then also create update OpCode for updating the interpolation. + 2. If Element node create OpCode to create/destroy the element node. + 3. If Element has attributes create OpCode to create the attributes. + - If attribute has placeholders than create update instructions for the attribute. + +The above should generate create, remove, and update OpCodes for each of the case. + +NOTE: The updates to attributes with placeholders require that we go through sanitization. + + + +## Translation without top level element + +Placing `i18n` attribute on an existing elements is easy because the element defines parent and the translated element can be insert synchronously. +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. + +Example: + +```html +Translated text +``` + +Would generate: +```typescript +const MSG_text = 'Translated text'; + +function MyComponent_Template_0(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_text, 1); + i18nEnd(); + } + ... +} +``` + +Which would get parsed into: +```typescript +{ + vars: 2, // Number of slots to allocate in EXPANDO. + expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 + create: [ // Processed by `i18nEnd` + // Equivalent to: + // const node = lViewData[expandoIndex++] = document.createTextNode(''); + // lViewData[0].insertBefore(node, lViewData[3]); + "Translated text", 0 << SHIFT_PARENT | AppendChild, + ], + update: [ ], + icus: null, +} +``` + +RESOLVE: +- One way we could solve it is by `i18nStart` would store an object in `LViewData` at its position which would implement `RNode` but which would handle the corner case of inserting into a synthetic parent. +- Another way this could be implemented is for `i18nStore` to leave a marker in the `LViewData` which would tell the OpCode processor that it is dealing with a synthetic parent. + +## Nested ICUs + +ICU can have other ICUs embedded in them. + +Given: +```typescript +@Component({ + template: ` + {count, plural, + =0 {zero} + other {{{count}} {animal, select, + cat {cats} + dog {dogs} + other {animals} + }! + } + } + ` +}) +class MyComponent { + count: number; + animal: string; +} +``` + +Will generate: +```typescript +const MSG_nested = ` + {�0�, plural, + =0 {zero} + other {�0� {�1�, select, + cat {cats} + dog {dogs} + other {animals} + }! + } + } + `; + +class MyComponent { + count: number; + animal: string; + static ngComponentDef = defineComponent({ + ..., + template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf & RenderFlags.Create) { + i18nStart(0, MSG_nested); + i18nEnd(); + } + if (rf & RenderFlags.Update) { + i18nExp(bind(ctx.count)); // referenced by `�0�` + i18nExp(bind(ctx.animal)); // referenced by `�1�` + i18nApply(0); + } + } + }); +} +``` + +The way to think about is that the sub-ICU is replaced with comment node and then the rest of the system works as normal. +The main ICU writes out the comment node which acts like an anchor for the sub-ICU. +The sub-ICU uses the comment node as a parent and writes its data there. + +NOTE: +- Because more than one ICU is active at the time the system needs to take that into account when allocating the expando instructions. + +The internal data structure will be: +```typescript +{ + vars: 2, // Number of slots to allocate in EXPANDO. + expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 + create: [ // Processed by `i18nEnd` + ], + update: [ // Processed by `i18nApply` + // The following OpCodes represent: `{count, plural, ... }">` + // If `changeMask & 0b1` + // has changed then execute update OpCodes. + // has NOT changed then skip `2` values and start processing next OpCodes. + 0b1, 2, + -1, // accumulate(-1); + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuSwitch, + + // NOTE: the bit mask here is the logical OR of all of the masks in the ICU. + 0b1, 1, + // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 0 /*SHIFT_REF*/);` + // SHIFT_ICU: points to: `i18nStart(0, MSG_div, 1);` + // SHIFT_REF: is an index into which ICU is being updated. In our example we only have + // one ICU so it is 0-th ICU to update. + 0 << SHIFT_ICU | 0 << SHIFT_REF | IcuUpdate, + ], + icus: [ + { // {�0�, plural, =0 {zero} other {�0� }} + cases: [0, 'other'], + childIcus: [[1]], // pointer to child ICUs. Needed to properly clean up. + vars: [1, 2], + expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100 + create: [ + [ // Case: `0`: `{zero}` + 'zero ', 1 << SHIFT_PARENT | AppendChild, // Expando location: 100 + ], + [ // Case: `other`: `{�0� }` + '', 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, + ], + [ // Case: `other`: `{�0� }` + 1 << SHIFT_PARENT | 100 << SHIFT_REF | Remove, + 1 << SHIFT_PARENT | 101 << SHIFT_REF | Remove, + ], + ], + update: [ + [ // Case: `0`: `{zero}` + ], + [ // Case: `other`: `{�0� }` + 0b1, 3, + -2, ' ', 100 << SHIFT_REF | Text // Case: `�0� ` + 0b10, 5, + -1 + // Switch ICU: `icuSwitchCase(lViewData[0 /*SHIFT_ICU*/], 1 /*SHIFT_REF*/, accumulatorFlush());` + 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuSwitch, + + // NOTE: the bit mask here is the logical OR of all of the masks int the ICU. + 0b10, 1, + // Update ICU: `icuUpdateCase(lViewData[0 /*SHIFT_ICU*/], 1 /*SHIFT_REF*/);` + 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuUpdate, + ], + ] + }, + { // {�1�, select, cat {cats} dog {dogs} other {animals} } + cases: ['cat', 'dog', 'other'], + vars: [1, 1, 1], + expandoStartIndex: 102, // Assume in this example EXPANDO starts at 102. (parent ICU 100 + max(1, 2)) + childIcus: [], + create: [ + [ // Case: `cat`: `{cats}` + 'cats', 101 << SHIFT_PARENT | AppendChild, // Expando location: 102; 101 is location of comment/anchor + ], + [ // Case: `doc`: `docs` + 'cats', 101 << SHIFT_PARENT | AppendChild, // Expando location: 102; 101 is location of comment/anchor + ], + [ // Case: `other`: `animals` + 'animals', 101 << SHIFT_PARENT | AppendChild, // Expando location: 102; 101 is location of comment/anchor + ], + ] + remove: [ + [ // Case: `cat`: `{cats}` + 101 << SHIFT_PARENT | 102 << SHIFT_REF | Remove, + ], + [ // Case: `doc`: `docs` + 101 << SHIFT_PARENT | 102 << SHIFT_REF | Remove, + ], + [ // Case: `other`: `animals` + 101 << SHIFT_PARENT | 102 << SHIFT_REF | Remove, + ], + ], + update: [ + [ // Case: `cat`: `{cats}` + ], + [ // Case: `doc`: `docs` + ], + [ // Case: `other`: `animals` + ], + ] + } + ], +} +``` + + + + +# Translation Message Retrieval + +The generated code needs work with: +- Closure: This requires that the translation string is retrieved using `goog.getMsg`. +- Non-closure: This requires the use of Angular service to retrieve the translation string. +- Server Side: All translations need to be retrieved so that one server VM can respond to all locales. + +The solution is to take advantage of compile time constants like so: +```typescript +import {localize} from '@angular/core'; + +let MSG_hello; +if (CLOSURE) { + /** + * @desc extracted description goes here. + */ + const MSG_hello_ = goog.getMsg('Hello World!'); + MSG_hello = MSG_hello_; +} else { + // This would work in non-closure mode, and can work for both browser and SSR use case. + MSG_hello = localize('31451231531' /** representing 'Hello World!' message id*/); +} +const MSG_div_attr = ['title', MSG_hello]; + +class MyComponent { + static ngComponentDef = defineComponent({ + ..., + template: function(rf: RenderFlags, ctx: MyComponent) { + if (rf & RenderFlags.Create) { + i18nAttributes(1, MSG_hello); + i18nEnd(); + } + } + }); +} +``` + +NOTE: +- The compile time constant is important because when the generated code is shipped to NPM it must contain all formats, because at the time of packaging it is not known how the final application will be bundled. +- Alternatively because we already ship different source code for closure we could generated different code for closure folder. + + +## `goog.getMsg()` + +An important goal is to interpolate seamlessly with [`goog.getMsg()`](https://github.com/google/closure-library/blob/db35cf1524b36a50d021fb6cf47271687cc2ea33/closure/goog/base.js#L1970-L1998). +When `goog.getMsg` gets a translation it treats `{$some_text}` special by generating `..` tags in `.xmb` file. +```typescript +/** + * @desc Greeting. + */ +const MSG = goog.getMsg('Hello {$name}!', { + name: 'world' +}); +``` +This will result in: +```xml +Hello --! +``` + +Notice the `` placeholders. +`` is useful for translators because it can contain an example as well as description. +In case of `goog.getMsg` there is no way to encode the example, and the description defaults to capitalized version of the `{$}`. +In the example above `{$name}` is encoded in `` as `NAME`. +What is necessary is to generate `goog.getMsg` which uses `{$placeholder}` but is mapped to Angular's `�0�` placeholder. +This is achieved as follows. + +```typescript +/** + * @desc Greeting. + */ +const MSG = goog.getMsg('Hello {$name}!', { + name: '�0�' +}); +``` +The resulting string will be `"Hello �0�!"` which can be used by Angular's runtime. + +Here is a more complete example. + +Given this Angular's template: +```HTML +
+ {{count}} is rendered as: + + { count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } + . +
+``` + +The compiler will generate: +```typescript +/** + * @desc Some description. + */ +let MSG_div = goog.getMsg(`{$COUNT} is rendered as: {$START_BOLD_TEXT_1}{{$COUNT}, plural, + =0 {no {$START_BOLD_TEXT}emails{$CLOSE_BOLD_TEXT}!} + =1 {one {$START_ITALIC_TEXT}email{$CLOSE_ITALIC_TEXT}} + other {{$COUNT} {$START_TAG_SPAN}emails{$CLOSE_TAG_SPAN}} + }{$END_BOLD_TEXT_1}`, { + COUNT: '�0�', + START_BOLD_TEXT_1: '�*3:1��#1:1�', + END_BOLD_TEXT_1: '�/#1:1��/*3:1�', + START_BOLD_TEXT: '', + CLOSE_BOLD_TEXT: '', + START_ITALIC_TEXT: '', + CLOSE_ITALIC_TEXT: '', + START_TAG_SPAN: '', + CLOSE_TAG_SPAN: '' +}); +``` + +The result of the above will be a string which `i18nStart` can process: +``` +�0� is rendered as: �*3:1��#1:1�{�0:1�, plural, + =0 {no emails!} + =1 {one email} + other {�0:1� emails} +}�/#1:1��/*3:1�. +``` + +### Backwards compatibility with ViewEngine + +In order to upgrade from ViewEngine to Ivy runtime it is necessary to make sure that the translation IDs match between the two systems. +There are two issues which need to be solved: +1. The ViewEngine implementation splits a single `i18n` block into multiple messages when ICUs are embedded in the translation. +2. The ViewEngine does its own message extraction and uses a different hashing algorithm from `goog.getMsg`. + +To generate code where the extracted i18n messages have the same ids, the `ngtsc` can be placed into a special compatibility mode which will generate `goog.getMsg` in a special altered format as described next. + +Given this Angular's template: +```HTML +
+ {{count}} is rendered as: + + { count, plural, + =0 {no emails!} + =1 {one email} + other {{{count}} emails} + } + . +
+``` + +The ViewEngine implementation will generate following XMB file. +```XML +app/app.component.html:1,10 + -- + is rendered as: + -- + -- + -- + . + +app/app.component.html:4,8 + {VAR_PLURAL, plural, + =0 {no -- + emails -- + ! + } + =1 {one -- + email -- + } + other {-- + -- + emails + -- + } + } + +``` + +With the compatibility mode the compiler will generate following code which will match the IDs and structure of the ViewEngine: +```typescript +/** + * @desc [BACKUP_MESSAGE_ID:3639715378617754400] ICU extracted form: Some description. + */ +const MSG_div_icu = goog.getMsg(`{VAR_PLURAL, plural, + =0 {no {$START_BOLD_TEXT}emails{$CLOSE_BOLD_TEXT}!} + =1 {one {$START_ITALIC_TEXT}email{$CLOSE_ITALIC_TEXT}} + other {{$count} {$START_TAG_SPAN}emails{$CLOSE_TAG_SPAN}} + }`, { + START_BOLD_TEXT: '', + CLOSE_BOLD_TEXT: '', + START_ITALIC_TEXT: '', + CLOSE_ITALIC_TEXT: '', + COUNT: '�0:1�', + START_TAG_SPAN: '', + CLOSE_TAG_SPAN: '' + } +); + +/** + * @desc [BACKUP_MESSAGE_ID:2919330615509803611] Some description. + */ +const MSG_div = goog.getMsg('{$COUNT_1} is rendered as: {$START_BOLD_TEXT_1}{$ICU}{$END_BOLD_TEXT_1}', { + ICU: i18nIcuReplaceVar(MSG_div_icu, 'VAR_PLURAL', '�0:1�'), + COUNT: '�0:1�', + START_BOLD_TEXT_1: '�*3:1��#1�', + END_BOLD_TEXT_1: '�/#1:1��/*3:1�', +}); +``` +NOTE: +- The compiler generates `[BACKUP_MESSAGE_ID:2919330615509803611]` which forces the `goog.getMsg` to use a specific message ID. +- The compiler splits a single translation on ICU boundaries so that same number of messages are generated as with ViewEngine. +- The two messages are reassembled into a single message. + +Resulting in same string which Angular can process: +``` +�0� is rendered as: �*3:1��#1:1�{�0:1�, plural, + =0 {no emails!} + =1 {one email} + other {�0:1� emails} +}�/#1:1��/*3:1�. +``` + +### Notice `i18nIcuReplaceVar` function + +The `i18nIcuReplaceVar(MSG_div_icu, 'VAR_PLURAL', '�0:1�')` function is needed to replace `VAR_PLURAL` for `�0:1�`. +This is required because the ICU format does not allow placeholders in the ICU header location, a variable such as `VAR_PLURAL` must be used. +The point of `i18nIcuReplaceVar` is to format the ICU message to something that `i18nStart` can understand. + diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts new file mode 100644 index 0000000000..2c39b244ff --- /dev/null +++ b/packages/core/src/render3/interfaces/i18n.ts @@ -0,0 +1,364 @@ +/** + * @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 + */ + +/** + * `I18nMutateOpCode` defines OpCodes for `I18nMutateOpCodes` array. + * + * OpCodes contain three parts: + * 1) Parent node index offset. + * 2) Reference node index offset. + * 3) The OpCode to execute. + * + * See: `I18nCreateOpCodes` for example of usage. + */ +export const enum I18nMutateOpCode { + /// 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 = 0b11, + /// Mask for reference index. + MASK_REF = ((2 ^ 16) - 1) << SHIFT_REF, + + /// OpCode to select a node. (next OpCode will contain the operation.) + Select = 0b00, + /// OpCode to append the current node to `PARENT`. + AppendChild = 0b01, + /// OpCode to insert the current node to `PARENT` before `REF`. + InsertBefore = 0b10, + /// OpCode to remove the `REF` node from `PARENT`. + Remove = 0b11, +} + +/** + * Marks that the next string is for element. + * + * See `I18nMutateOpCodes` documentation. + */ +export const ELEMENT_MARKER: ELEMENT_MARKER = { + marker: 'element' +}; +export interface ELEMENT_MARKER { marker: 'element'; } + +/** + * Marks that the next string is for comment. + * + * See `I18nMutateOpCodes` documentation. + */ +export const COMMENT_MARKER: COMMENT_MARKER = { + marker: 'comment' +}; + +export interface COMMENT_MARKER { marker: 'comment'; } + +/** + * Array storing OpCode for dynamically creating `i18n` blocks. + * + * Example: + * ``` + * [ + * // For adding text nodes + * // --------------------- + * // Equivalent to: + * // const node = lViewData[index++] = document.createTextNode('abc'); + * // lViewData[1].insertBefore(node, lViewData[2]); + * 'abc', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore, + * + * // Equivalent to: + * // const node = lViewData[index++] = document.createTextNode('xyz'); + * // lViewData[1].appendChild(node); + * 'xyz', 1 << SHIFT_PARENT | AppendChild, + * + * // For adding element nodes + * // --------------------- + * // Equivalent to: + * // const node = lViewData[index++] = document.createElement('div'); + * // lViewData[1].insertBefore(node, lViewData[2]); + * ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore, + * + * // Equivalent to: + * // const node = lViewData[index++] = document.createElement('div'); + * // lViewData[1].appendChild(node); + * ELEMENT_MARKER, 'div', 1 << SHIFT_PARENT | AppendChild, + * + * // For adding comment nodes + * // --------------------- + * // Equivalent to: + * // const node = lViewData[index++] = document.createComment(''); + * // lViewData[1].insertBefore(node, lViewData[2]); + * COMMENT_MARKER, '', 1 << SHIFT_PARENT | 2 << SHIFT_REF | InsertBefore, + * + * // Equivalent to: + * // const node = lViewData[index++] = document.createComment(''); + * // lViewData[1].appendChild(node); + * COMMENT_MARKER, '', 1 << SHIFT_PARENT | AppendChild, + * + * // For moving existing nodes to a different location + * // -------------------------------------------------- + * // Equivalent to: + * // const node = lViewData[1]; + * // lViewData[2].insertBefore(node, lViewData[3]); + * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | 3 << SHIFT_REF | InsertBefore, + * + * // Equivalent to: + * // const node = lViewData[1]; + * // lViewData[2].appendChild(node); + * 1 << SHIFT_REF | Select, 2 << SHIFT_PARENT | AppendChild, + * + * // For removing existing nodes + * // -------------------------------------------------- + * // const node = lViewData[1]; + * // lViewData[2].remove(node); + * 2 << SHIFT_PARENT | 1 << SHIFT_REF | Remove, + * + * // For writing attributes + * // -------------------------------------------------- + * // const node = lViewData[1]; + * // node.setAttribute('attr', 'value'); + * 1 << SHIFT_REF | Select, 'attr', 'value' + * // NOTE: Select followed by two string (vs select followed by OpCode) + * ]; + * ``` + * NOTE: + * - `index` is initial location where the extra nodes should be stored in the EXPANDO section of + * `LVIewData`. + * + * See: `applyI18nCreateOpCodes`; + */ +export interface I18nMutateOpCodes extends Array { +} + +export const enum I18nUpdateOpCode { + /// Stores shift amount for bits 17-2 that contain reference index. + SHIFT_REF = 2, + /// Stores shift amount for bits 31-17 that contain which ICU in i18n block are we referring to. + SHIFT_ICU = 17, + /// Mask for OpCode + MASK_OPCODE = 0b11, + /// Mask for reference index. + MASK_REF = ((2 ^ 16) - 1) << SHIFT_REF, + + /// OpCode to update a text node. + Text = 0b00, + /// OpCode to update a attribute of a node. + Attr = 0b01, + /// OpCode to switch the current ICU case. + IcuSwitch = 0b10, + /// OpCode to update the current ICU case. + IcuUpdate = 0b11, +} + +/** + * Stores DOM operations which need to be applied to update DOM render tree due to changes in + * expressions. + * + * The basic idea is that `i18nExp` OpCodes capture expression changes and update a change + * mask bit. (Bit 1 for expression 1, bit 2 for expression 2 etc..., bit 32 for expression 32 and + * higher.) The OpCodes then compare its own change mask against the expression change mask to + * determine if the OpCodes should execute. + * + * These OpCodes can be used by both the i18n block as well as ICU sub-block. + * + * ## Example + * + * Assume + * ``` + * if (rf & RenderFlags.Update) { + * i18nExp(bind(ctx.exp1)); // If changed set mask bit 1 + * i18nExp(bind(ctx.exp2)); // If changed set mask bit 2 + * i18nExp(bind(ctx.exp3)); // If changed set mask bit 3 + * i18nExp(bind(ctx.exp4)); // If changed set mask bit 4 + * i18nApply(0); // Apply all changes by executing the OpCodes. + * } + * ``` + * We can assume that each call to `i18nExp` sets an internal `changeMask` bit depending on the + * index of `i18nExp` index. + * + * OpCodes + * ``` + * [ + * // 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. + * 0b11, 7, + * // Concatenate `newValue = 'pre'+lViewData[bindIndex-4]+'in'+lViewData[bindIndex-3]+'post';`. + * 'pre', -4, 'in', -3, 'post', + * // Update attribute: `elementAttribute(1, 'title', sanitizerFn(newValue));` + * 1 << SHIFT_REF | Attr, 'title', sanitizerFn, + * + * // The following OpCodes represent: `
Hello {{exp3}}!">` + * // If `changeMask & 0b100` + * // has changed then execute update OpCodes. + * // has NOT changed then skip `4` values and start processing next OpCodes. + * 0b100, 4, + * // Concatenate `newValue = 'Hello ' + lViewData[bindIndex -2] + '!';`. + * 'Hello ', -2, '!', + * // Update text: `lViewData[1].textContent = newValue;` + * 1 << SHIFT_REF | Text, + * + * // The following OpCodes represent: `
{exp4, plural, ... }">` + * // If `changeMask & 0b1000` + * // has changed then execute update OpCodes. + * // has NOT changed then skip `4` values and start processing next OpCodes. + * 0b1000, 4, + * // Concatenate `newValue = lViewData[bindIndex -1];`. + * -1, + * // Switch ICU: `icuSwitchCase(lViewData[1], 0, newValue);` + * 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuSwitch, + * + * // Note `changeMask & -1` is always true, so the IcuUpdate will always execute. + * -1, 1, + * // Update ICU: `icuUpdateCase(lViewData[1], 0);` + * 0 << SHIFT_ICU | 1 << SHIFT_REF | IcuUpdate, + * + * ]; + * ``` + * + */ +export interface I18nUpdateOpCodes extends Array string | null)> {} + +/** + * Store information for the i18n translation block. + */ +export interface TI18n { + /** + * Number of slots to allocate in expando. + * + * 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; + + /** + * Index in EXPANDO where the i18n stores its DOM nodes. + * + * When the bindings are processed by the `i18nEnd` instruction it is necessary to know where the + * newly created DOM nodes will be inserted. + */ + expandoStartIndex: number; + + /** + * A set of OpCodes which will create the Text Nodes and ICU anchors for the translation blocks. + * + * NOTE: The ICU anchors are filled in with ICU Update OpCode. + */ + create: I18nMutateOpCodes; + + /** + * A set of OpCodes which will be executed on each change detection to determine if any changes to + * DOM are required. + */ + update: I18nUpdateOpCodes; + + /** + * A list of ICUs in a translation block (or `null` if block has no ICUs). + * + * Example: + * Given: `
You have {count, plural, ...} and {state, switch, ...}
` + * There would be 2 ICUs in this array. + * 1. `{count, plural, ...}` + * 2. `{state, switch, ...}` + */ + icus: TIcu[]|null; +} + +/** + * Defines the ICU type of `select` or `plural` + */ +export const enum IcuType { + select = 0, + plural = 1, +} + +export interface TIcu { + /** + * Defines the ICU type of `select` or `plural` + */ + type: IcuType; + + /** + * Number of slots to allocate in expando for each 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. + * + * In case of nested ICUs such as: + * ``` + * {�0�, plural, + * =0 {zero} + * other {�0� {�1�, select, + * cat {cats} + * dog {dogs} + * other {animals} + * }! + * } + * } + * ``` + * When the parent ICU is changing it must clean up child ICUs as well. For this reason it needs + * to know which child ICUs to run clean up for as well. + * + * In the above example this would be: + * ``` + * [ + * [], // `=0` has no sub ICUs + * [1], // `other` has one subICU at `1`st index. + * ] + * ``` + * + * The reason why it is Array of Arrays is because first array represents the case, and second + * represents the child ICUs to clean up. There may be more than one child ICUs per case. + */ + childIcus: number[][]; + + /** + * Index in EXPANDO where the i18n stores its DOM nodes. + * + * When the bindings are processed by the `i18nEnd` instruction it is necessary to know where the + * newly created DOM nodes will be inserted. + */ + expandoStartIndex: number; + + /** + * A list of case values which the current ICU will try to match. + * + * The last value is `other` + */ + cases: any[]; + + /** + * 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[]; +} + +/** + * 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 {}