feat(ivy): support i18n without closure (#28689)

So far using runtime i18n with ivy meant that you needed to use Closure and `goog.getMsg` (or a polyfill). This PR changes the compiler to output both closure & non-closure code, while the unused option will be tree-shaken by minifiers.
This means that if you use the Angular CLI with ivy and load a translations file, you can use i18n and the application will not throw at runtime.
For now it will not translate your application, but at least you can try ivy without having to remove all of your i18n code and configuration.
PR Close #28689
This commit is contained in:
Olivier Combe 2019-04-11 11:17:49 +02:00 committed by Igor Minar
parent 387fbb8106
commit 91c7b451d5
22 changed files with 1397 additions and 564 deletions

View File

@ -1887,7 +1887,7 @@ describe('ngtsc behavioral tests', () => {
`); `);
env.driveMain(); env.driveMain();
const jsContents = env.getContents('test.js'); const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i18n(1, MSG_EXTERNAL_8321000940098097247$$TEST_TS_0);'); expect(jsContents).toContain('MSG_EXTERNAL_8321000940098097247$$TEST_TS_1');
}); });
it('should take i18nUseExternalIds config option into account', () => { it('should take i18nUseExternalIds config option into account', () => {
@ -1902,7 +1902,7 @@ describe('ngtsc behavioral tests', () => {
`); `);
env.driveMain(); env.driveMain();
const jsContents = env.getContents('test.js'); const jsContents = env.getContents('test.js');
expect(jsContents).toContain('i18n(1, MSG_TEST_TS_0);'); expect(jsContents).not.toContain('MSG_EXTERNAL_');
}); });
it('@Component\'s `interpolation` should override default interpolation config', () => { it('@Component\'s `interpolation` should override default interpolation config', () => {

View File

@ -130,6 +130,7 @@ export class Identifiers {
static i18nEnd: o.ExternalReference = {name: 'Δi18nEnd', moduleName: CORE}; static i18nEnd: o.ExternalReference = {name: 'Δi18nEnd', moduleName: CORE};
static i18nApply: o.ExternalReference = {name: 'Δi18nApply', moduleName: CORE}; static i18nApply: o.ExternalReference = {name: 'Δi18nApply', moduleName: CORE};
static i18nPostprocess: o.ExternalReference = {name: 'Δi18nPostprocess', moduleName: CORE}; static i18nPostprocess: o.ExternalReference = {name: 'Δi18nPostprocess', moduleName: CORE};
static i18nLocalize: o.ExternalReference = {name: 'Δi18nLocalize', moduleName: CORE};
static load: o.ExternalReference = {name: 'Δload', moduleName: CORE}; static load: o.ExternalReference = {name: 'Δload', moduleName: CORE};

View File

@ -11,19 +11,21 @@ import {toPublicName} from '../../../i18n/serializers/xmb';
import * as html from '../../../ml_parser/ast'; import * as html from '../../../ml_parser/ast';
import {mapLiteral} from '../../../output/map_util'; import {mapLiteral} from '../../../output/map_util';
import * as o from '../../../output/output_ast'; import * as o from '../../../output/output_ast';
import {Identifiers as R3} from '../../r3_identifiers';
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */ /* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
const CLOSURE_TRANSLATION_PREFIX = 'MSG_'; const CLOSURE_TRANSLATION_PREFIX = 'MSG_';
const CLOSURE_TRANSLATION_MATCHER_REGEXP = new RegExp(`^${CLOSURE_TRANSLATION_PREFIX}`);
/* Prefix for non-`goog.getMsg` i18n-related vars */ /* Prefix for non-`goog.getMsg` i18n-related vars */
const TRANSLATION_PREFIX = 'I18N_'; export const TRANSLATION_PREFIX = 'I18N_';
/** Closure uses `goog.getMsg(message)` to lookup translations */ /** Closure uses `goog.getMsg(message)` to lookup translations */
const GOOG_GET_MSG = 'goog.getMsg'; const GOOG_GET_MSG = 'goog.getMsg';
/** Name of the global variable that is used to determine if we use Closure translations or not */
const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode';
/** I18n separators for metadata **/ /** I18n separators for metadata **/
const I18N_MEANING_SEPARATOR = '|'; const I18N_MEANING_SEPARATOR = '|';
const I18N_ID_SEPARATOR = '@@'; const I18N_ID_SEPARATOR = '@@';
@ -48,17 +50,36 @@ export type I18nMeta = {
}; };
function i18nTranslationToDeclStmt( function i18nTranslationToDeclStmt(
variable: o.ReadVarExpr, message: string, variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
params?: {[name: string]: o.Expression}): o.DeclareVarStmt { params?: {[name: string]: o.Expression}): o.Statement[] {
const statements: o.Statement[] = [];
// var I18N_X;
statements.push(
new o.DeclareVarStmt(variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan));
const args = [o.literal(message) as o.Expression]; const args = [o.literal(message) as o.Expression];
if (params && Object.keys(params).length) { if (params && Object.keys(params).length) {
args.push(mapLiteral(params, true)); args.push(mapLiteral(params, true));
} }
const fnCall = o.variable(GOOG_GET_MSG).callFn(args);
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]); // Closure JSDoc comments
const docStatements = i18nMetaToDocStmt(meta);
const thenStatements: o.Statement[] = docStatements ? [docStatements] : [];
const googFnCall = o.variable(GOOG_GET_MSG).callFn(args);
// const MSG_... = goog.getMsg(..);
thenStatements.push(closureVar.set(googFnCall).toConstDecl());
// I18N_X = MSG_...;
thenStatements.push(new o.ExpressionStatement(variable.set(closureVar)));
const localizeFnCall = o.importExpr(R3.i18nLocalize).callFn(args);
// I18N_X = i18nLocalize(...);
const elseStatements = [new o.ExpressionStatement(variable.set(localizeFnCall))];
// if(ngI18nClosureMode) { ... } else { ... }
statements.push(o.ifStmt(o.variable(NG_I18N_CLOSURE_MODE), thenStatements, elseStatements));
return statements;
} }
// Converts i18n meta informations for a message (id, description, meaning) // Converts i18n meta information for a message (id, description, meaning)
// to a JsDoc statement formatted as expected by the Closure compiler. // to a JsDoc statement formatted as expected by the Closure compiler.
function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null { function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
const tags: o.JSDocTag[] = []; const tags: o.JSDocTag[] = [];
@ -231,6 +252,7 @@ export function getTranslationConstPrefix(extra: string): string {
* Generates translation declaration statements. * Generates translation declaration statements.
* *
* @param variable Translation value reference * @param variable Translation value reference
* @param closureVar Variable for Closure `goog.getMsg` calls
* @param message Text message to be translated * @param message Text message to be translated
* @param meta Object that contains meta information (id, meaning and description) * @param meta Object that contains meta information (id, meaning and description)
* @param params Object with placeholders key-value pairs * @param params Object with placeholders key-value pairs
@ -238,27 +260,16 @@ export function getTranslationConstPrefix(extra: string): string {
* @returns Array of Statements that represent a given translation * @returns Array of Statements that represent a given translation
*/ */
export function getTranslationDeclStmts( export function getTranslationDeclStmts(
variable: o.ReadVarExpr, message: string, meta: I18nMeta, variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
params: {[name: string]: o.Expression} = {}, params: {[name: string]: o.Expression} = {},
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] { transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] {
const statements: o.Statement[] = []; const statements: o.Statement[] = [];
const docStatements = i18nMetaToDocStmt(meta);
if (docStatements) { statements.push(...i18nTranslationToDeclStmt(variable, closureVar, message, meta, params));
statements.push(docStatements);
}
if (transformFn) { if (transformFn) {
statements.push(i18nTranslationToDeclStmt(variable, message, params)); statements.push(new o.ExpressionStatement(variable.set(transformFn(variable))));
// Closure Compiler doesn't allow non-goo.getMsg const names to start with `MSG_`,
// so we update variable name prefix in case post processing is required, so we can
// assign the result of post-processing function to the var that starts with `I18N_`
const raw = o.variable(variable.name !);
variable.name = variable.name !.replace(CLOSURE_TRANSLATION_MATCHER_REGEXP, TRANSLATION_PREFIX);
statements.push(
variable.set(transformFn(raw)).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
} else {
statements.push(i18nTranslationToDeclStmt(variable, message, params));
} }
return statements; return statements;
} }

View File

@ -35,7 +35,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prep
import {I18nContext} from './i18n/context'; import {I18nContext} from './i18n/context';
import {I18nMetaVisitor} from './i18n/meta'; import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer'; import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util'; import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {Instruction, StylingBuilder} from './styling_builder'; import {Instruction, StylingBuilder} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
@ -321,14 +321,18 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
i18nTranslate( i18nTranslate(
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr, message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr { transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
const _ref = ref || this.i18nAllocateRef(message.id); const _ref = ref || o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
const closureVar = this.i18nGenerateClosureVar(message.id);
const _params: {[key: string]: any} = {}; const _params: {[key: string]: any} = {};
if (params && Object.keys(params).length) { if (params && Object.keys(params).length) {
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]); Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
} }
const meta = metaFromI18nMessage(message); const meta = metaFromI18nMessage(message);
const content = getSerializedI18nContent(message); const content = getSerializedI18nContent(message);
const statements = getTranslationDeclStmts(_ref, content, meta, _params, transformFn); const statements =
getTranslationDeclStmts(_ref, closureVar, content, meta, _params, transformFn);
this.constantPool.statements.push(...statements); this.constantPool.statements.push(...statements);
return _ref; return _ref;
} }
@ -360,7 +364,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
return bound; return bound;
} }
i18nAllocateRef(messageId: string): o.ReadVarExpr { i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
let name: string; let name: string;
const suffix = this.fileBasedI18nSuffix.toUpperCase(); const suffix = this.fileBasedI18nSuffix.toUpperCase();
if (this.i18nUseExternalIds) { if (this.i18nUseExternalIds) {
@ -424,7 +428,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
if (this.i18nContext) { if (this.i18nContext) {
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta); this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta);
} else { } else {
const ref = this.i18nAllocateRef((meta as i18n.Message).id); const ref = o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta); this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
} }

View File

@ -134,6 +134,8 @@ export {
Δi18nEnd, Δi18nEnd,
Δi18nApply, Δi18nApply,
Δi18nPostprocess, Δi18nPostprocess,
i18nConfigureLocalize as ɵi18nConfigureLocalize,
Δi18nLocalize,
setClassMetadata as ɵsetClassMetadata, setClassMetadata as ɵsetClassMetadata,
ΔresolveWindow, ΔresolveWindow,
ΔresolveDocument, ΔresolveDocument,

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import '../util/ng_i18n_closure_mode';
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer'; import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
import {InertBodyHelper} from '../sanitization/inert_body'; import {InertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer'; import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
@ -1604,3 +1606,38 @@ function parseNodes(
} }
} }
} }
let TRANSLATIONS: {[key: string]: string} = {};
export interface I18nLocalizeOptions { translations: {[key: string]: string}; }
/**
* Set the configuration for `i18nLocalize`.
*
* @deprecated this method is temporary & should not be used as it will be removed soon
*/
export function i18nConfigureLocalize(options: I18nLocalizeOptions = {
translations: {}
}) {
TRANSLATIONS = options.translations;
}
const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g;
/**
* 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.
*
* @publicApi
* @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];
}
return Object.keys(placeholders).length ?
input.replace(LOCALIZE_PH_REGEXP, (match, key) => placeholders[key] || '') :
input;
}

View File

@ -107,6 +107,8 @@ export {
Δi18nEnd, Δi18nEnd,
Δi18nApply, Δi18nApply,
Δi18nPostprocess, Δi18nPostprocess,
i18nConfigureLocalize,
Δi18nLocalize,
} from './i18n'; } from './i18n';
export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';

View File

@ -117,6 +117,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'Δi18nEnd': r3.Δi18nEnd, 'Δi18nEnd': r3.Δi18nEnd,
'Δi18nApply': r3.Δi18nApply, 'Δi18nApply': r3.Δi18nApply,
'Δi18nPostprocess': r3.Δi18nPostprocess, 'Δi18nPostprocess': r3.Δi18nPostprocess,
'Δi18nLocalize': r3.Δi18nLocalize,
'ΔresolveWindow': r3.ΔresolveWindow, 'ΔresolveWindow': r3.ΔresolveWindow,
'ΔresolveDocument': r3.ΔresolveDocument, 'ΔresolveDocument': r3.ΔresolveDocument,
'ΔresolveBody': r3.ΔresolveBody, 'ΔresolveBody': r3.ΔresolveBody,

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {global} from './global';
declare global { declare global {
const ngDevMode: null|NgDevModePerfCounters; const ngDevMode: null|NgDevModePerfCounters;
interface NgDevModePerfCounters { interface NgDevModePerfCounters {
@ -38,8 +40,6 @@ declare global {
} }
} }
declare let global: any;
export function ngDevModeResetPerfCounters(): NgDevModePerfCounters { export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
const newCounters: NgDevModePerfCounters = { const newCounters: NgDevModePerfCounters = {
firstTemplatePass: 0, firstTemplatePass: 0,
@ -69,31 +69,20 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
stylingApply: 0, stylingApply: 0,
stylingApplyCacheMiss: 0, stylingApplyCacheMiss: 0,
}; };
// NOTE: Under Ivy we may have both window & global defined in the Node
// environment since ensureDocument() in render3.ts sets global.window. // Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
if (typeof window != 'undefined') { global['ngDevMode'] = newCounters;
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
(window as any)['ngDevMode'] = newCounters;
}
if (typeof global != 'undefined') {
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
(global as any)['ngDevMode'] = newCounters;
}
if (typeof self != 'undefined') {
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
(self as any)['ngDevMode'] = newCounters;
}
return newCounters; return newCounters;
} }
/** /**
* This checks to see if the `ngDevMode` has been set. If yes, * This checks to see if the `ngDevMode` has been set. If yes,
* than we honor it, otherwise we default to dev mode with additional checks. * then we honor it, otherwise we default to dev mode with additional checks.
* *
* The idea is that unless we are doing production build where we explicitly * The idea is that unless we are doing production build where we explicitly
* set `ngDevMode == false` we should be helping the developer by providing * set `ngDevMode == false` we should be helping the developer by providing
* as much early warning and errors as possible. * as much early warning and errors as possible.
*/ */
if (typeof ngDevMode === 'undefined' || ngDevMode) { if (typeof global['ngDevMode'] === 'undefined' || global['ngDevMode']) {
ngDevModeResetPerfCounters(); ngDevModeResetPerfCounters();
} }

View File

@ -0,0 +1,19 @@
/**
* @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
*/
import {global} from './global';
declare global {
const ngI18nClosureMode: boolean;
}
if (typeof global['ngI18nClosureMode'] === 'undefined') {
// Make sure to refer to ngI18nClosureMode as ['ngI18nClosureMode'] for closure.
global['ngI18nClosureMode'] =
typeof global['goog'] !== 'undefined' && typeof global['goog'].getMsg === 'function';
}

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {Component, Directive, NO_ERRORS_SCHEMA, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled, onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
describe('ViewContainerRef', () => { describe('ViewContainerRef', () => {
@ -22,7 +22,7 @@ describe('ViewContainerRef', () => {
}; };
beforeEach(() => { beforeEach(() => {
polyfillGoogGetMsg(TRANSLATIONS); ɵi18nConfigureLocalize({translations: TRANSLATIONS});
TestBed.configureTestingModule( TestBed.configureTestingModule(
{declarations: [StructDir, ViewContainerRefComp, ViewContainerRefApp, DestroyCasesComp]}); {declarations: [StructDir, ViewContainerRefComp, ViewContainerRefApp, DestroyCasesComp]});
}); });

View File

@ -23,14 +23,6 @@
property renaming. property renaming.
--> -->
<script> <script>
// `goog.getMsg()` will be provided by Closure
const translations = {
'Hello World!': 'Bonjour Monde!',
'Hello Title!': 'Bonjour Titre!',
};
window.goog = window.goog || {};
window.goog.getMsg = (key) => translations[key] || key;
document.write('<script src="' + document.write('<script src="' +
(document.location.search.endsWith('debug') ? '/bundle.min_debug.js' : '/bundle.min.js') + (document.location.search.endsWith('debug') ? '/bundle.min_debug.js' : '/bundle.min.js') +
'"></' + 'script>'); '"></' + 'script>');

View File

@ -6,7 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, NgModule, ɵrenderComponent as renderComponent} from '@angular/core'; import {Component, NgModule, ɵi18nConfigureLocalize, ɵrenderComponent as renderComponent} from '@angular/core';
const translations = {
'Hello World!': 'Bonjour Monde!',
'Hello Title!': 'Bonjour Titre!',
};
ɵi18nConfigureLocalize({translations});
@Component({ @Component({
selector: 'hello-world', selector: 'hello-world',

View File

@ -116,6 +116,9 @@
{ {
"name": "_currentInjector" "name": "_currentInjector"
}, },
{
"name": "_global"
},
{ {
"name": "catchInjectorError" "name": "catchInjectorError"
}, },
@ -140,6 +143,9 @@
{ {
"name": "getClosureSafeProperty" "name": "getClosureSafeProperty"
}, },
{
"name": "getGlobal"
},
{ {
"name": "getInjectableDef" "name": "getInjectableDef"
}, },

View File

@ -7,10 +7,16 @@
*/ */
import '@angular/core/test/bundling/util/src/reflect_metadata'; import '@angular/core/test/bundling/util/src/reflect_metadata';
/**
* TODO(ocombe): replace this with the real runtime i18n service configuration
* For now we define inline translations that are added with the function `ɵi18nConfigureLocalize`,
* but this function will go away once we have finished designing and implementing the new runtime
* service. At this point we should revisit this code and update it to use that new service.
* See FW-114.
*/
import './translations';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent} from '@angular/core'; import {Component, Injectable, NgModule, ViewEncapsulation, ɵmarkDirty as markDirty, ɵrenderComponent as renderComponent, Δi18nLocalize as localize} from '@angular/core';
// TODO(ocombe): replace this with the real runtime i18n service
import {localize} from './translations';
class Todo { class Todo {
editing: boolean; editing: boolean;

View File

@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
declare var global: any; import {ɵi18nConfigureLocalize} from '@angular/core';
declare var window: any;
export const translations: {[key: string]: string} = { export const translations: {[key: string]: string} = {
'What needs to be done?': `Qu'y a-t-il à faire ?`, 'What needs to be done?': `Qu'y a-t-il à faire ?`,
@ -25,18 +24,4 @@ export const translations: {[key: string]: string} = {
'Demonstrate internationalization': `Démontrer l'internationalisation` 'Demonstrate internationalization': `Démontrer l'internationalisation`
}; };
// Runtime i18n uses Closure goog.getMsg for now ɵi18nConfigureLocalize({translations});
// It will be replaced by the runtime service for external people
const glob = typeof global !== 'undefined' ? global : window;
glob.goog = glob.goog || {};
glob.goog.getMsg =
glob.goog.getMsg || function(input: string, placeholders: {[key: string]: string} = {}) {
if (typeof translations[input] !== 'undefined') { // to account for empty string
input = translations[input];
}
return Object.keys(placeholders).length ?
input.replace(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') :
input;
};
export const localize = goog.getMsg;

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; import {Component, ContentChild, ContentChildren, Directive, QueryList, TemplateRef, ViewChild, ViewContainerRef, ɵi18nConfigureLocalize} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy, polyfillGoogGetMsg} from '@angular/private/testing'; import {onlyInIvy} from '@angular/private/testing';
@Directive({ @Directive({
selector: '[tplRef]', selector: '[tplRef]',
@ -79,7 +79,7 @@ const getFixtureWithOverrides = (overrides = {}) => {
onlyInIvy('Ivy i18n logic').describe('i18n', function() { onlyInIvy('Ivy i18n logic').describe('i18n', function() {
beforeEach(() => { beforeEach(() => {
polyfillGoogGetMsg(TRANSLATIONS); ɵi18nConfigureLocalize({translations: TRANSLATIONS});
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTplRef]}); TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTplRef]});
}); });

View File

@ -11,7 +11,7 @@ import {AfterContentInit, AfterViewInit, Component, ContentChildren, Directive,
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {modifiedInIvy, polyfillGoogGetMsg} from '@angular/private/testing'; import {modifiedInIvy} from '@angular/private/testing';
if (ivyEnabled) { if (ivyEnabled) {
describe('ivy', () => { declareTests(); }); describe('ivy', () => { declareTests(); });
@ -24,11 +24,6 @@ function declareTests(config?: {useJit: boolean}) {
describe('<ng-container>', function() { describe('<ng-container>', function() {
beforeEach(() => { beforeEach(() => {
// Injecting goog.getMsg-like function into global scope to unblock tests run outside of
// Closure Compiler. It's a *temporary* measure until runtime translation service support is
// introduced.
polyfillGoogGetMsg();
TestBed.configureCompiler({...config}); TestBed.configureCompiler({...config});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ declarations: [

View File

@ -7,5 +7,4 @@
*/ */
export * from './src/render3'; export * from './src/render3';
export * from './src/goog_get_msg';
export * from './src/ivy_test_selectors'; export * from './src/ivy_test_selectors';

View File

@ -1,28 +0,0 @@
/**
* @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
*/
/**
* A method that injects goog.getMsg-like function into global scope.
*
* 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.
*/
export function polyfillGoogGetMsg(translations: {[key: string]: string} = {}): void {
const glob = (global as any);
glob.goog = glob.goog || {};
glob.goog.getMsg = function(input: string, placeholders: {[key: string]: string} = {}) {
if (typeof translations[input] !== 'undefined') { // to account for
// empty string
input = translations[input];
}
return Object.keys(placeholders).length ?
input.replace(/\{\$(.*?)\}/g, (match, key) => placeholders[key] || '') :
input;
};
}

View File

@ -1236,6 +1236,11 @@ export declare function Δi18nEnd(): void;
export declare function Δi18nExp<T>(expression: T | NO_CHANGE): void; export declare function Δi18nExp<T>(expression: T | NO_CHANGE): void;
/** @deprecated */
export declare function Δi18nLocalize(input: string, placeholders?: {
[key: string]: string;
}): string;
export declare function Δi18nPostprocess(message: string, replacements?: { export declare function Δi18nPostprocess(message: string, replacements?: {
[key: string]: (string | string[]); [key: string]: (string | string[]);
}): string; }): string;