feat(ivy): ICU support for Ivy (#26794)

PR Close #26794
This commit is contained in:
Andrew Kushnir
2018-10-18 10:08:51 -07:00
committed by Misko Hevery
parent a4934a74b6
commit 92e80af875
28 changed files with 3106 additions and 933 deletions

View File

@ -110,12 +110,13 @@ export {
PipeDef as ɵPipeDef,
PipeDefWithMeta as ɵPipeDefWithMeta,
whenRendered as ɵwhenRendered,
i18n as ɵi18n,
i18nAttributes as ɵi18nAttributes,
i18nExp as ɵi18nExp,
i18nStart as ɵi18nStart,
i18nEnd as ɵi18nEnd,
i18nApply as ɵi18nApply,
i18nIcuReplaceVars as ɵi18nIcuReplaceVars,
i18nPostprocess as ɵi18nPostprocess,
WRAP_RENDERER_FACTORY2 as ɵWRAP_RENDERER_FACTORY2,
setClassMetadata as ɵsetClassMetadata,
} from './render3/index';

View File

@ -208,10 +208,11 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
| i18nStart | ✅ | ✅ | ✅ |
| i18nEnd | ✅ | ✅ | ✅ |
| i18nAttributes | ✅ | ✅ | ✅ |
| i18nExp | ✅ | ✅ | ✅ |
| i18nExp | ✅ | ✅ | ✅ |
| i18nApply | ✅ | ✅ | ✅ |
| ICU expressions | ✅ | ✅ | |
| closure support for g3 | ✅ | ✅ | |
| ICU expressions | ✅ | ✅ | |
| closure support for g3 | ✅ | ✅ | |
| `<ng-container>` support | ✅ | ✅ | ✅ |
| runtime service for external world | ❌ | ❌ | ❌ |
| migration tool | ❌ | ❌ | ❌ |

View File

@ -1175,12 +1175,13 @@ const MSG_div_icu = goog.getMsg(`{VAR_PLURAL, plural,
/**
* @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', '<27>0:1<>'),
const MSG_div_raw = goog.getMsg('{$COUNT_1} is rendered as: {$START_BOLD_TEXT_1}{$ICU}{$END_BOLD_TEXT_1}', {
ICU: MSG_div_icu,
COUNT: '<27>0:1<>',
START_BOLD_TEXT_1: '<27>*3:1<><31>#1<>',
END_BOLD_TEXT_1: '<27>/#1:1<><31>/*3:1<>',
});
const MSG_div = i18nPostprocess(MSG_div_raw, {VAR_PLURAL: '<27>0:1<>'});
```
NOTE:
- The compiler generates `[BACKUP_MESSAGE_ID:2919330615509803611]` which forces the `goog.getMsg` to use a specific message ID.
@ -1196,9 +1197,38 @@ Resulting in same string which Angular can process:
}<7D>/#1:1<><31>/*3:1<>.
```
### Notice `i18nIcuReplaceVar` function
### Placeholders with multiple values
The `i18nIcuReplaceVar(MSG_div_icu, 'VAR_PLURAL', '<27>0:1<>')` function is needed to replace `VAR_PLURAL` for `<60>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.
While extracting messages via `ng xi18n`, the tool performs an optimization and reuses the same placeholders for elements/interpolations in case placeholder content is identical.
For example the following template:
```html
<b>My text 1</b><b>My text 2</b>
```
is transformed into:
```html
{$START_TAG_BOLD}My text 1{$CLOSE_TAG_BOLD}{$START_TAG_BOLD}My text 2{$CLOSE_TAG_BOLD}
```
In IVY we need to have specific element instruction indices for open and close tags, so the result string (that can be consumed by `i18nStart`) produced, should look like this:
```html
<EFBFBD>#1<>My text 1<>/#1<><31>#2<>My text 1<>/#2<>
```
In order to resolve this, we need to supply all values that a given placeholder represents and invoke post processing function to transform intermediate string into its final version.
In this case the `goog.getMsg` invocation will look like this:
```typescript
/**
* @desc [BACKUP_MESSAGE_ID:2919330615509803611] Some description.
*/
const MSG_div_raw = goog.getMsg('{$START_TAG_BOLD}My text 1{$CLOSE_TAG_BOLD}{$START_TAG_BOLD}My text 2{$CLOSE_TAG_BOLD}', {
START_TAG_BOLD: '[<5B>#1<>|<7C>#2<>]',
CLOSE_TAG_BOLD: '[<5B>/#2<>|<7C>/#1<>]'
});
const MSG_div = i18nPostprocess(MSG_div_raw);
```
### `i18nPostprocess` function
Due to backwards-compatibility requirements and some limitations of `goog.getMsg`, in some cases we need to run post process to convert intermediate string into its final version that can be consumed by Ivy runtime code (something that `i18nStart` can understand), specifically:
- we replace all `VAR_PLURAL` and `VAR_SELECT` with respective values. 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.
- in some cases, ICUs may share the same placeholder name (like `ICU_1`). For this scenario we inject a special markers (`<60>I18N_EXP_ICU<43>) into a string and resolve this within the post processing function
- this function also resolves the case when one placeholder is used to represent multiple elements (see example above)

View File

@ -30,6 +30,11 @@ const PH_REGEXP = /<2F>(\/?[#*]\d+):?\d*<2A>/gi;
const BINDING_REGEXP = /<2F>(\d+):?\d*<2A>/gi;
const ICU_REGEXP = /({\s*<2A>\d+<2B>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
// i18nPostproocess regexps
const PP_PLACEHOLDERS = /\[(<28>.+?<3F>?)\]/g;
const PP_ICU_VARS = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
const PP_ICUS = /<2F>I18N_EXP_(ICU(_\d+)?)<29>/g;
interface IcuExpression {
type: IcuType;
mainBinding: number;
@ -482,6 +487,77 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
return tNode;
}
/**
* Handles message string post-processing for internationalization.
*
* Handles message string post-processing by transforming it from intermediate
* format (that might contain some markers that we need to replace) to the final
* form, consumable by i18nStart instruction. Post processing steps include:
*
* 1. Resolve all multi-value cases (like [<5B>*1:1<><31>#2:1<>|<7C>#4:1<>|<7C>5<EFBFBD>])
* 2. Replace all ICU vars (like "VAR_PLURAL")
* 3. Replace all ICU references with corresponding values (like <20>ICU_EXP_ICU_1<5F>)
* in case multiple ICUs have the same placeholder name
*
* @param message Raw translation string for post processing
* @param replacements Set of replacements that should be applied
*
* @returns Transformed string that can be consumed by i18nStart instruction
*
* @publicAPI
*/
export function i18nPostprocess(
message: string, replacements: {[key: string]: (string | string[])}): string {
//
// Step 1: resolve all multi-value cases (like [<5B>*1:1<><31>#2:1<>|<7C>#4:1<>|<7C>5<EFBFBD>])
//
const matches: {[key: string]: string[]} = {};
let result = message.replace(PP_PLACEHOLDERS, (_match, content: string): string => {
if (!matches[content]) {
matches[content] = content.split('|');
}
if (!matches[content].length) {
throw new Error(`i18n postprocess: unmatched placeholder - ${content}`);
}
return matches[content].shift() !;
});
// verify that we injected all values
const hasUnmatchedValues = Object.keys(matches).some(key => !!matches[key].length);
if (hasUnmatchedValues) {
throw new Error(`i18n postprocess: unmatched values - ${JSON.stringify(matches)}`);
}
// return current result if no replacements specified
if (!Object.keys(replacements).length) {
return result;
}
//
// Step 2: replace all ICU vars (like "VAR_PLURAL")
//
result = result.replace(PP_ICU_VARS, (match, start, key, _type, _idx, end): string => {
return replacements.hasOwnProperty(key) ? `${start}${replacements[key]}${end}` : match;
});
//
// Step 3: replace all ICU references with corresponding values (like <20>ICU_EXP_ICU_1<5F>)
// in case multiple ICUs have the same placeholder name
//
result = result.replace(PP_ICUS, (match, key): string => {
if (replacements.hasOwnProperty(key)) {
const list = replacements[key] as string[];
if (!list.length) {
throw new Error(`i18n postprocess: unmatched ICU - ${match} with key: ${key}`);
}
return list.shift() !;
}
return match;
});
return result;
}
/**
* 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.
@ -1433,27 +1509,4 @@ function parseNodes(
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
}
}
}
const RAW_ICU_REGEXP = /{\s*(\S*)\s*,\s*\S{6}\s*,[\s\S]*}/gi;
/**
* Replaces the variable parameter (main binding) of an ICU by a given value.
*
* 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: "<22>0<EFBFBD>" });
* // --> MSG_APP_1 = "{<7B>0<EFBFBD>, select, male {male} female {female} other {other}}"
* ```
*/
export function i18nIcuReplaceVars(message: string, replacements: {[key: string]: string}): string {
const keys = Object.keys(replacements);
function replaceFn(replacement: string) {
return (str: string, varMatch: string) => { return str.replace(varMatch, replacement); };
}
for (let i = 0; i < keys.length; i++) {
message = message.replace(RAW_ICU_REGEXP, replaceFn(replacements[keys[i]]));
}
return message;
}
}

View File

@ -87,12 +87,13 @@ export {
} from './state';
export {
i18n,
i18nAttributes,
i18nExp,
i18nStart,
i18nEnd,
i18nApply,
i18nIcuReplaceVars,
i18nPostprocess
} from './i18n';
export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';

View File

@ -97,11 +97,13 @@ export const angularCoreEnv: {[name: string]: Function} = {
'ɵtextBinding': r3.textBinding,
'ɵembeddedViewStart': r3.embeddedViewStart,
'ɵembeddedViewEnd': r3.embeddedViewEnd,
'ɵi18n': r3.i18n,
'ɵi18nAttributes': r3.i18nAttributes,
'ɵi18nExp': r3.i18nExp,
'ɵi18nStart': r3.i18nStart,
'ɵi18nEnd': r3.i18nEnd,
'ɵi18nApply': r3.i18nApply,
'ɵi18nPostprocess': r3.i18nPostprocess,
'ɵsanitizeHtml': sanitization.sanitizeHtml,
'ɵsanitizeStyle': sanitization.sanitizeStyle,