Skip to content

Commit 91c7b45

Browse files
ocombeIgorMinar
authored andcommitted
feat(ivy): support i18n without closure (angular#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 angular#28689
1 parent 387fbb8 commit 91c7b45

File tree

22 files changed

+1397
-564
lines changed

22 files changed

+1397
-564
lines changed

packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts

Lines changed: 1243 additions & 443 deletions
Large diffs are not rendered by default.

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,7 +1887,7 @@ describe('ngtsc behavioral tests', () => {
18871887
`);
18881888
env.driveMain();
18891889
const jsContents = env.getContents('test.js');
1890-
expect(jsContents).toContain('i18n(1, MSG_EXTERNAL_8321000940098097247$$TEST_TS_0);');
1890+
expect(jsContents).toContain('MSG_EXTERNAL_8321000940098097247$$TEST_TS_1');
18911891
});
18921892

18931893
it('should take i18nUseExternalIds config option into account', () => {
@@ -1902,7 +1902,7 @@ describe('ngtsc behavioral tests', () => {
19021902
`);
19031903
env.driveMain();
19041904
const jsContents = env.getContents('test.js');
1905-
expect(jsContents).toContain('i18n(1, MSG_TEST_TS_0);');
1905+
expect(jsContents).not.toContain('MSG_EXTERNAL_');
19061906
});
19071907

