fix(core): move generated i18n statements to the consts field of ComponentDef (#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 #38404
This commit is contained in:
Andrew Kushnir
2020-08-10 17:25:51 -07:00
committed by Andrew Scott
parent 5f90b64328
commit cb05c0102f
10 changed files with 751 additions and 449 deletions

View File

@ -18,7 +18,7 @@ import {stringify} from '../util/stringify';
import {EMPTY_ARRAY, EMPTY_OBJ} from './empty';
import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF, NG_LOC_ID_DEF, NG_MOD_DEF, NG_PIPE_DEF} from './fields';
import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction} from './interfaces/definition';
import {AttributeMarker, TAttributes, TConstants} from './interfaces/node';
import {AttributeMarker, TAttributes, TConstantsOrFactory} from './interfaces/node';
import {CssSelectorList, SelectorFlags} from './interfaces/projection';
import {NgModuleType} from './ng_module_ref';
@ -220,7 +220,7 @@ export function ɵɵdefineComponent<T>(componentDefinition: {
* Constants for the nodes in the component's view.
* Includes attribute arrays, local definition arrays etc.
*/
consts?: TConstants;
consts?: TConstantsOrFactory;
/**
* An array of `ngContent[selector]` values that were found in the template.

View File

@ -15,6 +15,7 @@ import {i18nAttributesFirstPass, i18nStartFirstPass} from '../i18n/i18n_parse';
import {i18nPostprocess} from '../i18n/i18n_postprocess';
import {HEADER_OFFSET} from '../interfaces/view';
import {getLView, getTView, nextBindingIndex} from '../state';
import {getConstant} from '../util/view_utils';
import {setDelayProjection} from './all';
@ -42,14 +43,15 @@ import {setDelayProjection} from './all';
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
*
* @param index A unique index of the translation in the static block.
* @param message The translation message.
* @param messageIndex An index of the translation message from the `def.consts` array.
* @param subTemplateIndex Optional sub-template index in the `message`.
*
* @codeGenApi
*/
export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?: number): void {
export function ɵɵi18nStart(index: number, messageIndex: number, subTemplateIndex?: number): void {
const tView = getTView();
ngDevMode && assertDefined(tView, `tView should be defined`);
const message = getConstant<string>(tView.consts, messageIndex)!;
pushI18nIndex(index);
// We need to delay projections until `i18nEnd`
setDelayProjection(true);
@ -96,13 +98,13 @@ export function ɵɵi18nEnd(): void {
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
*
* @param index A unique index of the translation in the static block.
* @param message The translation message.
* @param messageIndex An index of the translation message from the `def.consts` array.
* @param subTemplateIndex Optional sub-template index in the `message`.
*
* @codeGenApi
*/
export function ɵɵi18n(index: number, message: string, subTemplateIndex?: number): void {
ɵɵi18nStart(index, message, subTemplateIndex);
export function ɵɵi18n(index: number, messageIndex: number, subTemplateIndex?: number): void {
ɵɵi18nStart(index, messageIndex, subTemplateIndex);
ɵɵi18nEnd();
}
@ -114,11 +116,12 @@ export function ɵɵi18n(index: number, message: string, subTemplateIndex?: numb
*
* @codeGenApi
*/
export function ɵɵi18nAttributes(index: number, values: string[]): void {
export function ɵɵi18nAttributes(index: number, attrsIndex: number): void {
const lView = getLView();
const tView = getTView();
ngDevMode && assertDefined(tView, `tView should be defined`);
i18nAttributesFirstPass(lView, tView, index, values);
const attrs = getConstant<string[]>(tView.consts, attrsIndex)!;
i18nAttributesFirstPass(lView, tView, index, attrs);
}

View File

@ -26,7 +26,7 @@ import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} fr
import {CONTAINER_HEADER_OFFSET, HAS_TRANSPLANTED_VIEWS, LContainer, MOVED_VIEWS} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstants, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliases, PropertyAliasValue, TAttributes, TConstantsOrFactory, TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node';
import {isProceduralRenderer, RComment, RElement, Renderer3, RendererFactory3, RNode, RText} from '../interfaces/renderer';
import {SanitizerFn} from '../interfaces/sanitization';
import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRootView} from '../interfaces/type_checks';
@ -650,7 +650,7 @@ export function createTView(
type: TViewType, viewIndex: number, templateFn: ComponentTemplate<any>|null, decls: number,
vars: number, directives: DirectiveDefListOrFactory|null, pipes: PipeDefListOrFactory|null,
viewQuery: ViewQueriesFunction<any>|null, schemas: SchemaMetadata[]|null,
consts: TConstants|null): TView {
constsOrFactory: TConstantsOrFactory|null): TView {
ngDevMode && ngDevMode.tView++;
const bindingStartIndex = HEADER_OFFSET + decls;
// This length does not yet contain host bindings from child directives because at this point,
@ -658,6 +658,7 @@ export function createTView(
// that has a host binding, we will update the blueprint with that def's hostVars count.
const initialViewLength = bindingStartIndex + vars;
const blueprint = createViewBlueprint(bindingStartIndex, initialViewLength);
const consts = typeof constsOrFactory === 'function' ? constsOrFactory() : constsOrFactory;
const tView = blueprint[TVIEW as any] = ngDevMode ?
new TViewConstructor(
type,

View File

@ -10,7 +10,7 @@ import {SchemaMetadata, ViewEncapsulation} from '../../core';
import {ProcessProvidersFunction} from '../../di/interface/provider';
import {Type} from '../../interface/type';
import {TAttributes, TConstants} from './node';
import {TAttributes, TConstantsOrFactory} from './node';
import {CssSelectorList} from './projection';
import {TView} from './view';
@ -299,7 +299,7 @@ export interface ComponentDef<T> extends DirectiveDef<T> {
readonly template: ComponentTemplate<T>;
/** Constants associated with the component's view. */
readonly consts: TConstants|null;
readonly consts: TConstantsOrFactory|null;
/**
* An array of `ngContent[selector]` values that were found in the template.

View File

@ -255,9 +255,24 @@ export type TAttributes = (string|AttributeMarker|CssSelector)[];
* Constants that are associated with a view. Includes:
* - Attribute arrays.
* - Local definition arrays.
* - Translated messages (i18n).
*/
export type TConstants = (TAttributes|string)[];
/**
* Factory function that returns an array of consts. Consts can be represented as a function in case
* any additional statements are required to define consts in the list. An example is i18n where
* additional i18n calls are generated, which should be executed when consts are requested for the
* first time.
*/
export type TConstantsFactory = () => TConstants;
/**
* TConstants type that describes how the `consts` field is generated on ComponentDef: it can be
* either an array or a factory function that returns that array.
*/
export type TConstantsOrFactory = TConstants|TConstantsFactory;
/**
* Binding data (flyweight) for a particular node that is shared between all templates
* of a specific type.

View File

@ -12,6 +12,7 @@ import {getTranslationForTemplate} from '@angular/core/src/render3/i18n/i18n_par
import {noop} from '../../../compiler/src/render3/view/util';
import {setDelayProjection, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all';
import {I18nUpdateOpCodes, TI18n, TIcu} from '../../src/render3/interfaces/i18n';
import {TConstants} from '../../src/render3/interfaces/node';
import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view';
import {getNativeByIndex} from '../../src/render3/util/view_utils';
@ -57,26 +58,29 @@ describe('Runtime i18n', () => {
});
function prepareFixture(
createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts = 0,
nbVars = 0): TemplateFixture {
return new TemplateFixture(createTemplate, updateTemplate || noop, nbConsts, nbVars);
createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts = 0, nbVars = 0,
consts: TConstants = []): TemplateFixture {
return new TemplateFixture(
createTemplate, updateTemplate || noop, nbConsts, nbVars, null, null, null, undefined,
consts);
}
function getOpCodes(
createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts: number,
index: number): TI18n|I18nUpdateOpCodes {
const fixture = prepareFixture(createTemplate, updateTemplate, nbConsts);
messageOrAtrs: string|string[], createTemplate: () => void, updateTemplate: (() => void)|null,
nbConsts: number, index: number): TI18n|I18nUpdateOpCodes {
const fixture =
prepareFixture(createTemplate, updateTemplate, nbConsts, undefined, [messageOrAtrs]);
const tView = fixture.hostView[TVIEW];
return tView.data[index + HEADER_OFFSET] as TI18n;
}
describe('i18nStart', () => {
it('for text', () => {
const MSG_DIV = `simple text`;
const message = 'simple text';
const nbConsts = 1;
const index = 0;
const opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index) as TI18n;
expect(opCodes).toEqual({
@ -91,13 +95,13 @@ describe('Runtime i18n', () => {
});
it('for elements', () => {
const MSG_DIV = `Hello <20>#2<>world<6C>/#2<> and <20>#3<>universe<73>/#3<>!`;
const message = `Hello <20>#2<>world<6C>/#2<> and <20>#3<>universe<73>/#3<>!`;
// Template: `<div>Hello <div>world</div> and <span>universe</span>!`
// 3 consts for the 2 divs and 1 span + 1 const for `i18nStart` = 4 consts
const nbConsts = 4;
const index = 1;
const opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
expect(opCodes).toEqual({
@ -124,11 +128,11 @@ describe('Runtime i18n', () => {
});
it('for simple bindings', () => {
const MSG_DIV = `Hello <20>0<EFBFBD>!`;
const message = `Hello <20>0<EFBFBD>!`;
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
expect((opCodes as any).update.debug).toEqual([
@ -148,11 +152,11 @@ describe('Runtime i18n', () => {
});
it('for multiple bindings', () => {
const MSG_DIV = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
const message = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
expect(opCodes).toEqual({
@ -176,17 +180,15 @@ describe('Runtime i18n', () => {
// </span>
// !
// </div>
const MSG_DIV =
const message =
`<EFBFBD>0<EFBFBD> is rendered as: <20>*2:1<><31>#1:1<>before<72>*2:2<><32>#1:2<>middle<6C>/#1:2<><32>/*2:2<>after<65>/#1:1<><31>/*2:1<>!`;
/**** Root template ****/
// <20>0<EFBFBD> is rendered as: <20>*2:1<><31>/*2:1<>!
let nbConsts = 3;
let index = 1;
const firstTextNode = 3;
const rootTemplate = 2;
let opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
let opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
expect(opCodes).toEqual({
@ -207,10 +209,8 @@ describe('Runtime i18n', () => {
// <20>#1:1<>before<72>*2:2<>middle<6C>/*2:2<>after<65>/#1:1<>
nbConsts = 3;
index = 0;
const spanElement = 1;
const bElementSubTemplate = 2;
opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV, 1);
opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0, 1);
}, null, nbConsts, index);
expect(opCodes).toEqual({
@ -233,9 +233,8 @@ describe('Runtime i18n', () => {
// middle
nbConsts = 2;
index = 0;
const bElement = 1;
opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV, 2);
opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0, 2);
}, null, nbConsts, index);
expect(opCodes).toEqual({
@ -252,15 +251,15 @@ describe('Runtime i18n', () => {
});
it('for ICU expressions', () => {
const MSG_DIV = `{<7B>0<EFBFBD>, plural,
const message = `{<7B>0<EFBFBD>, plural,
=0 {no <b title="none">emails</b>!}
=1 {one <i>email</i>}
other {<7B>0<EFBFBD> <span title="<22>1<EFBFBD>">emails</span>}
}`;
const nbConsts = 1;
const index = 0;
const opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index) as TI18n;
expect(opCodes).toEqual({
@ -337,7 +336,7 @@ describe('Runtime i18n', () => {
});
it('for nested ICU expressions', () => {
const MSG_DIV = `{<7B>0<EFBFBD>, plural,
const message = `{<7B>0<EFBFBD>, plural,
=0 {zero}
other {<7B>0<EFBFBD> {<7B>1<EFBFBD>, select,
cat {cats}
@ -347,16 +346,9 @@ describe('Runtime i18n', () => {
}`;
const nbConsts = 1;
const index = 0;
const opCodes = getOpCodes(() => {
ɵɵi18nStart(index, MSG_DIV);
const opCodes = getOpCodes(message, () => {
ɵɵi18nStart(index, 0);
}, null, nbConsts, index);
const icuCommentNodeIndex = index + 1;
const firstTextNodeIndex = index + 2;
const nestedIcuCommentNodeIndex = index + 3;
const lastTextNodeIndex = index + 4;
const nestedTextNodeIndex = index + 5;
const tIcuIndex = 1;
const nestedTIcuIndex = 0;
expect(opCodes).toEqual({
vars: 9,
@ -443,31 +435,31 @@ describe('Runtime i18n', () => {
describe(`i18nAttribute`, () => {
it('for text', () => {
const MSG_title = `Hello world!`;
const MSG_div_attr = ['title', MSG_title];
const message = `Hello world!`;
const attrs = ['title', message];
const nbConsts = 2;
const index = 1;
const fixture = prepareFixture(() => {
ɵɵelementStart(0, 'div');
ɵɵi18nAttributes(index, MSG_div_attr);
ɵɵi18nAttributes(index, 0);
ɵɵelementEnd();
}, null, nbConsts, index);
}, null, nbConsts, index, [attrs]);
const tView = fixture.hostView[TVIEW];
const opCodes = tView.data[index + HEADER_OFFSET] as I18nUpdateOpCodes;
expect(opCodes).toEqual([]);
expect(
(getNativeByIndex(0, fixture.hostView as LView) as any as Element).getAttribute('title'))
.toEqual(MSG_title);
.toEqual(message);
});
it('for simple bindings', () => {
const MSG_title = `Hello <20>0<EFBFBD>!`;
const MSG_div_attr = ['title', MSG_title];
const message = `Hello <20>0<EFBFBD>!`;
const attrs = ['title', message];
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => {
ɵɵi18nAttributes(index, MSG_div_attr);
const opCodes = getOpCodes(attrs, () => {
ɵɵi18nAttributes(index, 0);
}, null, nbConsts, index);
expect(opCodes).toEqual(debugMatch([
@ -476,12 +468,12 @@ describe('Runtime i18n', () => {
});
it('for multiple bindings', () => {
const MSG_title = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
const MSG_div_attr = ['title', MSG_title];
const message = `Hello <20>0<EFBFBD> and <20>1<EFBFBD>, again <20>0<EFBFBD>!`;
const attrs = ['title', message];
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => {
ɵɵi18nAttributes(index, MSG_div_attr);
const opCodes = getOpCodes(attrs, () => {
ɵɵi18nAttributes(index, 0);
}, null, nbConsts, index);
expect(opCodes).toEqual(debugMatch([
@ -490,12 +482,12 @@ describe('Runtime i18n', () => {
});
it('for multiple attributes', () => {
const MSG_title = `Hello <20>0<EFBFBD>!`;
const MSG_div_attr = ['title', MSG_title, 'aria-label', MSG_title];
const message = `Hello <20>0<EFBFBD>!`;
const attrs = ['title', message, 'aria-label', message];
const nbConsts = 2;
const index = 1;
const opCodes = getOpCodes(() => {
ɵɵi18nAttributes(index, MSG_div_attr);
const opCodes = getOpCodes(attrs, () => {
ɵɵi18nAttributes(index, 0);
}, null, nbConsts, index);
expect(opCodes).toEqual(debugMatch([
@ -643,4 +635,4 @@ describe('Runtime i18n', () => {
.toThrowError();
});
});
});
});