fix(compiler): support i18n attributes on `<ng-template>` tags (#35681)
Prior to this commit, i18n attributes defined on `<ng-template>` tags were not processed by the compiler. This commit adds the necessary logic to handle i18n attributes in the same way how these attrs are processed for regular elements. PR Close #35681
This commit is contained in:
parent
35c9f0dc2f
commit
40da51f641
|
@ -331,6 +331,78 @@ describe('i18n support in the template compiler', () => {
|
||||||
verify(input, output);
|
verify(input, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support i18n attributes on explicit <ng-template> elements', () => {
|
||||||
|
const input = `
|
||||||
|
<ng-template i18n-title title="Hello"></ng-template>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO (FW-1942): update the code to avoid adding `title` attribute in plain form
|
||||||
|
// into the `consts` array on Component def.
|
||||||
|
const output = String.raw `
|
||||||
|
var $I18N_0$;
|
||||||
|
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||||
|
const $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$ = goog.getMsg("Hello");
|
||||||
|
$I18N_0$ = $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$I18N_0$ = $localize \`Hello\`;
|
||||||
|
}
|
||||||
|
const $_c2$ = ["title", $I18N_0$];
|
||||||
|
…
|
||||||
|
consts: [["title", "Hello"]],
|
||||||
|
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, $_c2$);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support i18n attributes on explicit <ng-template> with structural directives',
|
||||||
|
() => {
|
||||||
|
const input = `
|
||||||
|
<ng-template *ngIf="visible" i18n-title title="Hello">Test</ng-template>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO (FW-1942): update the code to avoid adding `title` attribute in plain form
|
||||||
|
// into the `consts` array on Component def.
|
||||||
|
const output = String.raw `
|
||||||
|
var $I18N_0$;
|
||||||
|
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||||
|
const $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$ = goog.getMsg("Hello");
|
||||||
|
$I18N_0$ = $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$I18N_0$ = $localize \`Hello\`;
|
||||||
|
}
|
||||||
|
const $_c2$ = ["title", $I18N_0$];
|
||||||
|
function MyComponent_0_ng_template_0_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵtext(0, "Test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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, $_c2$);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
…
|
||||||
|
consts: [[${AttributeMarker.Template}, "ngIf"], ["title", "Hello"]],
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 2, 0, undefined, 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵɵproperty("ngIf", ctx.visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
verify(input, output);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not create translations for empty attributes', () => {
|
it('should not create translations for empty attributes', () => {
|
||||||
const input = `
|
const input = `
|
||||||
<div id="static" i18n-title="m|d" title></div>
|
<div id="static" i18n-title="m|d" title></div>
|
||||||
|
|
|
@ -472,6 +472,46 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this.i18n = null; // reset local i18n context
|
this.i18n = null; // reset local i18n context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private i18nAttributesInstruction(
|
||||||
|
nodeIndex: number, attrs: (t.TextAttribute|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) {
|
||||||
|
const placeholders = assembleBoundTextPlaceholders(message);
|
||||||
|
const params = placeholdersToParams(placeholders);
|
||||||
|
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
|
||||||
|
converted.expressions.forEach(expression => {
|
||||||
|
hasBindings = true;
|
||||||
|
bindings.push({
|
||||||
|
sourceSpan,
|
||||||
|
value: () => this.convertPropertyBinding(expression),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (bindings.length > 0) {
|
||||||
|
this.updateInstructionChainWithAdvance(nodeIndex, R3.i18nExp, bindings);
|
||||||
|
}
|
||||||
|
if (i18nAttrArgs.length > 0) {
|
||||||
|
const index: o.Expression = o.literal(this.allocateDataSlot());
|
||||||
|
const args = this.constantPool.getConstLiteral(o.literalArr(i18nAttrArgs), true);
|
||||||
|
this.creationInstruction(sourceSpan, R3.i18nAttributes, [index, args]);
|
||||||
|
if (hasBindings) {
|
||||||
|
this.updateInstruction(sourceSpan, R3.i18nApply, [index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getNamespaceInstruction(namespaceKey: string|null) {
|
private getNamespaceInstruction(namespaceKey: string|null) {
|
||||||
switch (namespaceKey) {
|
switch (namespaceKey) {
|
||||||
case 'math':
|
case 'math':
|
||||||
|
@ -548,10 +588,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
stylingBuilder.registerClassAttr(value);
|
stylingBuilder.registerClassAttr(value);
|
||||||
} else {
|
} else {
|
||||||
if (attr.i18n) {
|
if (attr.i18n) {
|
||||||
// Place attributes into a separate array for i18n processing, but also keep such
|
|
||||||
// attributes in the main list to make them available for directive matching at runtime.
|
|
||||||
// TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart`
|
|
||||||
// arguments
|
|
||||||
i18nAttrs.push(attr);
|
i18nAttrs.push(attr);
|
||||||
} else {
|
} else {
|
||||||
outputAttrs.push(attr);
|
outputAttrs.push(attr);
|
||||||
|
@ -575,10 +611,6 @@ 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) {
|
||||||
// Place attributes into a separate array for i18n processing, but also keep such
|
|
||||||
// attributes in the main list to make them available for directive matching at runtime.
|
|
||||||
// TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart`
|
|
||||||
// arguments
|
|
||||||
i18nAttrs.push(input);
|
i18nAttrs.push(input);
|
||||||
} else {
|
} else {
|
||||||
allOtherInputs.push(input);
|
allOtherInputs.push(input);
|
||||||
|
@ -631,43 +663,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
this.creationInstruction(element.sourceSpan, R3.disableBindings);
|
this.creationInstruction(element.sourceSpan, R3.disableBindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// process i18n element attributes
|
if (i18nAttrs.length > 0) {
|
||||||
if (i18nAttrs.length) {
|
this.i18nAttributesInstruction(elementIndex, i18nAttrs, element.sourceSpan);
|
||||||
let hasBindings: boolean = false;
|
|
||||||
const i18nAttrArgs: o.Expression[] = [];
|
|
||||||
const bindings: ChainableBindingInstruction[] = [];
|
|
||||||
i18nAttrs.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) {
|
|
||||||
const placeholders = assembleBoundTextPlaceholders(message);
|
|
||||||
const params = placeholdersToParams(placeholders);
|
|
||||||
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
|
|
||||||
converted.expressions.forEach(expression => {
|
|
||||||
hasBindings = true;
|
|
||||||
bindings.push({
|
|
||||||
sourceSpan: element.sourceSpan,
|
|
||||||
value: () => this.convertPropertyBinding(expression)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (bindings.length) {
|
|
||||||
this.updateInstructionChainWithAdvance(elementIndex, R3.i18nExp, bindings);
|
|
||||||
}
|
|
||||||
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.i18nAttributes, [index, args]);
|
|
||||||
if (hasBindings) {
|
|
||||||
this.updateInstruction(element.sourceSpan, R3.i18nApply, [index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate Listeners (outputs)
|
// Generate Listeners (outputs)
|
||||||
|
@ -850,6 +847,8 @@ 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)
|
||||||
|
// TODO (FW-1942): exclude i18n attributes from the main attribute list and pass them
|
||||||
|
// as an `i18nAttrs` argument of the `getAttributeExpressions` function below.
|
||||||
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
|
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
|
||||||
template.attributes, template.inputs, template.outputs, undefined, template.templateAttrs,
|
template.attributes, template.inputs, template.outputs, undefined, template.templateAttrs,
|
||||||
undefined);
|
undefined);
|
||||||
|
@ -894,8 +893,17 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
// handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al;
|
// handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al;
|
||||||
this.templatePropertyBindings(templateIndex, template.templateAttrs);
|
this.templatePropertyBindings(templateIndex, template.templateAttrs);
|
||||||
|
|
||||||
// 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) {
|
||||||
|
// 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>
|
||||||
|
// elements, in case of inline templates, corresponding instructions will be generated in the
|
||||||
|
// nested template function.
|
||||||
|
const i18nAttrs: t.TextAttribute[] = template.attributes.filter(attr => !!attr.i18n);
|
||||||
|
if (i18nAttrs.length > 0) {
|
||||||
|
this.i18nAttributesInstruction(templateIndex, i18nAttrs, template.sourceSpan);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the input bindings
|
// Add the input bindings
|
||||||
this.templatePropertyBindings(templateIndex, template.inputs);
|
this.templatePropertyBindings(templateIndex, template.inputs);
|
||||||
// Generate listeners for directive output
|
// Generate listeners for directive output
|
||||||
|
|
|
@ -1358,6 +1358,71 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
|
||||||
expect(element.title).toBe('Bonjour Angular');
|
expect(element.title).toBe('Bonjour Angular');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should process i18n attributes on explicit <ng-template> elements', () => {
|
||||||
|
const titleDirInstances: TitleDir[] = [];
|
||||||
|
loadTranslations({[computeMsgId('Hello')]: 'Bonjour'});
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[title]',
|
||||||
|
})
|
||||||
|
class TitleDir {
|
||||||
|
@Input() title = '';
|
||||||
|
constructor() { titleDirInstances.push(this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'comp',
|
||||||
|
template: '<ng-template i18n-title title="Hello"></ng-template>',
|
||||||
|
})
|
||||||
|
class Comp {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [Comp, TitleDir],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(Comp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// make sure we only match `TitleDir` once
|
||||||
|
expect(titleDirInstances.length).toBe(1);
|
||||||
|
|
||||||
|
expect(titleDirInstances[0].title).toBe('Bonjour');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match directive only once in case i18n attrs are present on inline template', () => {
|
||||||
|
const titleDirInstances: TitleDir[] = [];
|
||||||
|
loadTranslations({[computeMsgId('Hello')]: 'Bonjour'});
|
||||||
|
|
||||||
|
@Directive({selector: '[title]'})
|
||||||
|
class TitleDir {
|
||||||
|
@Input() title: string = '';
|
||||||
|
constructor(public elRef: ElementRef) { titleDirInstances.push(this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-cmp',
|
||||||
|
template: `
|
||||||
|
<button *ngIf="true" i18n-title title="Hello"></button>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule],
|
||||||
|
declarations: [Cmp, TitleDir],
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// make sure we only match `TitleDir` once and on the right element
|
||||||
|
expect(titleDirInstances.length).toBe(1);
|
||||||
|
expect(titleDirInstances[0].elRef.nativeElement instanceof HTMLButtonElement).toBeTruthy();
|
||||||
|
|
||||||
|
expect(titleDirInstances[0].title).toBe('Bonjour');
|
||||||
|
});
|
||||||
|
|
||||||
it('should apply i18n attributes during second template pass', () => {
|
it('should apply i18n attributes during second template pass', () => {
|
||||||
loadTranslations({[computeMsgId('Set')]: 'Set'});
|
loadTranslations({[computeMsgId('Set')]: 'Set'});
|
||||||
@Directive({
|
@Directive({
|
||||||
|
|
Loading…
Reference in New Issue