diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 558e00eaf9..6f5a741f96 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -197,7 +197,7 @@ describe('i18n support in the view compiler', () => { else { $I18N_0$ = $r3$.ɵɵi18nLocalize("Content A"); } - const $_c2$ = [${AttributeMarker.Bindings}, "title"]; + const $_c2$ = [${AttributeMarker.I18n}, "title"]; var $I18N_3$; if (ngI18nClosureMode) { /** @@ -330,7 +330,7 @@ describe('i18n support in the view compiler', () => { `; const output = ` - const $_c0$ = ["id", "static", ${AttributeMarker.Bindings}, "title"]; + const $_c0$ = ["id", "static", ${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { /** @@ -371,7 +371,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = ["id", "dynamic-1", ${AttributeMarker.Bindings}, "aria-roledescription", "title", "aria-label"]; + const $_c0$ = ["id", "dynamic-1", ${AttributeMarker.I18n}, "aria-roledescription", "title", "aria-label"]; var $I18N_1$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_5526535577705876535$$APP_SPEC_TS_1$ = goog.getMsg("static text"); @@ -417,7 +417,7 @@ describe('i18n support in the view compiler', () => { "title", $I18N_2$, "aria-label", $I18N_3$ ]; - const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.Bindings}, "title", "aria-roledescription"]; + const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"]; var $I18N_6$; if (ngI18nClosureMode) { /** @@ -487,7 +487,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = [${AttributeMarker.Bindings}, "title"]; + const $_c0$ = [${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { /** @@ -532,7 +532,7 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = [${AttributeMarker.Template}, "ngFor", "ngForOf"]; - const $_c1$ = [${AttributeMarker.Bindings}, "title"]; + const $_c1$ = [${AttributeMarker.I18n}, "title"]; var $I18N_1$; if (ngI18nClosureMode) { /** @@ -599,7 +599,7 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = [ "id", "dynamic-1", - ${AttributeMarker.Bindings}, "aria-roledescription", "title", "aria-label" + ${AttributeMarker.I18n}, "aria-roledescription", "title", "aria-label" ]; var $I18N_1$; if (ngI18nClosureMode) { @@ -646,7 +646,7 @@ describe('i18n support in the view compiler', () => { "title", $I18N_2$, "aria-label", $I18N_3$ ]; - const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.Bindings}, "title", "aria-roledescription"]; + const $_c2$ = ["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"]; var $I18N_6$; if (ngI18nClosureMode) { /** @@ -719,7 +719,7 @@ describe('i18n support in the view compiler', () => { const output = String.raw ` const $_c0$ = [${AttributeMarker.Template}, "ngFor", "ngForOf"]; - const $_c1$ = [${AttributeMarker.Bindings}, "title"]; + const $_c1$ = [${AttributeMarker.I18n}, "title"]; var $I18N_2$; if (ngI18nClosureMode) { /** @@ -776,7 +776,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c0$ = [${AttributeMarker.Bindings}, "title"]; + const $_c0$ = [${AttributeMarker.I18n}, "title"]; var $I18N_0$; if (ngI18nClosureMode) { /** @@ -1256,7 +1256,7 @@ describe('i18n support in the view compiler', () => { `; const output = String.raw ` - const $_c1$ = [${AttributeMarker.Bindings}, "title"]; + const $_c1$ = [${AttributeMarker.I18n}, "title"]; var $I18N_2$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_4782264005467235841$$APP_SPEC_TS_3$ = goog.getMsg("Span title {$interpolation} and {$interpolation_1}", { @@ -1448,7 +1448,7 @@ describe('i18n support in the view compiler', () => { $r3$.ɵɵelement(0, "img", $_c0$); } } - const $_c3$ = ["src", "logo.png", ${AttributeMarker.Bindings}, "title"]; + const $_c3$ = ["src", "logo.png", ${AttributeMarker.I18n}, "title"]; var $I18N_2$; if (ngI18nClosureMode) { const $MSG_EXTERNAL_2367729185105559721$$APP_SPEC_TS__2$ = goog.getMsg("App logo #{$interpolation}", { diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index 18bc01d523..f228d6ead0 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -491,5 +491,21 @@ export const enum AttributeMarker { * ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']] * ``` */ - ProjectAs = 5 + ProjectAs = 5, + + /** + * Signals that the following attribute will be translated by runtime i18n + * + * For example, given the following HTML: + * + * ``` + *
+ * ``` + * + * the generated code is: + * + * ``` + * var _c1 = ['moo', 'car', AttributeMarker.I18n, 'foo', 'bar']; + */ + I18n = 6, } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 49444431ad..6b19902457 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -518,7 +518,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const i18nAttrs: (t.TextAttribute | t.BoundAttribute)[] = []; const outputAttrs: t.TextAttribute[] = []; - const allOtherInputs: (t.TextAttribute | t.BoundAttribute)[] = []; const [namespaceKey, elementName] = splitNsName(element.name); const isNgContainer = checkIsNgContainer(element.name); @@ -539,11 +538,6 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart` // arguments i18nAttrs.push(attr); - // We treat this attribute as if it was a binding so that we don't risk calling inputs - // with the untranslated value. - // Also this generates smaller templates until FW-1248 is fixed. - // TODO(FW-1332): Create an AttributeMarker for i18n attributes - allOtherInputs.push(attr); } else { outputAttrs.push(attr); } @@ -561,6 +555,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Add the attributes const attributes: o.Expression[] = []; + const allOtherInputs: t.BoundAttribute[] = []; element.inputs.forEach((input: t.BoundAttribute) => { const stylingInputWasSet = stylingBuilder.registerBoundInput(input); @@ -571,8 +566,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart` // arguments i18nAttrs.push(input); + } else { + allOtherInputs.push(input); } - allOtherInputs.push(input); } }); @@ -585,7 +581,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver }); // add attributes for directive and projection matching purposes - attributes.push(...this.prepareNonRenderAttrs(allOtherInputs, element.outputs, stylingBuilder)); + attributes.push(...this.prepareNonRenderAttrs( + allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs)); parameters.push(this.toAttrsParam(attributes)); // local refs (ex.:
) @@ -1147,16 +1144,17 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver * CLASSES, class1, class2, * STYLES, style1, value1, style2, value2, * BINDINGS, name1, name2, name3, - * TEMPLATE, name4, name5, ...] + * TEMPLATE, name4, name5, name6, + * I18N, name7, name8, ...] * ``` * * Note that this function will fully ignore all synthetic (@foo) attribute values * because those values are intended to always be generated as property instructions. */ private prepareNonRenderAttrs( - inputs: (t.TextAttribute|t.BoundAttribute)[], outputs: t.BoundEvent[], - styles?: StylingBuilder, - templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] { + inputs: t.BoundAttribute[], outputs: t.BoundEvent[], styles?: StylingBuilder, + templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [], + i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] { const alreadySeen = new Set(); const attrExprs: o.Expression[] = []; @@ -1183,7 +1181,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const attrsStartIndex = attrExprs.length; for (let i = 0; i < inputs.length; i++) { - const input = inputs[i] as t.BoundAttribute; + const input = inputs[i]; if (input.type !== BindingType.Animation) { addAttrExpr(input.name); } @@ -1210,6 +1208,11 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver templateAttrs.forEach(attr => addAttrExpr(attr.name)); } + if (i18nAttrs.length) { + attrExprs.push(o.literal(core.AttributeMarker.I18n)); + i18nAttrs.forEach(attr => addAttrExpr(attr.name)); + } + return attrExprs; } diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index cb6f99b466..4e88949712 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -180,7 +180,23 @@ export const enum AttributeMarker { * ['attr', 'value', AttributeMarker.ProjectAs, ['', 'title', '']] * ``` */ - ProjectAs = 5 + ProjectAs = 5, + + /** + * Signals that the following attribute will be translated by runtime i18n + * + * For example, given the following HTML: + * + * ``` + *
+ * ``` + * + * the generated code is: + * + * ``` + * var _c1 = ['moo', 'car', AttributeMarker.I18n, 'foo', 'bar']; + */ + I18n, } /** diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts index b291587bc2..a65facee12 100644 --- a/packages/core/src/render3/node_selector_matcher.ts +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -171,17 +171,18 @@ function readClassValueFromTNode(tNode: TNode): string { * Attribute matching depends upon `isInlineTemplate` and `isProjectionMode`. * The following table summarizes which types of attributes we attempt to match: * - * ========================================================================================= - * Modes | Normal Attributes | Bindings Attributes | Template Attributes - * ========================================================================================= - * Inline + Projection | YES | YES | NO - * ----------------------------------------------------------------------------------------- - * Inline + Directive | NO | NO | YES - * ----------------------------------------------------------------------------------------- - * Non-inline + Projection | YES | YES | NO - * ----------------------------------------------------------------------------------------- - * Non-inline + Directive | YES | YES | NO - * ========================================================================================= + * =========================================================================================================== + * Modes | Normal Attributes | Bindings Attributes | Template Attributes | I18n + * Attributes + * =========================================================================================================== + * Inline + Projection | YES | YES | NO | YES + * ----------------------------------------------------------------------------------------------------------- + * Inline + Directive | NO | NO | YES | NO + * ----------------------------------------------------------------------------------------------------------- + * Non-inline + Projection | YES | YES | NO | YES + * ----------------------------------------------------------------------------------------------------------- + * Non-inline + Directive | YES | YES | NO | YES + * =========================================================================================================== * * @param name the name of the attribute to find * @param attrs the attribute array to examine @@ -203,7 +204,8 @@ function findAttrIndexInNode( const maybeAttrName = attrs[i]; if (maybeAttrName === name) { return i; - } else if (maybeAttrName === AttributeMarker.Bindings) { + } else if ( + maybeAttrName === AttributeMarker.Bindings || maybeAttrName === AttributeMarker.I18n) { bindingsMode = true; } else if (maybeAttrName === AttributeMarker.Classes) { let value = attrs[++i]; diff --git a/packages/core/src/render3/util/attrs_utils.ts b/packages/core/src/render3/util/attrs_utils.ts index 445404948a..c64e2df6ce 100644 --- a/packages/core/src/render3/util/attrs_utils.ts +++ b/packages/core/src/render3/util/attrs_utils.ts @@ -109,8 +109,9 @@ export function attrsStylingIndexOf(attrs: TAttributes, startIndex: number): num * attribute values in a `TAttributes` array are only the names of attributes, * and not name-value pairs. * @param marker The attribute marker to test. - * @returns true if the marker is a "name-only" marker (e.g. `Bindings` or `Template`). + * @returns true if the marker is a "name-only" marker (e.g. `Bindings`, `Template` or `I18n`). */ export function isNameOnlyAttributeMarker(marker: string | AttributeMarker | CssSelector) { - return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template; + return marker === AttributeMarker.Bindings || marker === AttributeMarker.Template || + marker === AttributeMarker.I18n; }