parent
a7f61e63fa
commit
fe5caca884
|
@ -1,17 +1,24 @@
|
|||
# I18N
|
||||
# I18N translation support
|
||||
|
||||
Templates can be marked as requiring translation support via `i18n` and `i18n-...` attributes on elements.
|
||||
Translation support involves mapping component template contents to **i18n messages**, which may contain interpolations, DOM elements and sub-templates.
|
||||
|
||||
This document describes how this support is implemented in Angular templates.
|
||||
|
||||
|
||||
## Example of i18n message
|
||||
|
||||
Given an i18n component:
|
||||
The following component definition illustrates how i18n works in Angular:
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
template: `
|
||||
<div i18n-title title="Hello {{name}}!" i18n>
|
||||
{{count}} is rendered as:
|
||||
{{count}} is rendered as:
|
||||
<b *ngIf="exp">
|
||||
{ count, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
{ count, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {{{count}} <span title="{{count}}">emails</span>}
|
||||
}
|
||||
</b>.
|
||||
|
@ -21,31 +28,35 @@ Given an i18n component:
|
|||
class MyComponent {
|
||||
}
|
||||
```
|
||||
NOTE:
|
||||
- There really is only two kinds of i18n text.
|
||||
1. In attribute as in `title` (with `i18n-title` present).
|
||||
2. In element body marked as as `<div i18n>`.
|
||||
- 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.)
|
||||
NOTE:
|
||||
- There are only two kinds of i18n messages:
|
||||
1. In the text of an attribute (e.g. `title` of `<div i18n-title title="Hello {{name}}!">`, indicated by the presence of the `i18n-title` attribute).
|
||||
2. In the body of an element (e.g. `<div i18n>` indicated by the presence of the `i18n` attribute).
|
||||
- The body of an element marked with `i18n` can contain internal DOM structure (e.g. other DOM elements).
|
||||
- The internal structure of such an element may even contain Angular sub-templates (e.g. `ng-container` or `*ngFor` directives).
|
||||
|
||||
|
||||
## Implementation overview
|
||||
|
||||
- i18n messages must preserve the DOM structure in elements marked with `i18n` because those DOM structures may have components and directives.
|
||||
- **Parsed i18n messages** must live in `TView.data`. This is because in case of SSR we need to be able to execute multiple locales in the same VM.
|
||||
(If parsed i18n messages are only at the top level we could not have more than one locale.)
|
||||
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.
|
||||
|
||||
|
||||
### Generated code
|
||||
|
||||
Given the example component above, the Angular template compiler generates the following Ivy rendering instructions.
|
||||
|
||||
The compiler generates:
|
||||
```typescript
|
||||
// These messages need to be retrieved from some localization service described later
|
||||
// These i18n messages need to be retrieved from a "localization service", described later.
|
||||
const MSG_title = 'Hello <20>0<EFBFBD>!';
|
||||
const MSG_div_attr = ['title', MSG_title];
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0:1<> <span title="<22>0:1<>">emails</span>}
|
||||
}<7D>/#1:1<><31>/*3:1<>.`;
|
||||
|
||||
|
@ -74,95 +85,131 @@ class MyComponent {
|
|||
elementEnd();
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
i18nExp(bind(ctx.count)); // referenced by `<60>0<EFBFBD>`
|
||||
i18nExp(bind(ctx.name)); // referenced by `<60>0<EFBFBD>` in `MSG_title`
|
||||
i18nApply(1); // Updates the `i18n-title` binding
|
||||
i18nExp(bind(ctx.count)); // referenced by `<60>0<EFBFBD>`
|
||||
i18nExp(bind(ctx.count)); // referenced by `<60>0<EFBFBD>` in `MSG_div`
|
||||
i18nApply(2); // Updates the `<div i18n>...</div>`
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
The translated message can contain i18n placeholders (denoted by `<60>...<2E>`) which tell the translation how to interpolate the text.
|
||||
|
||||
### i18n markers (<28>...<2E>)
|
||||
|
||||
Each i18n message contains **i18n markers** (denoted by `<60>...<2E>`) which tell the renderer how to map the translated text onto the renderer instructions.
|
||||
The [<EFBFBD>](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:
|
||||
- `<60>{index}(:{block})<29>`: *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.
|
||||
- `<60>#{index}(:{block})<29>`/`<60>/#{index}(:{block})<29>`: *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.
|
||||
- `<60>*{index}:{block}<7D>`/`<60>/*{index}:{block}<7D>`: *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.
|
||||
Each i18n marker contains an `index` (and optionally a `block`) which provide binding information for the marker.
|
||||
|
||||
No other place holder format is supported.
|
||||
The i18n markers are:
|
||||
|
||||
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.
|
||||
- `<60>{index}(:{block})<29>`: *Binding placeholder*: Marks a location where an interpolated expression will be rendered.
|
||||
- `index`: the index of the binding within this i18n message block.
|
||||
- `block` (*optional*): the index of the sub-template block, in which this placeholder was declared.
|
||||
|
||||
## Accumulator
|
||||
- `<60>#{index}(:{block})<29> ... <20>/#{index}(:{block})<29>`: *Element block*: Marks the beginning and end of a DOM element that is embedded in the original translation string.
|
||||
- `index`: the index of the element, as defined in the template instructions (e.g. `elementStart(index, ...)`).
|
||||
- `block` (*optional*): the index of the sub-template block, in which this element was declared.
|
||||
|
||||
For concatenating strings we use an accumulator.
|
||||
- `<60>*{index}:{block}<7D>`/`<60>/*{index}:{block}<7D>`: *Sub-template block*: Marks a sub-template block that is translated separately in its own angular template function.
|
||||
- `index`: the index of the `template` instruction, as defined in the template instructions (e.g. `template(index, ...)`).
|
||||
- `block`: the index of the parent sub-template block, in which this child sub-template block was declared.
|
||||
|
||||
- `<60>!{index}:{block}<7D>/<2F>/!{index}:{block}<7D>`: *Projection block*: Marks the beginning and end of <ng-content> that was embedded in the original translation block.
|
||||
- `index`: the index of the projection, as defined in the template instructions (e.g. `projection(index, ...)`).
|
||||
- `block` (*optional*): the index of the parent sub-template block, in which this child sub-template block was declared.
|
||||
|
||||
No other i18n marker format is supported.
|
||||
|
||||
The i18n markers in the example above can be interpreted as follows:
|
||||
|
||||
```typescript
|
||||
const MSG_title = 'Hello <20>0<EFBFBD>!';
|
||||
const MSG_div_attr = ['title', MSG_title];
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0:1<> <span title="<22>0:1<>">emails</span>}
|
||||
}<7D>/#1:1<><31>/*3:1<>.`;
|
||||
```
|
||||
|
||||
- `<60>0<EFBFBD>`: the `{{name}}` interpolated expression with index 0.
|
||||
- `<60>*3:1<>`: the start of the `*ngIf` template with index 3, effectively defining sub-template block 1.
|
||||
- `<60>/*3:1<>`: the end of the `*ngIf` template with index 3 (sub-template block 1).
|
||||
- `<60>#1:1<>`: the start of the `<b>` element with index 1, found inside sub-template block 1.
|
||||
- `<60>/#1:1<>`: the end of the `</b>` element with index 1, found inside sub-template block 1.
|
||||
- `<60>0:1<>`: the binding expression `count` (both as the parameter `count` for the `plural` ICU and as the `{{count}}` interpolation) with index 0, found inside sub-template block 1.
|
||||
|
||||
NOTE:
|
||||
|
||||
- Each closing i18n marker has the same information as its opening i18n marker.
|
||||
This is so that the parser can verify that opening and closing i18n markers are properly nested.
|
||||
Failure to properly nest the i18n markers implies that the translator changed the order of translation incorrectly and should be a runtime error.
|
||||
- The optional `block` index is added to i18n markers contained within sub-template blocks.
|
||||
This is because blocks must be properly nested and it is an error for the translator to move an i18n marker outside of its block. This will result in runtime error.
|
||||
- i18n markers are unique within the translation string in which they are found.
|
||||
|
||||
|
||||
### Rendering i18n messages
|
||||
|
||||
i18n messages are rendered by concatenating each piece of the string using an accumulator.
|
||||
The pieces to be concatenated may be a substring from the i18n message or an index to a binding.
|
||||
This is best explained through pseudo code:
|
||||
|
||||
```typescript
|
||||
const accumulator:string[] = [];
|
||||
function render18nString(i18nStringParts: string|number) {
|
||||
const accumulator:string[] = [];
|
||||
i18nStringParts.forEach(part => accumulate(part));
|
||||
return accumulatorFlush(sanitizer);
|
||||
|
||||
/**
|
||||
* 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];
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
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);
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
accumulator.length = 0;
|
||||
return interpolation;
|
||||
}
|
||||
```
|
||||
|
||||
## i18n Attributes
|
||||
|
||||
Let's look at the simpler case of i18n and attribute interpolation.
|
||||
Rendering i18n attributes is straightforward:
|
||||
|
||||
```typescript
|
||||
// These messages need to be retrieved from some localization service described later
|
||||
const MSG_title = 'Hello <20>0<EFBFBD>!';
|
||||
```html
|
||||
<div i18n-title title="Hello {{name}}!">
|
||||
```
|
||||
Next notice the `i18nAttributes` instruction inside the `RenderFlags.Create` block.
|
||||
|
||||
The template compiler will generate the following statements inside the `RenderFlags.Create` block.
|
||||
|
||||
```typescript
|
||||
const MSG_title = 'Hello <20>0<EFBFBD>!';
|
||||
const MSG_div_attr = ['title', MSG_title];
|
||||
elementStart(0, 'div');
|
||||
i18nAttributes(1, MSG_div_attr);
|
||||
```
|
||||
The above instruction checks the `TView.data` cache at position `1` and if empty will create `I18nUpdateOpCodes` like so:
|
||||
|
||||
The `i18nAttributes()` instruction checks the `TView.data` cache at position `1` and if empty will create `I18nUpdateOpCodes` like so:
|
||||
|
||||
```typescript
|
||||
const i18nUpdateOpCodes = <I18nUpdateOpCodes>[
|
||||
// The following OpCodes represent: `<div i18n-title title="Hello <20>0<EFBFBD>!">`
|
||||
// If `changeMask & 0b11`
|
||||
// If `changeMask & 0b1`
|
||||
// has changed then execute update OpCodes.
|
||||
// has NOT changed then skip `7` values and start processing next OpCodes.
|
||||
0b1, 7,
|
||||
|
@ -175,33 +222,48 @@ const i18nUpdateOpCodes = <I18nUpdateOpCodes>[
|
|||
0 << SHIFT_REF | Attr, 'title', null,
|
||||
]
|
||||
```
|
||||
|
||||
NOTE:
|
||||
- `i18nAttributes` updates the attributes of the previous element.
|
||||
- If there is more than one attribute which needs to be internationalized it is added to the array as `[attributeName, translation]` tuple.
|
||||
- Even attributes which don't have bindings must go through `i18nAttributes` so that they correctly work with i18n in server environment.
|
||||
- The `i18nAttributes()` instruction updates the attributes of the "previous" element.
|
||||
- Each attribute to be translated is provided as a pair of elements in the array passed to the `i18nAttributes()` instruction (e.g. `['title', MSG_title, 'src', MSG_src, ...]`).
|
||||
- Even attributes that don't have bindings must go through `i18nAttributes()` so that they correctly work with i18n in a server environment.
|
||||
|
||||
|
||||
## i18n Elements
|
||||
|
||||
Generating text inside existing elements is a bit more complicated but follows the same philosophy as attributes.
|
||||
Rendering i18n elements is more complicated but follows the same philosophy as attributes, with additional i18n markers.
|
||||
|
||||
First we define the message and mapping (which placeholders map to which expressions) as so:
|
||||
```html
|
||||
<div i18n>
|
||||
{{count}} is rendered as:
|
||||
<b *ngIf="exp">
|
||||
{ count, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {{{count}} <span title="{{count}}">emails</span>}
|
||||
}
|
||||
</b>.
|
||||
</div>
|
||||
```
|
||||
|
||||
The template compiler generates the following i18n message:
|
||||
|
||||
```typescript
|
||||
// These messages need to be retrieved from some localization service described later
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
// This message is retrieved from a "localization service" described later
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0:1<> <span title="<22>0:1<>">emails</span>}
|
||||
}<7D>/#1:1<><31>/*3:1<>.`;
|
||||
```
|
||||
|
||||
### Exclusion Zones / Sub-Templates
|
||||
### Sub-template blocks
|
||||
|
||||
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 `<60>*{index}:{block}<7D>` and `<60>/*{index}:{block}<7D>` 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.
|
||||
Most i18n translated elements do not have sub-templates (e.g. `*ngIf`), but where they do the i18n message describes a **sub-template block** defined by `<60>*{index}:{block}<7D>` and `<60>/*{index}:{block}<7D>` markers.
|
||||
The sub-template block is extracted from the translation so it is as if there are two separate translated strings for parent and sub-template.
|
||||
|
||||
Consider the following nested template:
|
||||
|
||||
Given nested template:
|
||||
```html
|
||||
<div i18n>
|
||||
List:
|
||||
|
@ -213,9 +275,10 @@ Given nested template:
|
|||
</div>
|
||||
```
|
||||
|
||||
will generate
|
||||
The template compiler will generate the following translated string and instructions:
|
||||
|
||||
```typescript
|
||||
// Text broken down to allow addition of comments (Generated code will not have comments)
|
||||
// The string split across lines to allow addition of comments. The generated code does not have comments.
|
||||
const MSG_div =
|
||||
'List: ' +
|
||||
'<27>*2:1<>' + // template(2, MyComponent_NgIf_Template_0, ...);
|
||||
|
@ -226,13 +289,14 @@ const MSG_div =
|
|||
'<27>/#1:2<>' +
|
||||
'<27>/*2:2<>' +
|
||||
'<27>/#1:1<>' +
|
||||
'<27>*2:1<>' +
|
||||
'<27>/*2:1<>' +
|
||||
'Summary: ' +
|
||||
'<27>*3:3<>' + // template(3, MyComponent_NgIf_Template_2, ...);
|
||||
'<27>#1:3<>' + // element(1, 'span');
|
||||
'<27>#1:3<>' +
|
||||
'<27>*3:3<>';
|
||||
'<27>/#1:3<>' +
|
||||
'<27>/*3:3<>';
|
||||
|
||||
// <20>*2:2<> ... <20>/*2:2<> (`*ngFor` template, instruction index 2, inside the `*ngIf` template, sub-template block 1)
|
||||
function MyComponent_NgIf_NgFor_Template_1(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
i18nStart(0, MSG_div, 2); // 2nd `*` content: `<60>#1:2<>item<65>/#1:2<>`
|
||||
|
@ -242,9 +306,10 @@ function MyComponent_NgIf_NgFor_Template_1(rf: RenderFlags, ctx: any) {
|
|||
...
|
||||
}
|
||||
|
||||
// <20>*2:1<> ... <20>/*2:1<>
|
||||
function MyComponent_NgIf_Template_0(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
i18nStart(0, MSG_div, 1); // 1st `*` content: `<60>#1:1<><31>*2:2<><32>/*2:2<><32>/#1:1<>`
|
||||
i18nStart(0, MSG_div, 1); // 1st `*` content: `<60>#1:1<><31>*2:2<><32>/*2:2<><32>/#1:1<>`
|
||||
elementStart(1, 'ul');
|
||||
template(2, MyComponent_NgIf_NgFor_Template_1, ...);
|
||||
elementEnd();
|
||||
|
@ -253,6 +318,7 @@ function MyComponent_NgIf_Template_0(rf: RenderFlags, ctx: any) {
|
|||
...
|
||||
}
|
||||
|
||||
// <20>*3:3<> ... <20>/*3:3<>
|
||||
function MyComponent_NgIf_Template_2(rf: RenderFlags, ctx: any) {
|
||||
if (rf & RenderFlags.Create) {
|
||||
i18nStart(0, MSG_div, 3); // 3rd `*` content: `<60>#1:3<><33>/#1:3<>`
|
||||
|
@ -276,20 +342,22 @@ class MyComponent {
|
|||
}
|
||||
...
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### `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.)
|
||||
It is the job of the instruction `i18nStart` to parse the i18n message and to provide the appropriate text to each of the following instructions.
|
||||
|
||||
Note:
|
||||
- Inside a block that is marked with `i18n` 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)
|
||||
MSG_div, // The i18n message to parse which has been translated
|
||||
// Optional sub-template block index. Empty implies `0` (most common)
|
||||
);
|
||||
...
|
||||
i18nEnd(); // The instruction which is responsible for inserting text nodes into
|
||||
|
@ -300,7 +368,7 @@ The `i18nStart` generates these instructions which are cached in the `TView` and
|
|||
|
||||
```typescript
|
||||
const tI18n = <TI18n>{
|
||||
vars: 2, // Number of slots to allocate in EXPANDO.
|
||||
vars: 2, // Number of slots to allocate in EXPANDO.
|
||||
expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100
|
||||
create: <I18nMutateOpCodes>[ // Processed by `i18nEnd`
|
||||
// Equivalent to:
|
||||
|
@ -322,7 +390,7 @@ const tI18n = <TI18n>{
|
|||
0b1, 3,
|
||||
-1, // accumulate(-1);
|
||||
'is rendered as: ', // accumulate('is rendered as: ');
|
||||
// Flush the concatenated string to text node at position 100.
|
||||
// Flush the concatenated string to text node at position 100.
|
||||
100 << SHIFT_REF | Text, // lView[100].textContent = accumulatorFlush();
|
||||
],
|
||||
icus: null,
|
||||
|
@ -332,7 +400,7 @@ const tI18n = <TI18n>{
|
|||
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
|
||||
### `i18nStart` in sub-template blocks
|
||||
|
||||
```typescript
|
||||
i18nStart(
|
||||
|
@ -347,15 +415,14 @@ This means that the instruction has to extract out 1st sub-block from the root-t
|
|||
|
||||
Starting with
|
||||
```typescript
|
||||
// These messages need to be retrieved from some localization service described later
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
const MSG_div = `<60>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0:1<> <span title="<22>0:1<>">emails</span>}
|
||||
}<7D>/#1:1<><31>/*3:1<>.`;
|
||||
```
|
||||
|
||||
The `i18nStart` instruction traverses `MSG_div` and looks for 1st sub-template marked with `<60>*3:1<>`.
|
||||
The `i18nStart` instruction traverses `MSG_div` and looks for 1st sub-template block marked with `<60>*3:1<>`.
|
||||
Notice that the `<60>*3:1<>` contains index to the DOM element `3`.
|
||||
The rest of the code should work same as described above.
|
||||
|
||||
|
@ -499,7 +566,7 @@ const tI18n = <TI18n>{
|
|||
// Concatenate `newValue = '' + lView[bindIndex -1];`.
|
||||
-1, // accumulate(-1);
|
||||
// Update attribute: `lView[204].setAttribute(204, 'title', 0b1, 2,(null));`
|
||||
// NOTE: `null` implies no sanitization.
|
||||
// NOTE: `null` implies no sanitization.
|
||||
204 << SHIFT_REF | Attr, 'title', null
|
||||
]
|
||||
]
|
||||
|
@ -510,12 +577,12 @@ const tI18n = <TI18n>{
|
|||
|
||||
## Sanitization
|
||||
|
||||
Any text coming from translators is considered safe and has no sanitization applied to it.
|
||||
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.
|
||||
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
|
||||
|
||||
|
@ -523,7 +590,7 @@ Assume we have translation like so: `<div i18n>Hello {{name}}!</div>`.
|
|||
The above calls generates the following template instruction code:
|
||||
|
||||
```typescript
|
||||
// These messages need to be retrieved from some localization service described later
|
||||
// This message will be retrieved from some localization service described later
|
||||
const MSG_div = 'Hello <20>0<EFBFBD>!';
|
||||
|
||||
template: function(rf: RenderFlags, ctx: MyComponent) {
|
||||
|
@ -580,9 +647,9 @@ Given an i18n component:
|
|||
@Component({
|
||||
template: `
|
||||
<div i18n-title
|
||||
title="You have { count, plural,
|
||||
=0 {no emails}
|
||||
=1 {one email}
|
||||
title="You have { count, plural,
|
||||
=0 {no emails}
|
||||
=1 {one email}
|
||||
other {{{count}} emails}
|
||||
}.">
|
||||
</div>
|
||||
|
@ -595,9 +662,9 @@ class MyComponent {
|
|||
The compiler generates:
|
||||
```typescript
|
||||
// These messages need to be retrieved from some localization service described later
|
||||
const MSG_title = `You have {<7B>0<EFBFBD>, plural,
|
||||
=0 {no emails}
|
||||
=1 {one email}
|
||||
const MSG_title = `You have {<7B>0<EFBFBD>, plural,
|
||||
=0 {no emails}
|
||||
=1 {one email}
|
||||
other {<7B>0<EFBFBD> emails}
|
||||
}.`;
|
||||
const MSG_div_attr = ['title', MSG_title, optionalSanitizerFn];
|
||||
|
@ -616,7 +683,7 @@ class MyComponent {
|
|||
i18nApply(1); // Updates the `i18n-title` binding
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -648,7 +715,7 @@ const tI18n = <TI18n>{
|
|||
// SHIFT_ICU: is an index into which ICU is being updated. In our example we only have
|
||||
// one ICU so it is 0-th ICU to update.
|
||||
200 << SHIFT_REF | 0 << SHIFT_ICU | IcuUpdate,
|
||||
|
||||
|
||||
'.', // accumulate('.');
|
||||
|
||||
// Update attribute: `elementAttribute(1, 'title', accumulatorFlush(null));`
|
||||
|
@ -687,7 +754,7 @@ const tI18n = <TI18n>{
|
|||
'no emails', // accumulate('no emails');
|
||||
]
|
||||
// Case: `1`: `{one email}`
|
||||
<I18nMutateOpCodes>[
|
||||
<I18nMutateOpCodes>[
|
||||
// If `changeMask & -1` // always true
|
||||
// has changed then execute update OpCodes.
|
||||
// has NOT changed then skip `1` values and start processing next OpCodes.
|
||||
|
@ -717,9 +784,9 @@ First part of ICU parsing is breaking down the ICU into cases.
|
|||
|
||||
Given
|
||||
```
|
||||
{<7B>0<EFBFBD>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
{<7B>0<EFBFBD>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0<EFBFBD> <span title="<22>0<EFBFBD>">emails</span>}
|
||||
}
|
||||
```
|
||||
|
@ -736,7 +803,7 @@ const icu = {
|
|||
}
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
@ -758,7 +825,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 inserted synchronously.
|
||||
For virtual elements such as `<ng-container>` or `<ng-template>` 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 `<ng-container>` behavior.
|
||||
|
@ -785,7 +852,7 @@ function MyComponent_Template_0(rf: RenderFlags, ctx: any) {
|
|||
Which would get parsed into:
|
||||
```typescript
|
||||
const tI18n = <TI18n>{
|
||||
vars: 2, // Number of slots to allocate in EXPANDO.
|
||||
vars: 2, // Number of slots to allocate in EXPANDO.
|
||||
expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100
|
||||
create: <I18nMutateOpCodes>[ // Processed by `i18nEnd`
|
||||
// Equivalent to:
|
||||
|
@ -804,17 +871,17 @@ RESOLVE:
|
|||
|
||||
## Nested ICUs
|
||||
|
||||
ICU can have other ICUs embedded in them.
|
||||
ICU can have other ICUs embedded in them.
|
||||
|
||||
Given:
|
||||
```typescript
|
||||
@Component({
|
||||
template: `
|
||||
{count, plural,
|
||||
=0 {zero}
|
||||
other {{{count}} {animal, select,
|
||||
cat {cats}
|
||||
dog {dogs}
|
||||
=0 {zero}
|
||||
other {{{count}} {animal, select,
|
||||
cat {cats}
|
||||
dog {dogs}
|
||||
other {animals}
|
||||
}!
|
||||
}
|
||||
|
@ -831,10 +898,10 @@ Will generate:
|
|||
```typescript
|
||||
const MSG_nested = `
|
||||
{<7B>0<EFBFBD>, plural,
|
||||
=0 {zero}
|
||||
other {<7B>0<EFBFBD> {<7B>1<EFBFBD>, select,
|
||||
cat {cats}
|
||||
dog {dogs}
|
||||
=0 {zero}
|
||||
other {<7B>0<EFBFBD> {<7B>1<EFBFBD>, select,
|
||||
cat {cats}
|
||||
dog {dogs}
|
||||
other {animals}
|
||||
}!
|
||||
}
|
||||
|
@ -857,7 +924,7 @@ class MyComponent {
|
|||
i18nApply(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -871,7 +938,7 @@ NOTE:
|
|||
The internal data structure will be:
|
||||
```typescript
|
||||
const tI18n = <TI18n>{
|
||||
vars: 2, // Number of slots to allocate in EXPANDO.
|
||||
vars: 2, // Number of slots to allocate in EXPANDO.
|
||||
expandoStartIndex: 100, // Assume in this example EXPANDO starts at 100
|
||||
create: <I18nMutateOpCodes>[ // Processed by `i18nEnd`
|
||||
],
|
||||
|
@ -908,7 +975,7 @@ const tI18n = <TI18n>{
|
|||
COMMENT_MARKER, '', 0 << SHIFT_PARENT | AppendChild, // Expando location: 101
|
||||
],
|
||||
],
|
||||
remove: [
|
||||
remove: [
|
||||
<I18nMutateOpCodes>[ // Case: `0`: `{zero}`
|
||||
1 << SHIFT_PARENT | 100 << SHIFT_REF | Remove,
|
||||
],
|
||||
|
@ -917,7 +984,7 @@ const tI18n = <TI18n>{
|
|||
1 << SHIFT_PARENT | 101 << SHIFT_REF | Remove,
|
||||
],
|
||||
],
|
||||
update: [
|
||||
update: [
|
||||
<I18nMutateOpCodes>[ // Case: `0`: `{zero}`
|
||||
],
|
||||
<I18nMutateOpCodes>[ // Case: `other`: `{<7B>0<EFBFBD> <!--subICU-->}`
|
||||
|
@ -951,7 +1018,7 @@ const tI18n = <TI18n>{
|
|||
'animals', 101 << SHIFT_PARENT | AppendChild, // Expando location: 102; 101 is location of comment/anchor
|
||||
],
|
||||
]
|
||||
remove: [
|
||||
remove: [
|
||||
<I18nMutateOpCodes>[ // Case: `cat`: `{cats}`
|
||||
101 << SHIFT_PARENT | 102 << SHIFT_REF | Remove,
|
||||
],
|
||||
|
@ -962,7 +1029,7 @@ const tI18n = <TI18n>{
|
|||
101 << SHIFT_PARENT | 102 << SHIFT_REF | Remove,
|
||||
],
|
||||
],
|
||||
update: [
|
||||
update: [
|
||||
<I18nMutateOpCodes>[ // Case: `cat`: `{cats}`
|
||||
],
|
||||
<I18nMutateOpCodes>[ // Case: `doc`: `docs`
|
||||
|
@ -975,9 +1042,6 @@ const tI18n = <TI18n>{
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
# Translation Message Retrieval
|
||||
|
||||
The generated code needs work with:
|
||||
|
@ -985,9 +1049,10 @@ The generated code needs work with:
|
|||
- 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:
|
||||
The solution is to take advantage of compile time constants (e.g. `CLOSURE`) like so:
|
||||
|
||||
```typescript
|
||||
import {localize} from '@angular/core';
|
||||
import '@angular/localize';
|
||||
|
||||
let MSG_hello;
|
||||
if (CLOSURE) {
|
||||
|
@ -998,7 +1063,7 @@ if (CLOSURE) {
|
|||
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*/);
|
||||
MSG_hello = $localize`Hello World!`;
|
||||
}
|
||||
const MSG_div_attr = ['title', MSG_hello];
|
||||
|
||||
|
@ -1011,19 +1076,19 @@ class MyComponent {
|
|||
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.
|
||||
NOTE:
|
||||
- The compile time constant (`CLOSURE`) 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 `<ph>..</ph>` tags in `.xmb` file.
|
||||
When `goog.getMsg` gets a translation it treats `{$some_text}` special by generating `<ph>..</ph>` tags in `.xmb` file.
|
||||
```typescript
|
||||
/**
|
||||
* @desc Greeting.
|
||||
|
@ -1037,7 +1102,7 @@ This will result in:
|
|||
<msg id="1234567890" desc="Greeting.">Hello <ph name="NAME"><ex>-</ex>-</ph>!</msg>
|
||||
```
|
||||
|
||||
Notice the `<ph>` placeholders.
|
||||
Notice the `<ph>` placeholders.
|
||||
`<ph>` 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 `<ph>` as `NAME`.
|
||||
|
@ -1094,9 +1159,9 @@ let MSG_div = goog.getMsg(`{$COUNT} is rendered as: {$START_BOLD_TEXT_1}{{$COUNT
|
|||
|
||||
The result of the above will be a string which `i18nStart` can process:
|
||||
```
|
||||
<EFBFBD>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
<EFBFBD>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0:1<> <span title="<22>0:1<>">emails</span>}
|
||||
}<7D>/#1:1<><31>/*3:1<>.
|
||||
```
|
||||
|
@ -1135,14 +1200,14 @@ The ViewEngine implementation will generate following XMB file.
|
|||
.
|
||||
</msg>
|
||||
<msg id="3639715378617754400"><source>app/app.component.html:4,8</source>
|
||||
{VAR_PLURAL, plural,
|
||||
{VAR_PLURAL, plural,
|
||||
=0 {no <ph name="START_BOLD_TEXT"><ex>-</ex>-</ph>
|
||||
emails <ph name="CLOSE_BOLD_TEXT"><ex>-</ex>-</ph>
|
||||
!
|
||||
}
|
||||
}
|
||||
=1 {one <ph name="START_ITALIC_TEXT"><ex>-</ex>-</ph>
|
||||
email <ph name="CLOSE_ITALIC_TEXT"><ex>-</ex>-</ph>
|
||||
}
|
||||
}
|
||||
other {<ph name="INTERPOLATION"><ex>-</ex>-</ph>
|
||||
<ph name="START_TAG_SPAN"><ex>-</ex>-</ph>
|
||||
emails
|
||||
|
@ -1190,9 +1255,9 @@ NOTE:
|
|||
|
||||
Resulting in same string which Angular can process:
|
||||
```
|
||||
<EFBFBD>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
<EFBFBD>0<EFBFBD> is rendered as: <20>*3:1<><31>#1:1<>{<7B>0:1<>, plural,
|
||||
=0 {no <b title="none">emails</b>!}
|
||||
=1 {one <i>email</i>}
|
||||
other {<7B>0:1<> <span title="<22>0:1<>">emails</span>}
|
||||
}<7D>/#1:1<><31>/*3:1<>.
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue