refactor(ivy): update the compiler to emit $localize tags (#31609)

This commit changes the Angular compiler (ivy-only) to generate `$localize`
tagged strings for component templates that use `i18n` attributes.

BREAKING CHANGE

Since `$localize` is a global function, it must be included in any applications
that use i18n. This is achieved by importing the `@angular/localize` package
into an appropriate bundle, where it will be executed before the renderer
needs to call `$localize`. For CLI based projects, this is best done in
the `polyfills.ts` file.

```ts
import '@angular/localize';
```

For non-CLI applications this could be added as a script to the index.html
file or another suitable script file.

PR Close #31609
This commit is contained in:
Pete Bacon Darwin
2019-07-30 18:02:17 +01:00
committed by Misko Hevery
parent b21397bde9
commit fa79f51645
35 changed files with 1173 additions and 583 deletions

View File

@ -37,3 +37,14 @@ export * from './core_render3_private_export';
export {SecurityContext} from './sanitization/security';
export {Sanitizer} from './sanitization/sanitizer';
export * from './codegen_private_exports';
import {global} from './util/global';
if (ngDevMode) {
// This helper is to give a reasonable error message to people upgrading to v9 that have not yet
// installed `@angular/localize` in their app.
// tslint:disable-next-line: no-toplevel-property-access
global.$localize = global.$localize || function() {
throw new Error(
'The global function `$localize` is missing. Please add `import \'@angular/localize\';` to your polyfills.ts file.');
};
}

View File

@ -5,7 +5,6 @@
* 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
*/
import '../util/ng_i18n_closure_mode';
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
@ -13,6 +12,7 @@ import {InertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {addAllToArray} from '../util/array_utils';
import {assertDataInRange, assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
import {global} from '../util/global';
import {attachPatchData} from './context_discovery';
import {bind, setDelayProjection} from './instructions/all';
import {attachI18nOpCodesDebug} from './instructions/lview_debug';
@ -1317,40 +1317,86 @@ function replaceNgsp(value: string): string {
return value.replace(NGSP_UNICODE_REGEXP, ' ');
}
let TRANSLATIONS: {[key: string]: string} = {};
export interface I18nLocalizeOptions { translations: {[key: string]: string}; }
/**
* Set the configuration for `i18nLocalize`.
* Provide translations for `$localize`.
*
* @deprecated this method is temporary & should not be used as it will be removed soon
*/
export function i18nConfigureLocalize(options: I18nLocalizeOptions = {
translations: {}
}) {
TRANSLATIONS = options.translations;
}
type TranslationInfo = {messageParts: TemplateStringsArray, placeholderNames: string[]};
type MessageInfo = {translationKey: string, replacements: {[placeholderName: string]: any}};
const PLACEHOLDER_MARKER = ':';
const TRANSLATIONS: {[key: string]: TranslationInfo} = {};
const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g;
Object.keys(options.translations).forEach(key => {
TRANSLATIONS[key] = splitMessage(options.translations[key]);
});
/**
* A goog.getMsg-like function for users that do not use Closure.
*
* This method is required as a *temporary* measure to prevent i18n tests from being blocked while
* running outside of Closure Compiler. This method will not be needed once runtime translation
* service support is introduced.
*
* @codeGenApi
* @deprecated this method is temporary & should not be used as it will be removed soon
*/
export function ɵɵi18nLocalize(input: string, placeholders?: {[key: string]: string}) {
if (typeof TRANSLATIONS[input] !== 'undefined') { // to account for empty string
input = TRANSLATIONS[input];
if (ngDevMode) {
if (global.$localize === undefined) {
throw new Error(
'The global function `$localize` is missing. Please add `import \'@angular/localize\';` to your polyfills.ts file.');
}
}
if (placeholders !== undefined && Object.keys(placeholders).length) {
return input.replace(LOCALIZE_PH_REGEXP, (_, key) => placeholders[key] || '');
$localize.translate = function(messageParts: TemplateStringsArray, expressions: readonly any[]):
[TemplateStringsArray, readonly any[]] {
const message = parseMessage(messageParts, expressions);
const translation = TRANSLATIONS[message.translationKey];
const result: [TemplateStringsArray, readonly any[]] =
(translation === undefined ? [messageParts, expressions] : [
translation.messageParts,
translation.placeholderNames.map(placeholder => message.replacements[placeholder])
]);
return result;
};
function splitMessage(message: string): TranslationInfo {
const parts = message.split(/{\$([^}]*)}/);
const messageParts = [parts[0]];
const placeholderNames: string[] = [];
for (let i = 1; i < parts.length - 1; i += 2) {
placeholderNames.push(parts[i]);
messageParts.push(parts[i + 1]);
}
const rawMessageParts =
messageParts.map(part => part.charAt(0) === PLACEHOLDER_MARKER ? '\\' + part : part);
return {messageParts: makeTemplateObject(messageParts, rawMessageParts), placeholderNames};
}
function parseMessage(
messageParts: TemplateStringsArray, expressions: readonly any[]): MessageInfo {
const PLACEHOLDER_NAME_MARKER = ':';
const replacements: {[placeholderName: string]: any} = {};
let translationKey = messageParts[0];
for (let i = 1; i < messageParts.length; i++) {
const messagePart = messageParts[i];
const expression = expressions[i - 1];
// There is a problem with synthesized template literals in TS where the raw version
// cannot be found, since there is no original source code to read it from.
// In that case we just fall back on the non-raw version.
// This should be OK because synthesized nodes (from the template compiler) will always have
// placeholder names provided.
if ((messageParts.raw[i] || messagePart).charAt(0) === PLACEHOLDER_NAME_MARKER) {
const endOfPlaceholderName = messagePart.indexOf(PLACEHOLDER_NAME_MARKER, 1);
const placeholderName = messagePart.substring(1, endOfPlaceholderName);
translationKey += `{$${placeholderName}}${messagePart.substring(endOfPlaceholderName + 1)}`;
replacements[placeholderName] = expression;
} else {
translationKey += messagePart;
replacements[`ph_${i}`] = expression;
}
}
return {translationKey, replacements};
}
function makeTemplateObject(cooked: string[], raw: string[]): TemplateStringsArray {
Object.defineProperty(cooked, 'raw', {value: raw});
return cooked as any;
}
return input;
}
/**

View File

@ -149,7 +149,6 @@ export {
ɵɵi18nApply,
ɵɵi18nPostprocess,
i18nConfigureLocalize,
ɵɵi18nLocalize,
getLocaleId,
setLocaleId,
} from './i18n';