fix(compiler): treat i18n attributes with no bindings as static attributes (#39408)
Currently `i18n` attributes are treated the same no matter if they have data bindings or not. This both generates more code since they have to go through the `ɵɵi18nAttributes` instruction and prevents the translated attributes from being injected using the `@Attribute` decorator. These changes makes it so that static translated attributes are treated in the same way as regular static attributes and all other `i18n` attributes go through the old code path. Fixes #38231. PR Close #39408
This commit is contained in:
parent
421efbf69b
commit
58408d6a60
|
@ -153,7 +153,14 @@ ${output}
|
|||
`);
|
||||
};
|
||||
|
||||
const verify = (input: string, output: string, extra: any = {}): void => {
|
||||
const verify = (input: string, output: string, extra: {
|
||||
inputArgs?: any,
|
||||
compilerOptions?: any,
|
||||
skipPathBasedCheck?: boolean,
|
||||
skipIdBasedCheck?: boolean,
|
||||
verbose?: boolean,
|
||||
exceptions?: {}
|
||||
} = {}): void => {
|
||||
const files = getAppFilesWithTemplate(input, extra.inputArgs);
|
||||
const opts = (i18nUseExternalIds: boolean) =>
|
||||
({i18nUseExternalIds, ...(extra.compilerOptions || {})});
|
||||
|
@ -161,7 +168,7 @@ const verify = (input: string, output: string, extra: any = {}): void => {
|
|||
// invoke with file-based prefix translation names
|
||||
if (!extra.skipPathBasedCheck) {
|
||||
const result = compile(files, angularFiles, opts(false));
|
||||
maybePrint(result.source, extra.verbose);
|
||||
maybePrint(result.source, !!extra.verbose);
|
||||
expect(verifyPlaceholdersIntegrity(result.source)).toBe(true);
|
||||
expect(verifyUniqueConsts(result.source)).toBe(true);
|
||||
expectEmit(result.source, output, 'Incorrect template');
|
||||
|
@ -170,7 +177,7 @@ const verify = (input: string, output: string, extra: any = {}): void => {
|
|||
// invoke with translation names based on external ids
|
||||
if (!extra.skipIdBasedCheck) {
|
||||
const result = compile(files, angularFiles, opts(true));
|
||||
maybePrint(result.source, extra.verbose);
|
||||
maybePrint(result.source, !!extra.verbose);
|
||||
const interpolationConfig = extra.inputArgs && extra.inputArgs.interpolation ?
|
||||
InterpolationConfig.fromArray(extra.inputArgs.interpolation) :
|
||||
undefined;
|
||||
|
@ -346,7 +353,6 @@ describe('i18n support in the template compiler', () => {
|
|||
${i18n_7}
|
||||
return [
|
||||
$i18n_0$,
|
||||
[${AttributeMarker.I18n}, "title"],
|
||||
["title", $i18n_1$],
|
||||
["title", $i18n_2$],
|
||||
["title", $i18n_3$],
|
||||
|
@ -362,31 +368,25 @@ describe('i18n support in the template compiler', () => {
|
|||
$r3$.ɵɵi18n(1, 0);
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(2, "div", 1);
|
||||
$r3$.ɵɵi18nAttributes(3, 2);
|
||||
$r3$.ɵɵtext(4, "Content B");
|
||||
$r3$.ɵɵtext(3, "Content B");
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(5, "div", 1);
|
||||
$r3$.ɵɵi18nAttributes(6, 3);
|
||||
$r3$.ɵɵtext(7, "Content C");
|
||||
$r3$.ɵɵelementStart(4, "div", 2);
|
||||
$r3$.ɵɵtext(5, "Content C");
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(8, "div", 1);
|
||||
$r3$.ɵɵi18nAttributes(9, 4);
|
||||
$r3$.ɵɵtext(10, "Content D");
|
||||
$r3$.ɵɵelementStart(6, "div", 3);
|
||||
$r3$.ɵɵtext(7, "Content D");
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(11, "div", 1);
|
||||
$r3$.ɵɵi18nAttributes(12, 5);
|
||||
$r3$.ɵɵtext(13, "Content E");
|
||||
$r3$.ɵɵelementStart(8, "div", 4);
|
||||
$r3$.ɵɵtext(9, "Content E");
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(14, "div", 1);
|
||||
$r3$.ɵɵi18nAttributes(15, 6);
|
||||
$r3$.ɵɵtext(16, "Content F");
|
||||
$r3$.ɵɵelementStart(10, "div", 5);
|
||||
$r3$.ɵɵtext(11, "Content F");
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(17, "div", 1);
|
||||
$r3$.ɵɵi18nAttributes(18, 7);
|
||||
$r3$.ɵɵtext(19, "Content G");
|
||||
$r3$.ɵɵelementStart(12, "div", 6);
|
||||
$r3$.ɵɵtext(13, "Content G");
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(20, "div");
|
||||
$r3$.ɵɵi18n(21, 8);
|
||||
$r3$.ɵɵelementStart(14, "div");
|
||||
$r3$.ɵɵi18n(15, 7);
|
||||
$r3$.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
|
@ -405,14 +405,12 @@ describe('i18n support in the template compiler', () => {
|
|||
consts: function () {
|
||||
${i18n_0}
|
||||
return [
|
||||
[${AttributeMarker.I18n}, "title"],
|
||||
["title", $i18n_0$]
|
||||
];
|
||||
},
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵtemplate(0, MyComponent_ng_template_0_Template, 0, 0, "ng-template", 0);
|
||||
$r3$.ɵɵi18nAttributes(1, 1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -436,7 +434,6 @@ describe('i18n support in the template compiler', () => {
|
|||
function MyComponent_0_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵtemplate(0, MyComponent_0_ng_template_0_Template, 1, 0, "ng-template", 1);
|
||||
$r3$.ɵɵi18nAttributes(1, 2);
|
||||
}
|
||||
}
|
||||
…
|
||||
|
@ -444,13 +441,12 @@ describe('i18n support in the template compiler', () => {
|
|||
${i18n_0}
|
||||
return [
|
||||
[${AttributeMarker.Template}, "ngIf"],
|
||||
[${AttributeMarker.I18n}, "title"],
|
||||
["title", $i18n_0$]
|
||||
];
|
||||
},
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 2, 0, undefined, 0);
|
||||
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 1, 0, undefined, 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵproperty("ngIf", ctx.visible);
|
||||
|
@ -584,15 +580,12 @@ describe('i18n support in the template compiler', () => {
|
|||
consts: function() {
|
||||
${i18n_0}
|
||||
return [
|
||||
["id", "static", ${AttributeMarker.I18n}, "title"],
|
||||
["title", $i18n_0$]
|
||||
["id", "static", "title", $i18n_0$]
|
||||
];
|
||||
},
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵelementStart(0, "div", 0);
|
||||
$r3$.ɵɵi18nAttributes(1, 1);
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelement(0, "div", 0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -640,9 +633,9 @@ describe('i18n support in the template compiler', () => {
|
|||
${i18n_3}
|
||||
${i18n_4}
|
||||
return [
|
||||
["id", "dynamic-1", ${AttributeMarker.I18n}, "aria-roledescription",
|
||||
["id", "dynamic-1", "aria-roledescription", $i18n_0$, ${AttributeMarker.I18n},
|
||||
"title", "aria-label"],
|
||||
["aria-roledescription", $i18n_0$, "title", $i18n_1$, "aria-label", $i18n_2$],
|
||||
["title", $i18n_1$, "aria-label", $i18n_2$],
|
||||
["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"],
|
||||
["title", $i18n_3$, "aria-roledescription", $i18n_4$]
|
||||
];
|
||||
|
@ -790,76 +783,6 @@ describe('i18n support in the template compiler', () => {
|
|||
verify(input, output);
|
||||
});
|
||||
|
||||
it('should support interpolation', () => {
|
||||
const input = `
|
||||
<div id="dynamic-1"
|
||||
i18n-title="m|d" title="intro {{ valueA | uppercase }}"
|
||||
i18n-aria-label="m1|d1" aria-label="{{ valueB }}"
|
||||
i18n-aria-roledescription aria-roledescription="static text"
|
||||
></div>
|
||||
<div id="dynamic-2"
|
||||
i18n-title="m2|d2" title="{{ valueA }} and {{ valueB }} and again {{ valueA + valueB }}"
|
||||
i18n-aria-roledescription aria-roledescription="{{ valueC }}"
|
||||
></div>
|
||||
`;
|
||||
|
||||
const i18n_0 = i18nMsg('static text');
|
||||
const i18n_1 = i18nMsg(
|
||||
'intro {$interpolation}', [['interpolation', String.raw`\uFFFD0\uFFFD`]],
|
||||
{meaning: 'm', desc: 'd'});
|
||||
const i18n_2 = i18nMsg(
|
||||
'{$interpolation}', [['interpolation', String.raw`\uFFFD0\uFFFD`]],
|
||||
{meaning: 'm1', desc: 'd1'});
|
||||
const i18n_3 = i18nMsg(
|
||||
'{$interpolation} and {$interpolation_1} and again {$interpolation_2}',
|
||||
[
|
||||
['interpolation', String.raw`\uFFFD0\uFFFD`],
|
||||
['interpolation_1', String.raw`\uFFFD1\uFFFD`],
|
||||
['interpolation_2', String.raw`\uFFFD2\uFFFD`]
|
||||
],
|
||||
{meaning: 'm2', desc: 'd2'});
|
||||
const i18n_4 = i18nMsg('{$interpolation}', [['interpolation', String.raw`\uFFFD0\uFFFD`]]);
|
||||
|
||||
const output = String.raw`
|
||||
decls: 5,
|
||||
vars: 8,
|
||||
consts: function() {
|
||||
${i18n_0}
|
||||
${i18n_1}
|
||||
${i18n_2}
|
||||
${i18n_3}
|
||||
${i18n_4}
|
||||
return [
|
||||
["id", "dynamic-1", ${AttributeMarker.I18n}, "aria-roledescription",
|
||||
"title", "aria-label"],
|
||||
["aria-roledescription", $i18n_0$, "title", $i18n_1$, "aria-label", $i18n_2$],
|
||||
["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"],
|
||||
["title", $i18n_3$, "aria-roledescription", $i18n_4$]
|
||||
];
|
||||
},
|
||||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵelementStart(0, "div", 0);
|
||||
$r3$.ɵɵpipe(1, "uppercase");
|
||||
$r3$.ɵɵi18nAttributes(2, 1);
|
||||
$r3$.ɵɵelementEnd();
|
||||
$r3$.ɵɵelementStart(3, "div", 2);
|
||||
$r3$.ɵɵi18nAttributes(4, 3);
|
||||
$r3$.ɵɵelementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵi18nExp($r3$.ɵɵpipeBind1(1, 6, ctx.valueA))(ctx.valueB);
|
||||
$r3$.ɵɵi18nApply(2);
|
||||
$r3$.ɵɵadvance(3);
|
||||
$r3$.ɵɵi18nExp(ctx.valueA)(ctx.valueB)(ctx.valueA + ctx.valueB)(ctx.valueC);
|
||||
$r3$.ɵɵi18nApply(4);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
verify(input, output);
|
||||
});
|
||||
|
||||
it('should correctly bind to context in nested template', () => {
|
||||
const input = `
|
||||
<div *ngFor="let outer of items">
|
||||
|
@ -925,7 +848,6 @@ describe('i18n support in the template compiler', () => {
|
|||
${i18n_0}
|
||||
${i18n_1}
|
||||
return [
|
||||
[${AttributeMarker.I18n}, "title"],
|
||||
["title", $i18n_0$],
|
||||
$i18n_1$
|
||||
];
|
||||
|
@ -933,8 +855,7 @@ describe('i18n support in the template compiler', () => {
|
|||
template: function MyComponent_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵelementStart(0, "div", 0);
|
||||
$r3$.ɵɵi18nAttributes(1, 1);
|
||||
$r3$.ɵɵi18n(2, 2);
|
||||
$r3$.ɵɵi18n(1, 1);
|
||||
$r3$.ɵɵelementEnd();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,11 @@ export function hasI18nMeta(node: t.Node&{i18n?: i18n.I18nMeta}): boolean {
|
|||
return !!node.i18n;
|
||||
}
|
||||
|
||||
export function isBoundI18nAttribute(node: t.TextAttribute|
|
||||
t.BoundAttribute): node is t.BoundAttribute {
|
||||
return node.i18n !== undefined && node instanceof t.BoundAttribute;
|
||||
}
|
||||
|
||||
export function hasI18nAttrs(element: html.Element): boolean {
|
||||
return element.attrs.some((attr: html.Attribute) => isI18nAttribute(attr.name));
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ import {I18nContext} from './i18n/context';
|
|||
import {createGoogleGetMsgStatements} from './i18n/get_msg_utils';
|
||||
import {createLocalizeStatements} from './i18n/localize_utils';
|
||||
import {I18nMetaVisitor} from './i18n/meta';
|
||||
import {assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, hasI18nMeta, I18N_ICU_MAPPING_PREFIX, i18nFormatPlaceholderNames, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, TRANSLATION_VAR_PREFIX, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, hasI18nMeta, I18N_ICU_MAPPING_PREFIX, i18nFormatPlaceholderNames, icuFromI18nMessage, isBoundI18nAttribute, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, TRANSLATION_VAR_PREFIX, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
||||
import {asLiteral, chainedInstruction, CONTEXT_NAME, getAttrsForDirectiveMatching, getInterpolationArgsLength, IMPLICIT_REFERENCE, invalid, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
|
@ -492,16 +492,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
}
|
||||
|
||||
private i18nAttributesInstruction(
|
||||
nodeIndex: number, attrs: (t.TextAttribute|t.BoundAttribute)[],
|
||||
sourceSpan: ParseSourceSpan): void {
|
||||
nodeIndex: number, attrs: t.BoundAttribute[], sourceSpan: ParseSourceSpan): void {
|
||||
let hasBindings: boolean = false;
|
||||
const i18nAttrArgs: o.Expression[] = [];
|
||||
const bindings: ChainableBindingInstruction[] = [];
|
||||
attrs.forEach(attr => {
|
||||
const message = attr.i18n! as i18n.Message;
|
||||
if (attr instanceof t.TextAttribute) {
|
||||
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message));
|
||||
} else {
|
||||
const converted = attr.value.visit(this._valueConverter);
|
||||
this.allocateBindingSlots(converted);
|
||||
if (converted instanceof Interpolation) {
|
||||
|
@ -516,7 +512,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (bindings.length > 0) {
|
||||
this.updateInstructionChainWithAdvance(nodeIndex, R3.i18nExp, bindings);
|
||||
|
@ -591,7 +586,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const isI18nRootElement: boolean =
|
||||
isI18nRootNode(element.i18n) && !isSingleI18nIcu(element.i18n);
|
||||
|
||||
const i18nAttrs: (t.TextAttribute|t.BoundAttribute)[] = [];
|
||||
const boundI18nAttrs: t.BoundAttribute[] = [];
|
||||
const outputAttrs: t.TextAttribute[] = [];
|
||||
|
||||
const [namespaceKey, elementName] = splitNsName(element.name);
|
||||
|
@ -606,8 +601,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
stylingBuilder.registerStyleAttr(value);
|
||||
} else if (name === 'class') {
|
||||
stylingBuilder.registerClassAttr(value);
|
||||
} else if (isBoundI18nAttribute(attr)) {
|
||||
// Note that we don't collect static i18n attributes here, because
|
||||
// they can be treated in the same way as regular attributes.
|
||||
boundI18nAttrs.push(attr);
|
||||
} else {
|
||||
(attr.i18n ? i18nAttrs : outputAttrs).push(attr);
|
||||
outputAttrs.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -627,7 +626,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const stylingInputWasSet = stylingBuilder.registerBoundInput(input);
|
||||
if (!stylingInputWasSet) {
|
||||
if (input.type === BindingType.Property && input.i18n) {
|
||||
i18nAttrs.push(input);
|
||||
boundI18nAttrs.push(input);
|
||||
} else {
|
||||
allOtherInputs.push(input);
|
||||
}
|
||||
|
@ -636,7 +635,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
|
||||
// add attributes for directive and projection matching purposes
|
||||
const attributes: o.Expression[] = this.getAttributeExpressions(
|
||||
element.name, outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs);
|
||||
element.name, outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [],
|
||||
boundI18nAttrs);
|
||||
parameters.push(this.addAttrsToConsts(attributes));
|
||||
|
||||
// local refs (ex.: <div #foo #bar="baz">)
|
||||
|
@ -662,7 +662,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
element.children.length > 0;
|
||||
|
||||
const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes &&
|
||||
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren;
|
||||
element.outputs.length === 0 && boundI18nAttrs.length === 0 && !hasChildren;
|
||||
const createSelfClosingI18nInstruction =
|
||||
!createSelfClosingInstruction && hasTextChildrenOnly(element.children);
|
||||
|
||||
|
@ -679,9 +679,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
this.creationInstruction(element.startSourceSpan, R3.disableBindings);
|
||||
}
|
||||
|
||||
if (i18nAttrs.length > 0) {
|
||||
if (boundI18nAttrs.length > 0) {
|
||||
this.i18nAttributesInstruction(
|
||||
elementIndex, i18nAttrs, element.startSourceSpan ?? element.sourceSpan);
|
||||
elementIndex, boundI18nAttrs, element.startSourceSpan ?? element.sourceSpan);
|
||||
}
|
||||
|
||||
// Generate Listeners (outputs)
|
||||
|
@ -866,10 +866,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
this.matchDirectives(NG_TEMPLATE_TAG_NAME, template);
|
||||
|
||||
// prepare attributes parameter (including attributes used for directive matching)
|
||||
const [i18nStaticAttrs, staticAttrs] = partitionArray(template.attributes, hasI18nMeta);
|
||||
const [boundI18nAttrs, attrs] = partitionArray<t.BoundAttribute, t.TextAttribute>(
|
||||
template.attributes, isBoundI18nAttribute);
|
||||
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
|
||||
NG_TEMPLATE_TAG_NAME, staticAttrs, template.inputs, template.outputs,
|
||||
undefined /* styles */, template.templateAttrs, i18nStaticAttrs);
|
||||
NG_TEMPLATE_TAG_NAME, attrs, template.inputs, template.outputs, undefined /* styles */,
|
||||
template.templateAttrs, boundI18nAttrs);
|
||||
parameters.push(this.addAttrsToConsts(attrsExprs));
|
||||
|
||||
// local refs (ex.: <ng-template #foo>)
|
||||
|
@ -913,8 +914,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
|
||||
// Only add normal input/output binding instructions on explicit <ng-template> elements.
|
||||
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
|
||||
const [i18nInputs, inputs] = partitionArray(template.inputs, hasI18nMeta);
|
||||
const i18nAttrs = [...i18nStaticAttrs, ...i18nInputs];
|
||||
const [i18nInputs, inputs] =
|
||||
partitionArray<t.BoundAttribute, t.BoundAttribute>(template.inputs, hasI18nMeta);
|
||||
const i18nAttrs = [...boundI18nAttrs, ...i18nInputs];
|
||||
|
||||
// Add i18n attributes that may act as inputs to directives. If such attributes are present,
|
||||
// generate `i18nAttributes` instruction. Note: we generate it only for explicit <ng-template>
|
||||
|
@ -1289,18 +1291,25 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
elementName: string, renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[],
|
||||
outputs: t.BoundEvent[], styles?: StylingBuilder,
|
||||
templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
|
||||
i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] {
|
||||
boundI18nAttrs: t.BoundAttribute[] = []): o.Expression[] {
|
||||
const alreadySeen = new Set<string>();
|
||||
const attrExprs: o.Expression[] = [];
|
||||
let ngProjectAsAttr: t.TextAttribute|undefined;
|
||||
|
||||
renderAttributes.forEach((attr: t.TextAttribute) => {
|
||||
for (const attr of renderAttributes) {
|
||||
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
|
||||
ngProjectAsAttr = attr;
|
||||
}
|
||||
|
||||
// Note that static i18n attributes aren't in the i18n array,
|
||||
// because they're treated in the same way as regular attributes.
|
||||
if (attr.i18n) {
|
||||
attrExprs.push(o.literal(attr.name), this.i18nTranslate(attr.i18n as i18n.Message));
|
||||
} else {
|
||||
attrExprs.push(
|
||||
...getAttributeNameLiterals(attr.name), trustedConstAttribute(elementName, attr));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep ngProjectAs next to the other name, value pairs so we can verify that we match
|
||||
// ngProjectAs marker in the attribute name slot.
|
||||
|
@ -1360,9 +1369,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
templateAttrs.forEach(attr => addAttrExpr(attr.name));
|
||||
}
|
||||
|
||||
if (i18nAttrs.length) {
|
||||
if (boundI18nAttrs.length) {
|
||||
attrExprs.push(o.literal(core.AttributeMarker.I18n));
|
||||
i18nAttrs.forEach(attr => addAttrExpr(attr.name));
|
||||
boundI18nAttrs.forEach(attr => addAttrExpr(attr.name));
|
||||
}
|
||||
|
||||
return attrExprs;
|
||||
|
|
|
@ -280,12 +280,12 @@ export function newArray<T>(size: number, value?: T): T[] {
|
|||
* @param conditionFn Condition function that is called for each item in a given array and returns a
|
||||
* boolean value.
|
||||
*/
|
||||
export function partitionArray<T>(
|
||||
arr: T[], conditionFn: <K extends T>(value: K) => boolean): [T[], T[]] {
|
||||
export function partitionArray<T, F = T>(
|
||||
arr: (T|F)[], conditionFn: (value: T|F) => boolean): [T[], F[]] {
|
||||
const truthy: T[] = [];
|
||||
const falsy: T[] = [];
|
||||
arr.forEach(item => {
|
||||
(conditionFn(item) ? truthy : falsy).push(item);
|
||||
});
|
||||
const falsy: F[] = [];
|
||||
for (const item of arr) {
|
||||
(conditionFn(item) ? truthy : falsy).push(item as any);
|
||||
}
|
||||
return [truthy, falsy];
|
||||
}
|
||||
|
|
|
@ -14,16 +14,14 @@ import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer';
|
|||
import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertOneOf, assertString} from '../../util/assert';
|
||||
import {CharCode} from '../../util/char_code';
|
||||
import {loadIcuContainerVisitor} from '../instructions/i18n_icu_container_visitor';
|
||||
import {allocExpando, createTNodeAtIndex, elementAttributeInternal, setInputsForProperty, setNgReflectProperties} from '../instructions/shared';
|
||||
import {allocExpando, createTNodeAtIndex} from '../instructions/shared';
|
||||
import {getDocument} from '../interfaces/document';
|
||||
import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nRemoveOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, ICU_MARKER, IcuCreateOpCode, IcuCreateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
|
||||
import {TNode, TNodeType} from '../interfaces/node';
|
||||
import {RComment, RElement} from '../interfaces/renderer';
|
||||
import {SanitizerFn} from '../interfaces/sanitization';
|
||||
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
|
||||
import {getCurrentParentTNode, getCurrentTNode, setCurrentTNode} from '../state';
|
||||
import {attachDebugGetter} from '../util/debug_utils';
|
||||
import {getNativeByIndex, getTNode} from '../util/view_utils';
|
||||
|
||||
import {i18nCreateOpCodesToString, i18nRemoveOpCodesToString, i18nUpdateOpCodesToString, icuCreateOpCodesToString} from './i18n_debug';
|
||||
import {addTNodeAndUpdateInsertBeforeIndex} from './i18n_insert_before_index';
|
||||
|
@ -102,7 +100,10 @@ export function i18nStartFirstCreatePass(
|
|||
// Verify that ICU expression has the right shape. Translations might contain invalid
|
||||
// constructions (while original messages were correct), so ICU parsing at runtime may
|
||||
// not succeed (thus `icuExpression` remains a string).
|
||||
if (ngDevMode && typeof icuExpression !== 'object') {
|
||||
// Note: we intentionally retain the error here by not using `ngDevMode`, because
|
||||
// the value can change based on the locale and users aren't guaranteed to hit
|
||||
// an invalid string while they're developing.
|
||||
if (typeof icuExpression !== 'object') {
|
||||
throw new Error(`Unable to parse ICU expression in "${message}" message.`);
|
||||
}
|
||||
const icuContainerTNode = createTNodeAndAddOpCode(
|
||||
|
@ -225,54 +226,34 @@ function i18nStartFirstCreatePassProcessTextNode(
|
|||
/**
|
||||
* See `i18nAttributes` above.
|
||||
*/
|
||||
export function i18nAttributesFirstPass(
|
||||
lView: LView, tView: TView, index: number, values: string[]) {
|
||||
export function i18nAttributesFirstPass(tView: TView, index: number, values: string[]) {
|
||||
const previousElement = getCurrentTNode()!;
|
||||
const previousElementIndex = previousElement.index;
|
||||
const updateOpCodes: I18nUpdateOpCodes = [] as any;
|
||||
if (ngDevMode) {
|
||||
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
||||
}
|
||||
if (tView.firstCreatePass && tView.data[index] === null) {
|
||||
for (let i = 0; i < values.length; i += 2) {
|
||||
const attrName = values[i];
|
||||
const message = values[i + 1];
|
||||
const parts = message.split(ICU_REGEXP);
|
||||
for (let j = 0; j < parts.length; j++) {
|
||||
const value = parts[j];
|
||||
|
||||
if (j & 1) {
|
||||
// Odd indexes are ICU expressions
|
||||
// TODO(ocombe): support ICU expressions in attributes
|
||||
throw new Error('ICU expressions are not yet supported in attributes');
|
||||
} else if (value !== '') {
|
||||
// Even indexes are text (including bindings)
|
||||
const hasBinding = !!value.match(BINDING_REGEXP);
|
||||
if (hasBinding) {
|
||||
if (tView.firstCreatePass && tView.data[index] === null) {
|
||||
generateBindingUpdateOpCodes(updateOpCodes, value, previousElementIndex, attrName);
|
||||
}
|
||||
} else {
|
||||
const tNode = getTNode(tView, previousElementIndex);
|
||||
// Set attributes for Elements only, for other types (like ElementContainer),
|
||||
// only set inputs below
|
||||
if (tNode.type & TNodeType.AnyRNode) {
|
||||
elementAttributeInternal(tNode, lView, attrName, value, null, null);
|
||||
}
|
||||
// Check if that attribute is a directive input
|
||||
const dataValue = tNode.inputs !== null && tNode.inputs[attrName];
|
||||
if (dataValue) {
|
||||
setInputsForProperty(tView, lView, dataValue, attrName, value);
|
||||
if (ngDevMode) {
|
||||
const element = getNativeByIndex(previousElementIndex, lView) as RElement | RComment;
|
||||
setNgReflectProperties(lView, element, tNode.type, dataValue, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message !== '') {
|
||||
// Check if attribute value contains an ICU and throw an error if that's the case.
|
||||
// ICUs in element attributes are not supported.
|
||||
// Note: we intentionally retain the error here by not using `ngDevMode`, because
|
||||
// the `value` can change based on the locale and users aren't guaranteed to hit
|
||||
// an invalid string while they're developing.
|
||||
if (ICU_REGEXP.test(message)) {
|
||||
throw new Error(
|
||||
`ICU expressions are not supported in attributes. Message: "${message}".`);
|
||||
}
|
||||
|
||||
if (tView.firstCreatePass && tView.data[index] === null) {
|
||||
// i18n attributes that hit this code path are guaranteed to have bindings, because
|
||||
// the compiler treats static i18n attributes as regular attribute bindings.
|
||||
generateBindingUpdateOpCodes(updateOpCodes, message, previousElementIndex, attrName);
|
||||
}
|
||||
}
|
||||
tView.data[index] = updateOpCodes;
|
||||
}
|
||||
}
|
||||
|
@ -622,10 +603,10 @@ function walkIcuTree(
|
|||
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name);
|
||||
}
|
||||
} else {
|
||||
ngDevMode && console.warn(` WARNING:
|
||||
ignoring unsafe attribute value ${lowerAttrName} on element $ {
|
||||
tagName
|
||||
} (see http://g.co/ng/security#xss)`);
|
||||
ngDevMode &&
|
||||
console.warn(
|
||||
`WARNING: ignoring unsafe attribute value ` +
|
||||
`${lowerAttrName} on element ${tagName} (see http://g.co/ng/security#xss)`);
|
||||
}
|
||||
} else {
|
||||
addCreateAttribute(create, newIndex, attr);
|
||||
|
|
|
@ -122,11 +122,10 @@ export function ɵɵi18n(index: number, messageIndex: number, subTemplateIndex?:
|
|||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵi18nAttributes(index: number, attrsIndex: number): void {
|
||||
const lView = getLView();
|
||||
const tView = getTView();
|
||||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||||
const attrs = getConstant<string[]>(tView.consts, attrsIndex)!;
|
||||
i18nAttributesFirstPass(lView, tView, index + HEADER_OFFSET, attrs);
|
||||
i18nAttributesFirstPass(tView, index + HEADER_OFFSET, attrs);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2054,6 +2054,26 @@ describe('di', () => {
|
|||
expect(directive.outputAttr).toBeNull();
|
||||
expect(directive.other).toBe('otherValue');
|
||||
});
|
||||
|
||||
it('should inject `null` for attributes with data bindings', () => {
|
||||
@Directive({selector: '[dir]'})
|
||||
class MyDir {
|
||||
constructor(@Attribute('title') public attrValue: string) {}
|
||||
}
|
||||
|
||||
@Component({template: '<div dir title="title {{ value }}"></div>'})
|
||||
class MyComp {
|
||||
@ViewChild(MyDir) directiveInstance!: MyDir;
|
||||
value = 'value';
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [MyDir, MyComp]});
|
||||
const fixture = TestBed.createComponent(MyComp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.directiveInstance.attrValue).toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('div').getAttribute('title')).toBe('title value');
|
||||
});
|
||||
});
|
||||
|
||||
it('should support dependencies in Pipes used inside ICUs', () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {CommonModule, DOCUMENT, registerLocaleData} from '@angular/common';
|
|||
import localeEs from '@angular/common/locales/es';
|
||||
import localeRo from '@angular/common/locales/ro';
|
||||
import {computeMsgId} from '@angular/compiler';
|
||||
import {Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core';
|
||||
import {Attribute, Component, ContentChild, ContentChildren, Directive, ElementRef, HostBinding, Input, LOCALE_ID, NO_ERRORS_SCHEMA, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, Type, ViewChild, ViewContainerRef, ɵsetDocument} from '@angular/core';
|
||||
import {DebugNode, HEADER_OFFSET, TVIEW} from '@angular/core/src/render3/interfaces/view';
|
||||
import {getComponentLView} from '@angular/core/src/render3/util/discovery_utils';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
@ -1843,40 +1843,6 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
|||
expect(fixture.nativeElement.firstChild.title).toEqual(`ANGULAR - value 1 - value 2 (fr)`);
|
||||
});
|
||||
|
||||
it('should create corresponding ng-reflect properties', () => {
|
||||
@Component({
|
||||
selector: 'welcome',
|
||||
template: '{{ messageText }}',
|
||||
})
|
||||
class WelcomeComp {
|
||||
@Input() messageText!: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<welcome
|
||||
messageText="Hello"
|
||||
i18n-messageText="Welcome message description">
|
||||
</welcome>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [App, WelcomeComp],
|
||||
});
|
||||
loadTranslations({
|
||||
[computeMsgId('Hello')]: 'Bonjour',
|
||||
});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
const comp = fixture.debugElement.query(By.css('welcome'));
|
||||
expect(comp.attributes['messagetext']).toBe('Bonjour');
|
||||
expect(comp.attributes['ng-reflect-message-text']).toBe('Bonjour');
|
||||
});
|
||||
|
||||
it('should support i18n attributes on <ng-container> elements', () => {
|
||||
loadTranslations({[computeMsgId('Hello', 'meaning')]: 'Bonjour'});
|
||||
|
||||
|
@ -3024,6 +2990,50 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
|||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toEqual(`two,one,`);
|
||||
});
|
||||
|
||||
it('should be able to inject a static i18n attribute', () => {
|
||||
loadTranslations({[computeMsgId('text')]: 'translatedText'});
|
||||
|
||||
@Directive({selector: '[injectTitle]'})
|
||||
class InjectTitleDir {
|
||||
constructor(@Attribute('title') public title: string) {}
|
||||
}
|
||||
|
||||
@Component({template: `<div i18n-title title="text" injectTitle></div>`})
|
||||
class App {
|
||||
@ViewChild(InjectTitleDir) dir!: InjectTitleDir;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App, InjectTitleDir]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.dir.title).toBe('translatedText');
|
||||
expect(fixture.nativeElement.querySelector('div').getAttribute('title')).toBe('translatedText');
|
||||
});
|
||||
|
||||
it('should inject `null` for an i18n attribute with an interpolation', () => {
|
||||
loadTranslations({[computeMsgId('text {$INTERPOLATION}')]: 'translatedText {$INTERPOLATION}'});
|
||||
|
||||
@Directive({selector: '[injectTitle]'})
|
||||
class InjectTitleDir {
|
||||
constructor(@Attribute('title') public title: string) {}
|
||||
}
|
||||
|
||||
@Component({template: `<div i18n-title title="text {{ value }}" injectTitle></div>`})
|
||||
class App {
|
||||
@ViewChild(InjectTitleDir) dir!: InjectTitleDir;
|
||||
value = 'value';
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [App, InjectTitleDir]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.dir.title).toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('div').getAttribute('title'))
|
||||
.toBe('translatedText value');
|
||||
});
|
||||
});
|
||||
|
||||
function initWithTemplate(compType: Type<any>, template: string) {
|
||||
|
|
|
@ -416,30 +416,6 @@ describe('Runtime i18n', () => {
|
|||
});
|
||||
|
||||
describe(`i18nAttribute`, () => {
|
||||
it('for text', () => {
|
||||
const message = `Hello world!`;
|
||||
const attrs = ['title', message];
|
||||
const nbDecls = 2;
|
||||
const index = 1;
|
||||
const fixture = new TemplateFixture({
|
||||
create: () => {
|
||||
ɵɵelementStart(0, 'div');
|
||||
ɵɵi18nAttributes(index, 0);
|
||||
ɵɵelementEnd();
|
||||
},
|
||||
decls: nbDecls,
|
||||
vars: index,
|
||||
consts: [attrs],
|
||||
});
|
||||
const tView = fixture.hostView[TVIEW];
|
||||
const opCodes = tView.data[HEADER_OFFSET + index] as I18nUpdateOpCodes;
|
||||
|
||||
expect(opCodes).toEqual([]);
|
||||
expect((getNativeByIndex(HEADER_OFFSET, fixture.hostView as LView) as any as Element)
|
||||
.getAttribute('title'))
|
||||
.toEqual(message);
|
||||
});
|
||||
|
||||
it('for simple bindings', () => {
|
||||
const message = `Hello <20>0<EFBFBD>!`;
|
||||
const attrs = ['title', message];
|
||||
|
|
Loading…
Reference in New Issue