feat(ivy): i18n compiler support for element attributes (#26280)
PR Close #26280
This commit is contained in:

committed by
Miško Hevery

parent
3f8ac238f1
commit
39f42bad1c
@ -95,6 +95,12 @@ export class Identifiers {
|
||||
static pipeBind4: o.ExternalReference = {name: 'ɵpipeBind4', moduleName: CORE};
|
||||
static pipeBindV: o.ExternalReference = {name: 'ɵpipeBindV', moduleName: CORE};
|
||||
|
||||
static i18nAttribute: o.ExternalReference = {name: 'ɵi18nAttribute', moduleName: CORE};
|
||||
static i18nExp: o.ExternalReference = {name: 'ɵi18nExp', moduleName: CORE};
|
||||
static i18nStart: o.ExternalReference = {name: 'ɵi18nStart', moduleName: CORE};
|
||||
static i18nEnd: o.ExternalReference = {name: 'ɵi18nEnd', moduleName: CORE};
|
||||
static i18nApply: o.ExternalReference = {name: 'ɵi18nApply', moduleName: CORE};
|
||||
|
||||
static load: o.ExternalReference = {name: 'ɵload', moduleName: CORE};
|
||||
static loadQueryList: o.ExternalReference = {name: 'ɵloadQueryList', moduleName: CORE};
|
||||
|
||||
|
@ -30,7 +30,7 @@ import {htmlAstToRender3Ast} from '../r3_template_transform';
|
||||
|
||||
import {R3QueryMetadata} from './api';
|
||||
import {parseStyle} from './styling';
|
||||
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, isI18NAttribute, mapToExpression, trimTrailingNulls, unsupported} from './util';
|
||||
import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, assembleI18nTemplate, getAttrsForDirectiveMatching, invalid, isI18NAttribute, mapToExpression, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefined {
|
||||
switch (type) {
|
||||
@ -243,6 +243,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// LocalResolver
|
||||
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
|
||||
|
||||
i18nTranslate(label: string, meta?: string): o.Expression {
|
||||
return this.constantPool.getTranslation(label, parseI18nMeta(meta), this.fileBasedI18nSuffix);
|
||||
}
|
||||
|
||||
visitContent(ngContent: t.Content) {
|
||||
const slot = this.allocateDataSlot();
|
||||
const selectorIndex = ngContent.selectorIndex;
|
||||
@ -306,7 +310,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
let isNonBindableMode: boolean = false;
|
||||
|
||||
// Handle i18n attributes
|
||||
// Handle i18n and ngNonBindable attributes
|
||||
for (const attr of element.attributes) {
|
||||
const name = attr.name;
|
||||
const value = attr.value;
|
||||
@ -346,6 +350,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
const classInputs: t.BoundAttribute[] = [];
|
||||
const allOtherInputs: t.BoundAttribute[] = [];
|
||||
|
||||
const i18nAttrs: Array<{name: string, value: string | AST}> = [];
|
||||
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
switch (input.type) {
|
||||
// [attr.style] or [attr.class] should not be treated as styling-based
|
||||
@ -360,6 +366,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
} else if (isClassBinding(input)) {
|
||||
// this should always go first in the compilation (for [class])
|
||||
classInputs.splice(0, 0, input);
|
||||
} else if (attrI18nMetas.hasOwnProperty(input.name)) {
|
||||
i18nAttrs.push({name: input.name, value: input.value});
|
||||
} else {
|
||||
allOtherInputs.push(input);
|
||||
}
|
||||
@ -394,13 +402,10 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
staticClassesMap ![className] = true;
|
||||
});
|
||||
} else {
|
||||
attributes.push(o.literal(name));
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
const meta = parseI18nMeta(attrI18nMetas[name]);
|
||||
const variable = this.constantPool.getTranslation(value, meta, this.fileBasedI18nSuffix);
|
||||
attributes.push(variable);
|
||||
i18nAttrs.push({name, value});
|
||||
} else {
|
||||
attributes.push(o.literal(value));
|
||||
attributes.push(o.literal(name), o.literal(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -482,7 +487,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
const implicit = o.variable(CONTEXT_NAME);
|
||||
|
||||
const createSelfClosingInstruction = !hasStylingInstructions && !isNgContainer &&
|
||||
element.children.length === 0 && element.outputs.length === 0;
|
||||
element.children.length === 0 && element.outputs.length === 0 && i18nAttrs.length === 0;
|
||||
|
||||
if (createSelfClosingInstruction) {
|
||||
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
||||
@ -495,6 +500,41 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this.creationInstruction(element.sourceSpan, R3.disableBindings);
|
||||
}
|
||||
|
||||
// process i18n element attributes
|
||||
if (i18nAttrs.length) {
|
||||
let hasBindings: boolean = false;
|
||||
const i18nAttrArgs: o.Expression[] = [];
|
||||
i18nAttrs.forEach(({name, value}) => {
|
||||
const meta = attrI18nMetas[name];
|
||||
if (typeof value === 'string') {
|
||||
// in case of static string value, 3rd argument is 0 declares
|
||||
// that there are no expressions defined in this translation
|
||||
i18nAttrArgs.push(o.literal(name), this.i18nTranslate(value, meta), o.literal(0));
|
||||
} else {
|
||||
const converted = value.visit(this._valueConverter);
|
||||
if (converted instanceof Interpolation) {
|
||||
const {strings, expressions} = converted;
|
||||
const label = assembleI18nTemplate(strings);
|
||||
i18nAttrArgs.push(
|
||||
o.literal(name), this.i18nTranslate(label, meta), o.literal(expressions.length));
|
||||
expressions.forEach(expression => {
|
||||
hasBindings = true;
|
||||
const binding = this.convertExpressionBinding(implicit, expression);
|
||||
this.updateInstruction(element.sourceSpan, R3.i18nExp, [binding]);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (i18nAttrArgs.length) {
|
||||
const index: o.Expression = o.literal(this.allocateDataSlot());
|
||||
const args = this.constantPool.getConstLiteral(o.literalArr(i18nAttrArgs), true);
|
||||
this.creationInstruction(element.sourceSpan, R3.i18nAttribute, [index, args]);
|
||||
if (hasBindings) {
|
||||
this.updateInstruction(element.sourceSpan, R3.i18nApply, [index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initial styling for static style="..." attributes
|
||||
if (hasStylingInstructions) {
|
||||
const paramsList: (o.Expression)[] = [];
|
||||
@ -791,8 +831,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// i0.ɵtext(1, MSG_XYZ);
|
||||
// ```
|
||||
visitSingleI18nTextChild(text: t.Text, i18nMeta: string) {
|
||||
const meta = parseI18nMeta(i18nMeta);
|
||||
const variable = this.constantPool.getTranslation(text.value, meta, this.fileBasedI18nSuffix);
|
||||
const variable = this.i18nTranslate(text.value, i18nMeta);
|
||||
this.creationInstruction(
|
||||
text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), variable]);
|
||||
}
|
||||
@ -840,6 +879,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this._bindingSlots += value instanceof Interpolation ? value.expressions.length : 1;
|
||||
}
|
||||
|
||||
private convertExpressionBinding(implicit: o.Expression, value: AST): o.Expression {
|
||||
const convertedPropertyBinding =
|
||||
convertPropertyBinding(this, implicit, value, this.bindingContext(), BindingForm.TrySimple);
|
||||
const valExpr = convertedPropertyBinding.currValExpr;
|
||||
return o.importExpr(R3.bind).callFn([valExpr]);
|
||||
}
|
||||
|
||||
private convertPropertyBinding(implicit: o.Expression, value: AST, skipBindFn?: boolean):
|
||||
o.Expression {
|
||||
const interpolationFn =
|
||||
|
@ -35,6 +35,9 @@ export const I18N_ATTR_PREFIX = 'i18n-';
|
||||
export const MEANING_SEPARATOR = '|';
|
||||
export const ID_SEPARATOR = '@@';
|
||||
|
||||
/** Placeholder wrapper for i18n expressions **/
|
||||
export const I18N_PLACEHOLDER_SYMBOL = '<27>';
|
||||
|
||||
/** Non bindable attribute name **/
|
||||
export const NON_BINDABLE_ATTR = 'ngNonBindable';
|
||||
|
||||
@ -71,6 +74,21 @@ export function isI18NAttribute(name: string): boolean {
|
||||
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
|
||||
}
|
||||
|
||||
export function wrapI18nPlaceholder(content: string | number): string {
|
||||
return `${I18N_PLACEHOLDER_SYMBOL}${content}${I18N_PLACEHOLDER_SYMBOL}`;
|
||||
}
|
||||
|
||||
export function assembleI18nTemplate(strings: Array<string>): string {
|
||||
if (!strings.length) return '';
|
||||
let acc = '';
|
||||
const lastIdx = strings.length - 1;
|
||||
for (let i = 0; i < lastIdx; i++) {
|
||||
acc += `${strings[i]}${wrapI18nPlaceholder(i)}`;
|
||||
}
|
||||
acc += strings[lastIdx];
|
||||
return acc;
|
||||
}
|
||||
|
||||
export function asLiteral(value: any): o.Expression {
|
||||
if (Array.isArray(value)) {
|
||||
return o.literalArr(value.map(asLiteral));
|
||||
|
Reference in New Issue
Block a user