Skip to content

Commit cb05c01

Browse files
AndrewKushniratscott
authored andcommitted
fix(core): move generated i18n statements to the consts field of ComponentDef (angular#38404)
This commit updates the code to move generated i18n statements into the `consts` field of ComponentDef to avoid invoking `$localize` function before component initialization (to better support runtime translations) and also avoid problems with lazy-loading when i18n defs may not be present in a chunk where it's referenced. Prior to this change the i18n statements were generated at the top leve: ``` var I18N_0; if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { var MSG_X = goog.getMsg(“…”); I18N_0 = MSG_X; } else { I18N_0 = $localize('...'); } defineComponent({ // ... template: function App_Template(rf, ctx) { i0.ɵɵi18n(2, I18N_0); } }); ``` This commit updates the logic to generate the following code instead: ``` defineComponent({ // ... consts: function() { var I18N_0; if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { var MSG_X = goog.getMsg(“…”); I18N_0 = MSG_X; } else { I18N_0 = $localize('...'); } return [ I18N_0 ]; }, template: function App_Template(rf, ctx) { i0.ɵɵi18n(2, 0); } }); ``` Also note that i18n template instructions now refer to the `consts` array using an index (similar to other template instructions). PR Close angular#38404
1 parent 5f90b64 commit cb05c01

File tree

10 files changed

+751
-449
lines changed

10 files changed

+751
-449
lines changed

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

Lines changed: 610 additions & 350 deletions
Large diffs are not rendered by default.

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,19 @@ export function compileComponentFromMetadata(
196196
// e.g. `vars: 2`
197197
definitionMap.set('vars', o.literal(templateBuilder.getVarCount()));
198198

199-
// e.g. `consts: [['one', 'two'], ['three', 'four']]
200-
const consts = templateBuilder.getConsts();
201-
if (consts.length > 0) {
202-
definitionMap.set('consts', o.literalArr(consts));
199+
// Generate `consts` section of ComponentDef:
200+
// - either as an array:
201+
// `consts: [['one', 'two'], ['three', 'four']]`
202+
// - or as a factory function in case additional statements are present (to support i18n):
203+
// `consts: function() { var i18n_0; if (ngI18nClosureMode) {...} else {...} return [i18n_0]; }`
204+
const {constExpressions, prepareStatements} = templateBuilder.getConsts();
205+
if (constExpressions.length > 0) {
206+
let constsExpr: o.LiteralArrayExpr|o.FunctionExpr = o.literalArr(constExpressions);
207+
// Prepare statements are present - turn `consts` into a function.
208+
if (prepareStatements.length > 0) {
209+
constsExpr = o.fn([], [...prepareStatements, new o.ReturnStatement(constsExpr)]);
210+
}
211+
definitionMap.set('consts', constsExpr);
203212
}
204213

205214
definitionMap.set('template', templateFunctionExpression);

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import * as o from '../../../output/output_ast';
1212
import * as t from '../../r3_ast';
1313

1414
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
15-
const CLOSURE_TRANSLATION_PREFIX = 'MSG_';
15+
const CLOSURE_TRANSLATION_VAR_PREFIX = 'MSG_';
1616

17-
/* Prefix for non-`goog.getMsg` i18n-related vars */
18-
export const TRANSLATION_PREFIX = 'I18N_';
17+
/**
18+
* Prefix for non-`goog.getMsg` i18n-related vars.
19+
* Note: the prefix uses lowercase characters intentionally due to a Closure behavior that
20+
* considers variables like `I18N_0` as constants and throws an error when their value changes.
21+
*/
22+
export const TRANSLATION_VAR_PREFIX = 'i18n_';
1923

2024
/** Name of the i18n attributes **/
2125
export const I18N_ATTR = 'i18n';
@@ -166,7 +170,7 @@ export function formatI18nPlaceholderName(name: string, useCamelCase: boolean =
166170
* @returns Complete translation const prefix
167171
*/
168172
export function getTranslationConstPrefix(extra: string): string {
169-
return `${CLOSURE_TRANSLATION_PREFIX}${extra}`.toUpperCase();
173+
return `${CLOSURE_TRANSLATION_VAR_PREFIX}${extra}`.toUpperCase();
170174
}
171175

172176
/**

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

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {I18nContext} from './i18n/context';
3636
import {createGoogleGetMsgStatements} from './i18n/get_msg_utils';
3737
import {createLocalizeStatements} from './i18n/localize_utils';
3838
import {I18nMetaVisitor} from './i18n/meta';
39-
import {assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, hasI18nMeta, I18N_ICU_MAPPING_PREFIX, i18nFormatPlaceholderNames, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, TRANSLATION_PREFIX, wrapI18nPlaceholder} from './i18n/util';
39+
import {assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, hasI18nMeta, I18N_ICU_MAPPING_PREFIX, i18nFormatPlaceholderNames, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, TRANSLATION_VAR_PREFIX, wrapI18nPlaceholder} from './i18n/util';
4040
import {StylingBuilder, StylingInstruction} from './styling_builder';
4141
import {asLiteral, chainedInstruction, CONTEXT_NAME, getAttrsForDirectiveMatching, getInterpolationArgsLength, IMPLICIT_REFERENCE, invalid, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, trimTrailingNulls, unsupported} from './util';
4242

@@ -103,6 +103,18 @@ export function prepareEventListenerParameters(
103103
return params;
104104
}
105105

106+
// Collects information needed to generate `consts` field of the ComponentDef.
107+
// When a constant requires some pre-processing, the `prepareStatements` section
108+
// contains corresponding statements.
109+
export interface ComponentDefConsts {
110+
prepareStatements: o.Statement[];
111+
constExpressions: o.Expression[];
112+
}
113+
114+
function createComponentDefConsts(): ComponentDefConsts {
115+
return {prepareStatements: [], constExpressions: []};
116+
}
117+
106118
export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver {
107119
private _dataIndex = 0;
108120
private _bindingContext = 0;
@@ -171,7 +183,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
171183
private directiveMatcher: SelectorMatcher|null, private directives: Set<o.Expression>,
172184
private pipeTypeByName: Map<string, o.Expression>, private pipes: Set<o.Expression>,
173185
private _namespace: o.ExternalReference, relativeContextFilePath: string,
174-
private i18nUseExternalIds: boolean, private _constants: o.Expression[] = []) {
186+
private i18nUseExternalIds: boolean,
187+
private _constants: ComponentDefConsts = createComponentDefConsts()) {
175188
this._bindingScope = parentBindingScope.nestedScope(level);
176189

177190
// Turn the relative context file path into an identifier by replacing non-alphanumeric
@@ -307,12 +320,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
307320
private i18nTranslate(
308321
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
309322
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
310-
const _ref = ref || o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
323+
const _ref = ref || this.i18nGenerateMainBlockVar();
311324
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
312325
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
313326
const closureVar = this.i18nGenerateClosureVar(message.id);
314327
const statements = getTranslationDeclStmts(message, _ref, closureVar, params, transformFn);
315-
this.constantPool.statements.push(...statements);
328+
this._constants.prepareStatements.push(...statements);
316329
return _ref;
317330
}
318331

@@ -364,6 +377,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
364377
return bound;
365378
}
366379

380+
// Generates top level vars for i18n blocks (i.e. `i18n_N`).
381+
private i18nGenerateMainBlockVar(): o.ReadVarExpr {
382+
return o.variable(this.constantPool.uniqueName(TRANSLATION_VAR_PREFIX));
383+
}
384+
385+
// Generates vars with Closure-specific names for i18n blocks (i.e. `MSG_XXX`).
367386
private i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
368387
let name: string;
369388
const suffix = this.fileBasedI18nSuffix.toUpperCase();
@@ -426,16 +445,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
426445
private i18nStart(span: ParseSourceSpan|null = null, meta: i18n.I18nMeta, selfClosing?: boolean):
427446
void {
428447
const index = this.allocateDataSlot();
429-
if (this.i18nContext) {
430-
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex!, meta);
431-
} else {
432-
const ref = o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
433-
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
434-
}
448+
this.i18n = this.i18nContext ?
449+
this.i18nContext.forkChildContext(index, this.templateIndex!, meta) :
450+
new I18nContext(index, this.i18nGenerateMainBlockVar(), 0, this.templateIndex, meta);
435451

436452
// generate i18nStart instruction
437453
const {id, ref} = this.i18n;
438-
const params: o.Expression[] = [o.literal(index), ref];
454+
const params: o.Expression[] = [o.literal(index), this.addToConsts(ref)];
439455
if (id > 0) {
440456
// do not push 3rd argument (sub-block id)
441457
// into i18nStart call for top level i18n context
@@ -507,8 +523,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
507523
}
508524
if (i18nAttrArgs.length > 0) {
509525
const index: o.Expression = o.literal(this.allocateDataSlot());
510-
const args = this.constantPool.getConstLiteral(o.literalArr(i18nAttrArgs), true);
511-
this.creationInstruction(sourceSpan, R3.i18nAttributes, [index, args]);
526+
const constIndex = this.addToConsts(o.literalArr(i18nAttrArgs));
527+
this.creationInstruction(sourceSpan, R3.i18nAttributes, [index, constIndex]);
512528
if (hasBindings) {
513529
this.updateInstruction(sourceSpan, R3.i18nApply, [index]);
514530
}
@@ -1028,7 +1044,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
10281044
return this._pureFunctionSlots;
10291045
}
10301046

1031-
getConsts() {
1047+
getConsts(): ComponentDefConsts {
10321048
return this._constants;
10331049
}
10341050

@@ -1352,14 +1368,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
13521368
return o.TYPED_NULL_EXPR;
13531369
}
13541370

1371+
const consts = this._constants.constExpressions;
1372+
13551373
// Try to reuse a literal that's already in the array, if possible.
1356-
for (let i = 0; i < this._constants.length; i++) {
1357-
if (this._constants[i].isEquivalent(expression)) {
1374+
for (let i = 0; i < consts.length; i++) {
1375+
if (consts[i].isEquivalent(expression)) {
13581376
return o.literal(i);
13591377
}
13601378
}
13611379

1362-
return o.literal(this._constants.push(expression) - 1);
1380+
return o.literal(consts.push(expression) - 1);
13631381
}
13641382

13651383
private addAttrsToConsts(attrs: o.Expression[]): o.LiteralExpr {

packages/core/src/render3/definition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {stringify} from '../util/stringify';
1818
import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
1919
import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF, NG_LOC_ID_DEF, NG_MOD_DEF, NG_PIPE_DEF} from './fields';
2020
import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction} from './interfaces/definition';
21-
import {AttributeMarker, TAttributes, TConstants} from './interfaces/node';
21+
import {AttributeMarker, TAttributes, TConstantsOrFactory} from './interfaces/node';
2222
import {CssSelectorList, SelectorFlags} from './interfaces/projection';
2323
import {NgModuleType} from './ng_module_ref';
2424

@@ -220,7 +220,7 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
220220
* Constants for the nodes in the component's view.
221221
* Includes attribute arrays, local definition arrays etc.
222222
*/
223-
consts?: TConstants;
223+
consts?: TConstantsOrFactory;
224224

225225
/**
226226
* An array of `ngContent[selector]` values that were found in the template.

packages/core/src/render3/instructions/i18n.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {i18nAttributesFirstPass, i18nStartFirstPass} from '../i18n/i18n_parse';
1515
import {i18nPostprocess} from '../i18n/i18n_postprocess';
1616
import {HEADER_OFFSET} from '../interfaces/view';
1717
import {getLView, getTView, nextBindingIndex} from '../state';
18+
import {getConstant} from '../util/view_utils';
1819

1920
import {setDelayProjection} from './all';
2021

@@ -42,14 +43,15 @@ import {setDelayProjection} from './all';
4243
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
4344
*
4445
* @param index A unique index of the translation in the static block.
45-
* @param message The translation message.
46+
* @param messageIndex An index of the translation message from the `def.consts` array.
4647
* @param subTemplateIndex Optional sub-template index in the `message`.
4748
*
4849
* @codeGenApi
4950
*/
50-
export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?: number): void {
51+
export function ɵɵi18nStart(index: number, messageIndex: number, subTemplateIndex?: number): void {
5152
const tView = getTView();
5253
ngDevMode && assertDefined(tView, `tView should be defined`);
54+
const message = getConstant<string>(tView.consts, messageIndex)!;
5355
pushI18nIndex(index);
5456
// We need to delay projections until `i18nEnd`
5557
setDelayProjection(true);
@@ -96,13 +98,13 @@ export function ɵɵi18nEnd(): void {
9698
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
9799
*
98100
* @param index A unique index of the translation in the static block.
99-
* @param message The translation message.
101+
* @param messageIndex An index of the translation message from the `def.consts` array.
100102
* @param subTemplateIndex Optional sub-template index in the `message`.
101103
*
102104
* @codeGenApi
103105
*/
104-
export function ɵɵi18n(index: number, message: string, subTemplateIndex?: number): void {
105-
ɵɵi18nStart(index, message, subTemplateIndex);
106+
export function ɵɵi18n(index: number, messageIndex: number, subTemplateIndex?: number): void {
107+
ɵɵi18nStart(index, messageIndex, subTemplateIndex);
106108
ɵɵi18nEnd();
107109
}
108110

@@ -114,11 +116,12 @@ export function ɵɵi18n(index: number, message: string, subTemplateIndex?: numb
114116
*
115117
* @codeGenApi
116118
*/
117-
export function ɵɵi18nAttributes(index: number, values: string[]): void {
119+
export function ɵɵi18nAttributes(index: number, attrsIndex: number): void {
118120
const lView = getLView();
119121
const tView = getTView();
120122
ngDevMode && assertDefined(tView, `tView should be defined`);
121-
i18nAttributesFirstPass(lView, tView, index, values);
123+
const attrs = getConstant<string[]>(tView.consts, attrsIndex)!;
124+
i18nAttributesFirstPass(lView, tView, index, attrs);
122125
}
123126

124127

packages/core/src/render3/instructions/shared.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr
2626
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
2727
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
2828
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector';
29-
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstants, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node';
29+
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node';
3030
import {isProceduralRenderer, RComment, RElement, Renderer3, RendererFactory3, RNode, RText} from '../interfaces/renderer';
3131
import {SanitizerFn} from '../interfaces/sanitization';
3232
import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks';
@@ -650,14 +650,15 @@ export function createTView(
650650
type: TViewType, viewIndex: number, templateFn: ComponentTemplate<any>|null, decls: number,
651651
vars: number, directives: DirectiveDefListOrFactory|null, pipes: PipeDefListOrFactory|null,
652652
viewQuery: ViewQueriesFunction<any>|null, schemas: SchemaMetadata[]|null,
653-
consts: TConstants|null): TView {
653+
constsOrFactory: TConstantsOrFactory|null): TView {
654654
ngDevMode && ngDevMode.tView++;
655655
const bindingStartIndex = HEADER_OFFSET + decls;
656656
// This length does not yet contain host bindings from child directives because at this point,
657657
// we don't know which directives are active on this template. As soon as a directive is matched
658658
// that has a host binding, we will update the blueprint with that def's hostVars count.
659659
const initialViewLength = bindingStartIndex + vars;
660660
const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength);
661+
const consts = typeof constsOrFactory === 'function' ? constsOrFactory() : constsOrFactory;
661662
const tView = blueprint[TVIEW as any] = ngDevMode ?
662663
new TViewConstructor(
663664
type,

packages/core/src/render3/interfaces/definition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {SchemaMetadata, ViewEncapsulation} from '../../core';
1010
import {ProcessProvidersFunction} from '../../di/interface/provider';
1111
import {Type} from '../../interface/type';
1212

13-
import {TAttributes, TConstants} from './node';
13+
import {TAttributes, TConstantsOrFactory} from './node';
1414
import {CssSelectorList} from './projection';
1515
import {TView} from './view';
1616

@@ -299,7 +299,7 @@ export interface ComponentDef<T> extends DirectiveDef<T> {
299299
readonly template: ComponentTemplate<T>;
300300

301301
/** Constants associated with the component's view. */
302-
readonly consts: TConstants|null;
302+
readonly consts: TConstantsOrFactory|null;
303303

304304
/**
305305
* An array of `ngContent[selector]` values that were found in the template.

packages/core/src/render3/interfaces/node.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,24 @@ export type TAttributes = (string|AttributeMarker|CssSelector)[];
255255
* Constants that are associated with a view. Includes:
256256
* - Attribute arrays.
257257
* - Local definition arrays.
258+
* - Translated messages (i18n).
258259
*/
259260
export type TConstants = (TAttributes|string)[];
260261

262+
/**
263+
* Factory function that returns an array of consts. Consts can be represented as a function in case
264+
* any additional statements are required to define consts in the list. An example is i18n where
265+
* additional i18n calls are generated, which should be executed when consts are requested for the
266+
* first time.
267+
*/
268+
export type TConstantsFactory = () => TConstants;
269+
270+
/**
271+
* TConstants type that describes how the `consts` field is generated on ComponentDef: it can be
272+
* either an array or a factory function that returns that array.
273+
*/
274+
export type TConstantsOrFactory = TConstants|TConstantsFactory;
275+
261276
/**
262277
* Binding data (flyweight) for a particular node that is shared between all templates
263278
* of a specific type.

0 commit comments

Comments
 (0)