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 files = getAppFilesWithTemplate(input, extra.inputArgs);
|
||||||
const opts = (i18nUseExternalIds: boolean) =>
|
const opts = (i18nUseExternalIds: boolean) =>
|
||||||
({i18nUseExternalIds, ...(extra.compilerOptions || {})});
|
({i18nUseExternalIds, ...(extra.compilerOptions || {})});
|
||||||
|
@ -161,7 +168,7 @@ const verify = (input: string, output: string, extra: any = {}): void => {
|
||||||
// invoke with file-based prefix translation names
|
// invoke with file-based prefix translation names
|
||||||
if (!extra.skipPathBasedCheck) {
|
if (!extra.skipPathBasedCheck) {
|
||||||
const result = compile(files, angularFiles, opts(false));
|
const result = compile(files, angularFiles, opts(false));
|
||||||
maybePrint(result.source, extra.verbose);
|
maybePrint(result.source, !!extra.verbose);
|
||||||
expect(verifyPlaceholdersIntegrity(result.source)).toBe(true);
|
expect(verifyPlaceholdersIntegrity(result.source)).toBe(true);
|
||||||
expect(verifyUniqueConsts(result.source)).toBe(true);
|
expect(verifyUniqueConsts(result.source)).toBe(true);
|
||||||
expectEmit(result.source, output, 'Incorrect template');
|
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
|
// invoke with translation names based on external ids
|
||||||
if (!extra.skipIdBasedCheck) {
|
if (!extra.skipIdBasedCheck) {
|
||||||
const result = compile(files, angularFiles, opts(true));
|
const result = compile(files, angularFiles, opts(true));
|
||||||
maybePrint(result.source, extra.verbose);
|
maybePrint(result.source, !!extra.verbose);
|
||||||
const interpolationConfig = extra.inputArgs && extra.inputArgs.interpolation ?
|
const interpolationConfig = extra.inputArgs && extra.inputArgs.interpolation ?
|
||||||
InterpolationConfig.fromArray(extra.inputArgs.interpolation) :
|
InterpolationConfig.fromArray(extra.inputArgs.interpolation) :
|
||||||
undefined;
|
undefined;
|
||||||
|
@ -346,7 +353,6 @@ describe('i18n support in the template compiler', () => {
|
||||||
${i18n_7}
|
${i18n_7}
|
||||||
return [
|
return [
|
||||||
$i18n_0$,
|
$i18n_0$,
|
||||||
[${AttributeMarker.I18n}, "title"],
|
|
||||||
["title", $i18n_1$],
|
["title", $i18n_1$],
|
||||||
["title", $i18n_2$],
|
["title", $i18n_2$],
|
||||||
["title", $i18n_3$],
|
["title", $i18n_3$],
|
||||||
|
@ -362,31 +368,25 @@ describe('i18n support in the template compiler', () => {
|
||||||
$r3$.ɵɵi18n(1, 0);
|
$r3$.ɵɵi18n(1, 0);
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(2, "div", 1);
|
$r3$.ɵɵelementStart(2, "div", 1);
|
||||||
$r3$.ɵɵi18nAttributes(3, 2);
|
$r3$.ɵɵtext(3, "Content B");
|
||||||
$r3$.ɵɵtext(4, "Content B");
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(5, "div", 1);
|
$r3$.ɵɵelementStart(4, "div", 2);
|
||||||
$r3$.ɵɵi18nAttributes(6, 3);
|
$r3$.ɵɵtext(5, "Content C");
|
||||||
$r3$.ɵɵtext(7, "Content C");
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(8, "div", 1);
|
$r3$.ɵɵelementStart(6, "div", 3);
|
||||||
$r3$.ɵɵi18nAttributes(9, 4);
|
$r3$.ɵɵtext(7, "Content D");
|
||||||
$r3$.ɵɵtext(10, "Content D");
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(11, "div", 1);
|
$r3$.ɵɵelementStart(8, "div", 4);
|
||||||
$r3$.ɵɵi18nAttributes(12, 5);
|
$r3$.ɵɵtext(9, "Content E");
|
||||||
$r3$.ɵɵtext(13, "Content E");
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(14, "div", 1);
|
$r3$.ɵɵelementStart(10, "div", 5);
|
||||||
$r3$.ɵɵi18nAttributes(15, 6);
|
$r3$.ɵɵtext(11, "Content F");
|
||||||
$r3$.ɵɵtext(16, "Content F");
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(17, "div", 1);
|
$r3$.ɵɵelementStart(12, "div", 6);
|
||||||
$r3$.ɵɵi18nAttributes(18, 7);
|
$r3$.ɵɵtext(13, "Content G");
|
||||||
$r3$.ɵɵtext(19, "Content G");
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
$r3$.ɵɵelementStart(20, "div");
|
$r3$.ɵɵelementStart(14, "div");
|
||||||
$r3$.ɵɵi18n(21, 8);
|
$r3$.ɵɵi18n(15, 7);
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -405,14 +405,12 @@ describe('i18n support in the template compiler', () => {
|
||||||
consts: function () {
|
consts: function () {
|
||||||
${i18n_0}
|
${i18n_0}
|
||||||
return [
|
return [
|
||||||
[${AttributeMarker.I18n}, "title"],
|
|
||||||
["title", $i18n_0$]
|
["title", $i18n_0$]
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵtemplate(0, MyComponent_ng_template_0_Template, 0, 0, "ng-template", 0);
|
$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) {
|
function MyComponent_0_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵtemplate(0, MyComponent_0_ng_template_0_Template, 1, 0, "ng-template", 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}
|
${i18n_0}
|
||||||
return [
|
return [
|
||||||
[${AttributeMarker.Template}, "ngIf"],
|
[${AttributeMarker.Template}, "ngIf"],
|
||||||
[${AttributeMarker.I18n}, "title"],
|
|
||||||
["title", $i18n_0$]
|
["title", $i18n_0$]
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
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) {
|
if (rf & 2) {
|
||||||
$r3$.ɵɵproperty("ngIf", ctx.visible);
|
$r3$.ɵɵproperty("ngIf", ctx.visible);
|
||||||
|
@ -584,15 +580,12 @@ describe('i18n support in the template compiler', () => {
|
||||||
consts: function() {
|
consts: function() {
|
||||||
${i18n_0}
|
${i18n_0}
|
||||||
return [
|
return [
|
||||||
["id", "static", ${AttributeMarker.I18n}, "title"],
|
["id", "static", "title", $i18n_0$]
|
||||||
["title", $i18n_0$]
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵelementStart(0, "div", 0);
|
$r3$.ɵɵelement(0, "div", 0);
|
||||||
$r3$.ɵɵi18nAttributes(1, 1);
|
|
||||||
$r3$.ɵɵelementEnd();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -640,9 +633,9 @@ describe('i18n support in the template compiler', () => {
|
||||||
${i18n_3}
|
${i18n_3}
|
||||||
${i18n_4}
|
${i18n_4}
|
||||||
return [
|
return [
|
||||||
["id", "dynamic-1", ${AttributeMarker.I18n}, "aria-roledescription",
|
["id", "dynamic-1", "aria-roledescription", $i18n_0$, ${AttributeMarker.I18n},
|
||||||
"title", "aria-label"],
|
"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"],
|
["id", "dynamic-2", ${AttributeMarker.I18n}, "title", "aria-roledescription"],
|
||||||
["title", $i18n_3$, "aria-roledescription", $i18n_4$]
|
["title", $i18n_3$, "aria-roledescription", $i18n_4$]
|
||||||
];
|
];
|
||||||
|
@ -790,76 +783,6 @@ describe('i18n support in the template compiler', () => {
|
||||||
verify(input, output);
|
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', () => {
|
it('should correctly bind to context in nested template', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div *ngFor="let outer of items">
|
<div *ngFor="let outer of items">
|
||||||
|
@ -925,7 +848,6 @@ describe('i18n support in the template compiler', () => {
|
||||||
${i18n_0}
|
${i18n_0}
|
||||||
${i18n_1}
|
${i18n_1}
|
||||||
return [
|
return [
|
||||||
[${AttributeMarker.I18n}, "title"],
|
|
||||||
["title", $i18n_0$],
|
["title", $i18n_0$],
|
||||||
$i18n_1$
|
$i18n_1$
|
||||||
];
|
];
|
||||||
|
@ -933,8 +855,7 @@ describe('i18n support in the template compiler', () => {
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵelementStart(0, "div", 0);
|
$r3$.ɵɵelementStart(0, "div", 0);
|
||||||
$r3$.ɵɵi18nAttributes(1, 1);
|
$r3$.ɵɵi18n(1, 1);
|
||||||
$r3$.ɵɵi18n(2, 2);
|
|
||||||
$r3$.ɵɵelementEnd();
|
$r3$.ɵɵelementEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,11 @@ export function hasI18nMeta(node: t.Node&{i18n?: i18n.I18nMeta}): boolean {
|
||||||
return !!node.i18n;
|
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 {
|
export function hasI18nAttrs(element: html.Element): boolean {
|
||||||
return element.attrs.some((attr: html.Attribute) => isI18nAttribute(attr.name));
|
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 {createGoogleGetMsgStatements} from './i18n/get_msg_utils';
|
||||||
import {createLocalizeStatements} from './i18n/localize_utils';
|
import {createLocalizeStatements} from './i18n/localize_utils';
|
||||||
import {I18nMetaVisitor} from './i18n/meta';
|
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 {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';
|
import {asLiteral, chainedInstruction, CONTEXT_NAME, getAttrsForDirectiveMatching, getInterpolationArgsLength, IMPLICIT_REFERENCE, invalid, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, trimTrailingNulls, unsupported} from './util';
|
||||||
|
|
||||||
|
@ -492,30 +492,25 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
private i18nAttributesInstruction(
|
private i18nAttributesInstruction(
|
||||||
nodeIndex: number, attrs: (t.TextAttribute|t.BoundAttribute)[],
|
nodeIndex: number, attrs: t.BoundAttribute[], sourceSpan: ParseSourceSpan): void {
|
||||||
sourceSpan: ParseSourceSpan): void {
|
|
||||||
let hasBindings: boolean = false;
|
let hasBindings: boolean = false;
|
||||||
const i18nAttrArgs: o.Expression[] = [];
|
const i18nAttrArgs: o.Expression[] = [];
|
||||||
const bindings: ChainableBindingInstruction[] = [];
|
const bindings: ChainableBindingInstruction[] = [];
|
||||||
attrs.forEach(attr => {
|
attrs.forEach(attr => {
|
||||||
const message = attr.i18n! as i18n.Message;
|
const message = attr.i18n! as i18n.Message;
|
||||||
if (attr instanceof t.TextAttribute) {
|
const converted = attr.value.visit(this._valueConverter);
|
||||||
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message));
|
this.allocateBindingSlots(converted);
|
||||||
} else {
|
if (converted instanceof Interpolation) {
|
||||||
const converted = attr.value.visit(this._valueConverter);
|
const placeholders = assembleBoundTextPlaceholders(message);
|
||||||
this.allocateBindingSlots(converted);
|
const params = placeholdersToParams(placeholders);
|
||||||
if (converted instanceof Interpolation) {
|
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
|
||||||
const placeholders = assembleBoundTextPlaceholders(message);
|
converted.expressions.forEach(expression => {
|
||||||
const params = placeholdersToParams(placeholders);
|
hasBindings = true;
|
||||||
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
|
bindings.push({
|
||||||
converted.expressions.forEach(expression => {
|
sourceSpan,
|
||||||
hasBindings = true;
|
value: () => this.convertPropertyBinding(expression),
|
||||||
bindings.push({
|
|
||||||
sourceSpan,
|
|
||||||
value: () => this.convertPropertyBinding(expression),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (bindings.length > 0) {
|
if (bindings.length > 0) {
|
||||||
|
@ -591,7 +586,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
const isI18nRootElement: boolean =
|
const isI18nRootElement: boolean =
|
||||||
isI18nRootNode(element.i18n) && !isSingleI18nIcu(element.i18n);
|
isI18nRootNode(element.i18n) && !isSingleI18nIcu(element.i18n);
|
||||||
|
|
||||||
const i18nAttrs: (t.TextAttribute|t.BoundAttribute)[] = [];
|
const boundI18nAttrs: t.BoundAttribute[] = [];
|
||||||
const outputAttrs: t.TextAttribute[] = [];
|
const outputAttrs: t.TextAttribute[] = [];
|
||||||
|
|
||||||
const [namespaceKey, elementName] = splitNsName(element.name);
|
const [namespaceKey, elementName] = splitNsName(element.name);
|
||||||
|
@ -606,8 +601,12 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
stylingBuilder.registerStyleAttr(value);
|
stylingBuilder.registerStyleAttr(value);
|
||||||
} else if (name === 'class') {
|
} else if (name === 'class') {
|
||||||
stylingBuilder.registerClassAttr(value);
|
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 {
|
} 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);
|
const stylingInputWasSet = stylingBuilder.registerBoundInput(input);
|
||||||
if (!stylingInputWasSet) {
|
if (!stylingInputWasSet) {
|
||||||
if (input.type === BindingType.Property && input.i18n) {
|
if (input.type === BindingType.Property && input.i18n) {
|
||||||
i18nAttrs.push(input);
|
boundI18nAttrs.push(input);
|
||||||
} else {
|
} else {
|
||||||
allOtherInputs.push(input);
|
allOtherInputs.push(input);
|
||||||
}
|
}
|
||||||
|
@ -636,7 +635,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
|
|
||||||
// add attributes for directive and projection matching purposes
|
// add attributes for directive and projection matching purposes
|
||||||
const attributes: o.Expression[] = this.getAttributeExpressions(
|
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));
|
parameters.push(this.addAttrsToConsts(attributes));
|
||||||
|
|
||||||
// local refs (ex.: <div #foo #bar="baz">)
|
// local refs (ex.: <div #foo #bar="baz">)
|
||||||
|
@ -662,7 +662,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
element.children.length > 0;
|
element.children.length > 0;
|
||||||
|
|
||||||
const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes &&
|
const createSelfClosingInstruction = !stylingBuilder.hasBindingsWithPipes &&
|
||||||
element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren;
|
element.outputs.length === 0 && boundI18nAttrs.length === 0 && !hasChildren;
|
||||||
const createSelfClosingI18nInstruction =
|
const createSelfClosingI18nInstruction =
|
||||||
!createSelfClosingInstruction && hasTextChildrenOnly(element.children);
|
!createSelfClosingInstruction && hasTextChildrenOnly(element.children);
|
||||||
|
|
||||||
|
@ -679,9 +679,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this.creationInstruction(element.startSourceSpan, R3.disableBindings);
|
this.creationInstruction(element.startSourceSpan, R3.disableBindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i18nAttrs.length > 0) {
|
if (boundI18nAttrs.length > 0) {
|
||||||
this.i18nAttributesInstruction(
|
this.i18nAttributesInstruction(
|
||||||
elementIndex, i18nAttrs, element.startSourceSpan ?? element.sourceSpan);
|
elementIndex, boundI18nAttrs, element.startSourceSpan ?? element.sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate Listeners (outputs)
|
// Generate Listeners (outputs)
|
||||||
|
@ -866,10 +866,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this.matchDirectives(NG_TEMPLATE_TAG_NAME, template);
|
this.matchDirectives(NG_TEMPLATE_TAG_NAME, template);
|
||||||
|
|
||||||
// prepare attributes parameter (including attributes used for directive matching)
|
// 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(
|
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
|
||||||
NG_TEMPLATE_TAG_NAME, staticAttrs, template.inputs, template.outputs,
|
NG_TEMPLATE_TAG_NAME, attrs, template.inputs, template.outputs, undefined /* styles */,
|
||||||
undefined /* styles */, template.templateAttrs, i18nStaticAttrs);
|
template.templateAttrs, boundI18nAttrs);
|
||||||
parameters.push(this.addAttrsToConsts(attrsExprs));
|
parameters.push(this.addAttrsToConsts(attrsExprs));
|
||||||
|
|
||||||
// local refs (ex.: <ng-template #foo>)
|
// 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.
|
// Only add normal input/output binding instructions on explicit <ng-template> elements.
|
||||||
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
|
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
|
||||||
const [i18nInputs, inputs] = partitionArray(template.inputs, hasI18nMeta);
|
const [i18nInputs, inputs] =
|
||||||
const i18nAttrs = [...i18nStaticAttrs, ...i18nInputs];
|
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,
|
// 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>
|
// 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[],
|
elementName: string, renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[],
|
||||||
outputs: t.BoundEvent[], styles?: StylingBuilder,
|
outputs: t.BoundEvent[], styles?: StylingBuilder,
|
||||||
templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
|
templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
|
||||||
i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] {
|
boundI18nAttrs: t.BoundAttribute[] = []): o.Expression[] {
|
||||||
const alreadySeen = new Set<string>();
|
const alreadySeen = new Set<string>();
|
||||||
const attrExprs: o.Expression[] = [];
|
const attrExprs: o.Expression[] = [];
|
||||||
let ngProjectAsAttr: t.TextAttribute|undefined;
|
let ngProjectAsAttr: t.TextAttribute|undefined;
|
||||||
|
|
||||||
renderAttributes.forEach((attr: t.TextAttribute) => {
|
for (const attr of renderAttributes) {
|
||||||
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
|
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
|
||||||
ngProjectAsAttr = attr;
|
ngProjectAsAttr = attr;
|
||||||
}
|
}
|
||||||
attrExprs.push(
|
|
||||||
...getAttributeNameLiterals(attr.name), trustedConstAttribute(elementName, 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
|
// Keep ngProjectAs next to the other name, value pairs so we can verify that we match
|
||||||
// ngProjectAs marker in the attribute name slot.
|
// 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));
|
templateAttrs.forEach(attr => addAttrExpr(attr.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i18nAttrs.length) {
|
if (boundI18nAttrs.length) {
|
||||||
attrExprs.push(o.literal(core.AttributeMarker.I18n));
|
attrExprs.push(o.literal(core.AttributeMarker.I18n));
|
||||||
i18nAttrs.forEach(attr => addAttrExpr(attr.name));
|
boundI18nAttrs.forEach(attr => addAttrExpr(attr.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrExprs;
|
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
|
* @param conditionFn Condition function that is called for each item in a given array and returns a
|
||||||
* boolean value.
|
* boolean value.
|
||||||
*/
|
*/
|
||||||
export function partitionArray<T>(
|
export function partitionArray<T, F = T>(
|
||||||
arr: T[], conditionFn: <K extends T>(value: K) => boolean): [T[], T[]] {
|
arr: (T|F)[], conditionFn: (value: T|F) => boolean): [T[], F[]] {
|
||||||
const truthy: T[] = [];
|
const truthy: T[] = [];
|
||||||
const falsy: T[] = [];
|
const falsy: F[] = [];
|
||||||
arr.forEach(item => {
|
for (const item of arr) {
|
||||||
(conditionFn(item) ? truthy : falsy).push(item);
|
(conditionFn(item) ? truthy : falsy).push(item as any);
|
||||||
});
|
}
|
||||||
return [truthy, falsy];
|
return [truthy, falsy];
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,16 +14,14 @@ import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer';
|
||||||
import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertOneOf, assertString} from '../../util/assert';
|
import {assertDefined, assertEqual, assertGreaterThanOrEqual, assertOneOf, assertString} from '../../util/assert';
|
||||||
import {CharCode} from '../../util/char_code';
|
import {CharCode} from '../../util/char_code';
|
||||||
import {loadIcuContainerVisitor} from '../instructions/i18n_icu_container_visitor';
|
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 {getDocument} from '../interfaces/document';
|
||||||
import {ELEMENT_MARKER, I18nCreateOpCode, I18nCreateOpCodes, I18nRemoveOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, ICU_MARKER, IcuCreateOpCode, IcuCreateOpCodes, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
|
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 {TNode, TNodeType} from '../interfaces/node';
|
||||||
import {RComment, RElement} from '../interfaces/renderer';
|
|
||||||
import {SanitizerFn} from '../interfaces/sanitization';
|
import {SanitizerFn} from '../interfaces/sanitization';
|
||||||
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
|
import {HEADER_OFFSET, LView, TView} from '../interfaces/view';
|
||||||
import {getCurrentParentTNode, getCurrentTNode, setCurrentTNode} from '../state';
|
import {getCurrentParentTNode, getCurrentTNode, setCurrentTNode} from '../state';
|
||||||
import {attachDebugGetter} from '../util/debug_utils';
|
import {attachDebugGetter} from '../util/debug_utils';
|
||||||
import {getNativeByIndex, getTNode} from '../util/view_utils';
|
|
||||||
|
|
||||||
import {i18nCreateOpCodesToString, i18nRemoveOpCodesToString, i18nUpdateOpCodesToString, icuCreateOpCodesToString} from './i18n_debug';
|
import {i18nCreateOpCodesToString, i18nRemoveOpCodesToString, i18nUpdateOpCodesToString, icuCreateOpCodesToString} from './i18n_debug';
|
||||||
import {addTNodeAndUpdateInsertBeforeIndex} from './i18n_insert_before_index';
|
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
|
// Verify that ICU expression has the right shape. Translations might contain invalid
|
||||||
// constructions (while original messages were correct), so ICU parsing at runtime may
|
// constructions (while original messages were correct), so ICU parsing at runtime may
|
||||||
// not succeed (thus `icuExpression` remains a string).
|
// 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.`);
|
throw new Error(`Unable to parse ICU expression in "${message}" message.`);
|
||||||
}
|
}
|
||||||
const icuContainerTNode = createTNodeAndAddOpCode(
|
const icuContainerTNode = createTNodeAndAddOpCode(
|
||||||
|
@ -225,54 +226,34 @@ function i18nStartFirstCreatePassProcessTextNode(
|
||||||
/**
|
/**
|
||||||
* See `i18nAttributes` above.
|
* See `i18nAttributes` above.
|
||||||
*/
|
*/
|
||||||
export function i18nAttributesFirstPass(
|
export function i18nAttributesFirstPass(tView: TView, index: number, values: string[]) {
|
||||||
lView: LView, tView: TView, index: number, values: string[]) {
|
|
||||||
const previousElement = getCurrentTNode()!;
|
const previousElement = getCurrentTNode()!;
|
||||||
const previousElementIndex = previousElement.index;
|
const previousElementIndex = previousElement.index;
|
||||||
const updateOpCodes: I18nUpdateOpCodes = [] as any;
|
const updateOpCodes: I18nUpdateOpCodes = [] as any;
|
||||||
if (ngDevMode) {
|
if (ngDevMode) {
|
||||||
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < values.length; i += 2) {
|
if (tView.firstCreatePass && tView.data[index] === null) {
|
||||||
const attrName = values[i];
|
for (let i = 0; i < values.length; i += 2) {
|
||||||
const message = values[i + 1];
|
const attrName = values[i];
|
||||||
const parts = message.split(ICU_REGEXP);
|
const message = values[i + 1];
|
||||||
for (let j = 0; j < parts.length; j++) {
|
|
||||||
const value = parts[j];
|
|
||||||
|
|
||||||
if (j & 1) {
|
if (message !== '') {
|
||||||
// Odd indexes are ICU expressions
|
// Check if attribute value contains an ICU and throw an error if that's the case.
|
||||||
// TODO(ocombe): support ICU expressions in attributes
|
// ICUs in element attributes are not supported.
|
||||||
throw new Error('ICU expressions are not yet supported in attributes');
|
// Note: we intentionally retain the error here by not using `ngDevMode`, because
|
||||||
} else if (value !== '') {
|
// the `value` can change based on the locale and users aren't guaranteed to hit
|
||||||
// Even indexes are text (including bindings)
|
// an invalid string while they're developing.
|
||||||
const hasBinding = !!value.match(BINDING_REGEXP);
|
if (ICU_REGEXP.test(message)) {
|
||||||
if (hasBinding) {
|
throw new Error(
|
||||||
if (tView.firstCreatePass && tView.data[index] === null) {
|
`ICU expressions are not supported in attributes. Message: "${message}".`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (tView.firstCreatePass && tView.data[index] === null) {
|
|
||||||
tView.data[index] = updateOpCodes;
|
tView.data[index] = updateOpCodes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -622,10 +603,10 @@ function walkIcuTree(
|
||||||
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name);
|
generateBindingUpdateOpCodes(update, attr.value, newIndex, attr.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ngDevMode && console.warn(` WARNING:
|
ngDevMode &&
|
||||||
ignoring unsafe attribute value ${lowerAttrName} on element $ {
|
console.warn(
|
||||||
tagName
|
`WARNING: ignoring unsafe attribute value ` +
|
||||||
} (see http://g.co/ng/security#xss)`);
|
`${lowerAttrName} on element ${tagName} (see http://g.co/ng/security#xss)`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addCreateAttribute(create, newIndex, attr);
|
addCreateAttribute(create, newIndex, attr);
|
||||||
|
@ -705,4 +686,4 @@ function addCreateNodeAndAppend(
|
||||||
|
|
||||||
function addCreateAttribute(create: IcuCreateOpCodes, newIndex: number, attr: Attr) {
|
function addCreateAttribute(create: IcuCreateOpCodes, newIndex: number, attr: Attr) {
|
||||||
create.push(newIndex << IcuCreateOpCode.SHIFT_REF | IcuCreateOpCode.Attr, attr.name, attr.value);
|
create.push(newIndex << IcuCreateOpCode.SHIFT_REF | IcuCreateOpCode.Attr, attr.name, attr.value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,11 +122,10 @@ export function ɵɵi18n(index: number, messageIndex: number, subTemplateIndex?:
|
||||||
* @codeGenApi
|
* @codeGenApi
|
||||||
*/
|
*/
|
||||||
export function ɵɵi18nAttributes(index: number, attrsIndex: number): void {
|
export function ɵɵi18nAttributes(index: number, attrsIndex: number): void {
|
||||||
const lView = getLView();
|
|
||||||
const tView = getTView();
|
const tView = getTView();
|
||||||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||||||
const attrs = getConstant<string[]>(tView.consts, attrsIndex)!;
|
const attrs = getConstant<string[]>(tView.consts, attrsIndex)!;
|
||||||
i18nAttributesFirstPass(lView, tView, index + HEADER_OFFSET, attrs);
|
i18nAttributesFirstPass(tView, index + HEADER_OFFSET, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,4 +180,4 @@ export function ɵɵi18nApply(index: number) {
|
||||||
export function ɵɵi18nPostprocess(
|
export function ɵɵi18nPostprocess(
|
||||||
message: string, replacements: {[key: string]: (string|string[])} = {}): string {
|
message: string, replacements: {[key: string]: (string|string[])} = {}): string {
|
||||||
return i18nPostprocess(message, replacements);
|
return i18nPostprocess(message, replacements);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2054,6 +2054,26 @@ describe('di', () => {
|
||||||
expect(directive.outputAttr).toBeNull();
|
expect(directive.outputAttr).toBeNull();
|
||||||
expect(directive.other).toBe('otherValue');
|
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', () => {
|
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 localeEs from '@angular/common/locales/es';
|
||||||
import localeRo from '@angular/common/locales/ro';
|
import localeRo from '@angular/common/locales/ro';
|
||||||
import {computeMsgId} from '@angular/compiler';
|
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 {DebugNode, HEADER_OFFSET, TVIEW} from '@angular/core/src/render3/interfaces/view';
|
||||||
import {getComponentLView} from '@angular/core/src/render3/util/discovery_utils';
|
import {getComponentLView} from '@angular/core/src/render3/util/discovery_utils';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
@ -642,9 +642,9 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
// such a case a single `TIcuContainerNode` should be generated only.
|
// such a case a single `TIcuContainerNode` should be generated only.
|
||||||
it('should create a single dynamic TNode for ICU', () => {
|
it('should create a single dynamic TNode for ICU', () => {
|
||||||
const fixture = initWithTemplate(AppComp, `
|
const fixture = initWithTemplate(AppComp, `
|
||||||
{count, plural,
|
{count, plural,
|
||||||
=0 {just now}
|
=0 {just now}
|
||||||
=1 {one minute ago}
|
=1 {one minute ago}
|
||||||
other {{{count}} minutes ago}
|
other {{{count}} minutes ago}
|
||||||
}
|
}
|
||||||
`.trim());
|
`.trim());
|
||||||
|
@ -662,13 +662,13 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
|
|
||||||
it('should support multiple ICUs', () => {
|
it('should support multiple ICUs', () => {
|
||||||
const fixture = initWithTemplate(AppComp, `
|
const fixture = initWithTemplate(AppComp, `
|
||||||
{count, plural,
|
{count, plural,
|
||||||
=0 {just now}
|
=0 {just now}
|
||||||
=1 {one minute ago}
|
=1 {one minute ago}
|
||||||
other {{{count}} minutes ago}
|
other {{{count}} minutes ago}
|
||||||
}
|
}
|
||||||
{name, select,
|
{name, select,
|
||||||
Angular {Mr. Angular}
|
Angular {Mr. Angular}
|
||||||
other {Sir}
|
other {Sir}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -1843,40 +1843,6 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
expect(fixture.nativeElement.firstChild.title).toEqual(`ANGULAR - value 1 - value 2 (fr)`);
|
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', () => {
|
it('should support i18n attributes on <ng-container> elements', () => {
|
||||||
loadTranslations({[computeMsgId('Hello', 'meaning')]: 'Bonjour'});
|
loadTranslations({[computeMsgId('Hello', 'meaning')]: 'Bonjour'});
|
||||||
|
|
||||||
|
@ -3024,6 +2990,50 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(fixture.nativeElement.textContent).toEqual(`two,one,`);
|
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) {
|
function initWithTemplate(compType: Type<any>, template: string) {
|
||||||
|
|
|
@ -416,30 +416,6 @@ describe('Runtime i18n', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`i18nAttribute`, () => {
|
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', () => {
|
it('for simple bindings', () => {
|
||||||
const message = `Hello <20>0<EFBFBD>!`;
|
const message = `Hello <20>0<EFBFBD>!`;
|
||||||
const attrs = ['title', message];
|
const attrs = ['title', message];
|
||||||
|
|
Loading…
Reference in New Issue