19081908
it('@Component\'s `interpolation` should override default interpolation config', () => {

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export class Identifiers {
130130
static i18nEnd: o.ExternalReference = {name: 'Δi18nEnd', moduleName: CORE};
131131
static i18nApply: o.ExternalReference = {name: 'Δi18nApply', moduleName: CORE};
132132
static i18nPostprocess: o.ExternalReference = {name: 'Δi18nPostprocess', moduleName: CORE};
133+
static i18nLocalize: o.ExternalReference = {name: 'Δi18nLocalize', moduleName: CORE};
133134

134135
static load: o.ExternalReference = {name: 'Δload', moduleName: CORE};
135136

packages/compiler/src/render3/view/i18n/util.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@ import {toPublicName} from '../../../i18n/serializers/xmb';
1111
import * as html from '../../../ml_parser/ast';
1212
import {mapLiteral} from '../../../output/map_util';
1313
import * as o from '../../../output/output_ast';
14+
import {Identifiers as R3} from '../../r3_identifiers';
1415

1516

1617
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
1718
const CLOSURE_TRANSLATION_PREFIX = 'MSG_';
18-
const CLOSURE_TRANSLATION_MATCHER_REGEXP = new RegExp(`^${CLOSURE_TRANSLATION_PREFIX}`);
1919

2020
/* Prefix for non-`goog.getMsg` i18n-related vars */
21-
const TRANSLATION_PREFIX = 'I18N_';
22-
21+
export const TRANSLATION_PREFIX = 'I18N_';
2322

2423
/** Closure uses `goog.getMsg(message)` to lookup translations */
2524
const GOOG_GET_MSG = 'goog.getMsg';
2625

26+
/** Name of the global variable that is used to determine if we use Closure translations or not */
27+
const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode';
28+
2729
/** I18n separators for metadata **/
2830
const I18N_MEANING_SEPARATOR = '|';
2931
const I18N_ID_SEPARATOR = '@@';
@@ -48,17 +50,36 @@ export type I18nMeta = {
4850
};
4951

5052
function i18nTranslationToDeclStmt(
51-
variable: o.ReadVarExpr, message: string,
52-
params?: {[name: string]: o.Expression}): o.DeclareVarStmt {
53+
variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
54+
params?: {[name: string]: o.Expression}): o.Statement[] {
55+
const statements: o.Statement[] = [];
56+
// var I18N_X;
57+
statements.push(
58+
new o.DeclareVarStmt(variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan));
59+
5360
const args = [o.literal(message) as o.Expression];
5461
if (params && Object.keys(params).length) {
5562
args.push(mapLiteral(params, true));
5663
}
57-
const fnCall = o.variable(GOOG_GET_MSG).callFn(args);
58-
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
64+
65+
// Closure JSDoc comments
66+
const docStatements = i18nMetaToDocStmt(meta);
67+
const thenStatements: o.Statement[] = docStatements ? [docStatements] : [];
68+
const googFnCall = o.variable(GOOG_GET_MSG).callFn(args);
69+
// const MSG_... = goog.getMsg(..);
70+
thenStatements.push(closureVar.set(googFnCall).toConstDecl());
71+
// I18N_X = MSG_...;
72+
thenStatements.push(new o.ExpressionStatement(variable.set(closureVar)));
73+
const localizeFnCall = o.importExpr(R3.i18nLocalize).callFn(args);
74+
// I18N_X = i18nLocalize(...);
75+
const elseStatements = [new o.ExpressionStatement(variable.set(localizeFnCall))];
76+
// if(ngI18nClosureMode) { ... } else { ... }
77+
statements.push(o.ifStmt(o.variable(NG_I18N_CLOSURE_MODE), thenStatements, elseStatements));
78+
79+
return statements;
5980
}
6081

61-
// Converts i18n meta informations for a message (id, description, meaning)
82+
// Converts i18n meta information for a message (id, description, meaning)
6283
// to a JsDoc statement formatted as expected by the Closure compiler.
6384
function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
6485
const tags: o.JSDocTag[] = [];
@@ -231,34 +252,24 @@ export function getTranslationConstPrefix(extra: string): string {
231252
* Generates translation declaration statements.
232253
*
233254
* @param variable Translation value reference
255+
* @param closureVar Variable for Closure `goog.getMsg` calls
234256
* @param message Text message to be translated
235257
* @param meta Object that contains meta information (id, meaning and description)
236258
* @param params Object with placeholders key-value pairs
237259
* @param transformFn Optional transformation (post processing) function reference
238260
* @returns Array of Statements that represent a given translation
239261
*/
240262
export function getTranslationDeclStmts(
241-
variable: o.ReadVarExpr, message: string, meta: I18nMeta,
263+
variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
242264
params: {[name: string]: o.Expression} = {},
243265
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] {
244266
const statements: o.Statement[] = [];
245-
const docStatements = i18nMetaToDocStmt(meta);
246-
if (docStatements) {
247-
statements.push(docStatements);
248-
}
267+
268+
statements.push(...i18nTranslationToDeclStmt(variable, closureVar, message, meta, params));
269+
249270
if (transformFn) {
250-
statements.push(i18nTranslationToDeclStmt(variable, message, params));
251-
252-
// Closure Compiler doesn't allow non-goo.getMsg const names to start with `MSG_`,
253-
// so we update variable name prefix in case post processing is required, so we can
254-
// assign the result of post-processing function to the var that starts with `I18N_`
255-
const raw = o.variable(variable.name !);
256-
variable.name = variable.name !.replace(CLOSURE_TRANSLATION_MATCHER_REGEXP, TRANSLATION_PREFIX);
257-
258-
statements.push(
259-
variable.set(transformFn(raw)).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
260-
} else {
261-
statements.push(i18nTranslationToDeclStmt(variable, message, params));
271+
statements.push(new o.ExpressionStatement(variable.set(transformFn(variable))));
262272
}
273+
263274
return statements;
264-
}
275+
}

packages/compiler/src/render3/view/template.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prep
3535
import {I18nContext} from './i18n/context';
3636
import {I18nMetaVisitor} from './i18n/meta';
3737
import {getSerializedI18nContent} from './i18n/serializer';
38-
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
38+
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
3939
import {Instruction, StylingBuilder} from './styling_builder';
4040
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
4141

@@ -321,14 +321,18 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
321321
i18nTranslate(
322322
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
323323
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
324-
const _ref = ref || this.i18nAllocateRef(message.id);
324+
const _ref = ref || o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
325+
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
326+
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
327+
const closureVar = this.i18nGenerateClosureVar(message.id);
325328
const _params: {[key: string]: any} = {};
326329
if (params && Object.keys(params).length) {
327330
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
328331
}
329332
const meta = metaFromI18nMessage(message);
330333
const content = getSerializedI18nContent(message);
331-
const statements = getTranslationDeclStmts(_ref, content, meta, _params, transformFn);
334+
const statements =
335+
getTranslationDeclStmts(_ref, closureVar, content, meta, _params, transformFn);
332336
this.constantPool.statements.push(...statements);
333337
return _ref;
334338
}
@@ -360,7 +364,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
360364
return bound;
361365
}
362366

363-
i18nAllocateRef(messageId: string): o.ReadVarExpr {
367+
i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
364368
let name: string;
365369
const suffix = this.fileBasedI18nSuffix.toUpperCase();
366370
if (this.i18nUseExternalIds) {
@@ -424,7 +428,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
424428
if (this.i18nContext) {
425429
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta);
426430
} else {
427-
const ref = this.i18nAllocateRef((meta as i18n.Message).id);
431+
const ref = o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
428432
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
429433
}
430434

packages/core/src/core_render3_private_export.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export {
134134
Δi18nEnd,
135135
Δi18nApply,
136136
Δi18nPostprocess,
137+
i18nConfigureLocalize as ɵi18nConfigureLocalize,
138+
Δi18nLocalize,
137139
setClassMetadata as ɵsetClassMetadata,
138140
ΔresolveWindow,
139141
ΔresolveDocument,

packages/core/src/render3/i18n.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import '../util/ng_i18n_closure_mode';
10+
911
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
1012
import {InertBodyHelper} from '../sanitization/inert_body';
1113
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
@@ -1604,3 +1606,38 @@ function parseNodes(
16041606
}
16051607
}
16061608
}
1609+
1610+
let TRANSLATIONS: {[key: string]: string} = {};
1611+
export interface I18nLocalizeOptions { translations: {[key: string]: string}; }
1612+
1613+
/**
1614+
* Set the configuration for `i18nLocalize`.
1615+
*
1616+
* @deprecated this method is temporary & should not be used as it will be removed soon
1617+
*/
1618+
export function i18nConfigureLocalize(options: I18nLocalizeOptions = {
1619+
translations: {}
1620+
}) {
1621+
TRANSLATIONS = options.translations;
1622+
}
1623+
1624+
const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g;
1625+
1626+
/**
1627+
* A goog.getMsg-like function for users that do not use Closure.
1628+
*
1629+
* This method is required as a *temporary* measure to prevent i18n tests from being blocked while
1630+
* running outside of Closure Compiler. This method will not be needed once runtime translation
1631+
* service support is introduced.
1632+
*
1633+
* @publicApi
1634+
* @deprecated this method is temporary & should not be used as it will be removed soon
1635+
*/
1636+
export function Δi18nLocalize(input: string, placeholders: {[key: string]: string} = {}) {
1637+
if (typeof TRANSLATIONS[input] !== 'undefined') { // to account for empty string
1638+
input = TRANSLATIONS[input];
1639+
}
1640+
return Object.keys(placeholders).length ?
1641+
input.replace(LOCALIZE_PH_REGEXP, (match, key) => placeholders[key] || '') :
1642+
input;
1643+
}

packages/core/src/render3/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export {
107107
Δi18nEnd,
108108
Δi18nApply,
109109
Δi18nPostprocess,
110+
i18nConfigureLocalize,
111+
Δi18nLocalize,
110112
} from './i18n';
111113

112114
export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';

packages/core/src/render3/jit/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
117117
'Δi18nEnd': r3.Δi18nEnd,
118118
'Δi18nApply': r3.Δi18nApply,
119119
'Δi18nPostprocess': r3.Δi18nPostprocess,
120+
'Δi18nLocalize': r3.Δi18nLocalize,
120121
'ΔresolveWindow': r3.ΔresolveWindow,
121122
'ΔresolveDocument': r3.ΔresolveDocument,
122123
'ΔresolveBody': r3.ΔresolveBody,

packages/core/src/util/ng_dev_mode.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {global} from './global';
10+
911
declare global {
1012
const ngDevMode: null|NgDevModePerfCounters;
1113
interface NgDevModePerfCounters {
@@ -38,8 +40,6 @@ declare global {
3840
}
3941
}
4042

41-
declare let global: any;
42-
4343
export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
4444
const newCounters: NgDevModePerfCounters = {
4545
firstTemplatePass: 0,
@@ -69,31 +69,20 @@ export function ngDevModeResetPerfCounters(): NgDevModePerfCounters {
6969
stylingApply: 0,
7070
stylingApplyCacheMiss: 0,
7171
};
72-
// NOTE: Under Ivy we may have both window & global defined in the Node
73-
// environment since ensureDocument() in render3.ts sets global.window.
74-
if (typeof window != 'undefined') {
75-
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
76-
(window as any)['ngDevMode'] = newCounters;
77-
}
78-
if (typeof global != 'undefined') {
79-
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
80-
(global as any)['ngDevMode'] = newCounters;
81-
}
82-
if (typeof self != 'undefined') {
83-
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
84-
(self as any)['ngDevMode'] = newCounters;
85-
}
72+
73+
// Make sure to refer to ngDevMode as ['ngDevMode'] for closure.
74+
global['ngDevMode'] = newCounters;
8675
return newCounters;
8776
}
8877

8978
/**
9079
* This checks to see if the `ngDevMode` has been set. If yes,
91-
* than we honor it, otherwise we default to dev mode with additional checks.
80+
* then we honor it, otherwise we default to dev mode with additional checks.
9281
*
9382
* The idea is that unless we are doing production build where we explicitly
9483
* set `ngDevMode == false` we should be helping the developer by providing
9584
* as much early warning and errors as possible.
9685
*/
97-
if (typeof ngDevMode === 'undefined' || ngDevMode) {
86+
if (typeof global['ngDevMode'] === 'undefined' || global['ngDevMode']) {
9887
ngDevModeResetPerfCounters();
9988
}

0 commit comments

Comments
 (0)