
committed by
Misko Hevery

parent
a4934a74b6
commit
92e80af875
@ -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';
|
||||
|
@ -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 | ❌ | ❌ | ❌ |
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user