Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 40da51f

Browse files
AndrewKushniratscott
authored andcommitted
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
1 parent 35c9f0d commit 40da51f

File tree

3 files changed

+191
-46
lines changed

3 files changed

+191
-46
lines changed

‎packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,78 @@ describe('i18n support in the template compiler', () => {
331331
verify(input, output);
332332
});
333333

334+
it('should support i18n attributes on explicit <ng-template> elements', () => {
335+
const input = `
336+
<ng-template i18n-title title="Hello"></ng-template>
337+
`;
338+
339+
// TODO (FW-1942): update the code to avoid adding `title` attribute in plain form
340+
// into the `consts` array on Component def.
341+
const output = String.raw `
342+
var $I18N_0$;
343+
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
344+
const $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$ = goog.getMsg("Hello");
345+
$I18N_0$ = $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$;
346+
}
347+
else {
348+
$I18N_0$ = $localize \`Hello\`;
349+
}
350+
const $_c2$ = ["title", $I18N_0$];
351+
…
352+
consts: [["title", "Hello"]],
353+
template: function MyComponent_Template(rf, ctx) {
354+
if (rf & 1) {
355+
$r3$.ɵɵtemplate(0, MyComponent_ng_template_0_Template, 0, 0, "ng-template", 0);
356+
$r3$.ɵɵi18nAttributes(1, $_c2$);
357+
}
358+
}
359+
`;
360+
verify(input, output);
361+
});
362+
363+
it('should support i18n attributes on explicit <ng-template> with structural directives',
364+
() => {
365+
const input = `
366+
<ng-template *ngIf="visible" i18n-title title="Hello">Test</ng-template>
367+
`;
368+
369+
// TODO (FW-1942): update the code to avoid adding `title` attribute in plain form
370+
// into the `consts` array on Component def.
371+
const output = String.raw `
372+
var $I18N_0$;
373+
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
374+
const $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$ = goog.getMsg("Hello");
375+
$I18N_0$ = $MSG_EXTERNAL_6616505470450179563$$APP_SPEC_TS_1$;
376+
}
377+
else {
378+
$I18N_0$ = $localize \`Hello\`;
379+
}
380+
const $_c2$ = ["title", $I18N_0$];
381+
function MyComponent_0_ng_template_0_Template(rf, ctx) {
382+
if (rf & 1) {
383+
$r3$.ɵɵtext(0, "Test");
384+
}
385+
}
386+
function MyComponent_0_Template(rf, ctx) {
387+
if (rf & 1) {
388+
$r3$.ɵɵtemplate(0, MyComponent_0_ng_template_0_Template, 1, 0, "ng-template", 1);
389+
$r3$.ɵɵi18nAttributes(1, $_c2$);
390+
}
391+
}
392+
…
393+
consts: [[${AttributeMarker.Template}, "ngIf"], ["title", "Hello"]],
394+
template: function MyComponent_Template(rf, ctx) {
395+
if (rf & 1) {
396+
$r3$.ɵɵtemplate(0, MyComponent_0_Template, 2, 0, undefined, 0);
397+
}
398+
if (rf & 2) {
399+
$r3$.ɵɵproperty("ngIf", ctx.visible);
400+
}
401+
}
402+
`;
403+
verify(input, output);
404+
});
405+
334406
it('should not create translations for empty attributes', () => {
335407
const input = `
336408
<div id="static" i18n-title="m|d" title></div>

‎packages/compiler/src/render3/view/template.ts

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,46 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
472472
this.i18n = null; // reset local i18n context
473473
}
474474

475+
private i18nAttributesInstruction(
476+
nodeIndex: number, attrs: (t.TextAttribute|t.BoundAttribute)[],
477+
sourceSpan: ParseSourceSpan): void {
478+
let hasBindings: boolean = false;
479+
const i18nAttrArgs: o.Expression[] = [];
480+
const bindings: ChainableBindingInstruction[] = [];
481+
attrs.forEach(attr => {
482+
const message = attr.i18n !as i18n.Message;
483+
if (attr instanceof t.TextAttribute) {
484+
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message));
485+
} else {
486+
const converted = attr.value.visit(this._valueConverter);
487+
this.allocateBindingSlots(converted);
488+
if (converted instanceof Interpolation) {
489+
const placeholders = assembleBoundTextPlaceholders(message);
490+
const params = placeholdersToParams(placeholders);
491+
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
492+
converted.expressions.forEach(expression => {
493+
hasBindings = true;
494+
bindings.push({
495+
sourceSpan,
496+
value: () => this.convertPropertyBinding(expression),
497+
});
498+
});
499+
}
500+
}
501+
});
502+
if (bindings.length > 0) {
503+
this.updateInstructionChainWithAdvance(nodeIndex, R3.i18nExp, bindings);
504+
}
505+
if (i18nAttrArgs.length > 0) {
506+
const index: o.Expression = o.literal(this.allocateDataSlot());
507+
const args = this.constantPool.getConstLiteral(o.literalArr(i18nAttrArgs), true);
508+
this.creationInstruction(sourceSpan, R3.i18nAttributes, [index, args]);
509+
if (hasBindings) {
510+
this.updateInstruction(sourceSpan, R3.i18nApply, [index]);
511+
}
512+
}
513+
}
514+
475515
private getNamespaceInstruction(namespaceKey: string|null) {
476516
switch (namespaceKey) {
477517
case 'math':
@@ -548,10 +588,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
548588
stylingBuilder.registerClassAttr(value);
549589
} else {
550590
if (attr.i18n) {
551-
// Place attributes into a separate array for i18n processing, but also keep such
552-
// attributes in the main list to make them available for directive matching at runtime.
553-
// TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart`
554-
// arguments
555591
i18nAttrs.push(attr);
556592
} else {
557593
outputAttrs.push(attr);
@@ -575,10 +611,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
575611
const stylingInputWasSet = stylingBuilder.registerBoundInput(input);
576612
if (!stylingInputWasSet) {
577613
if (input.type === BindingType.Property && input.i18n) {
578-
// Place attributes into a separate array for i18n processing, but also keep such
579-
// attributes in the main list to make them available for directive matching at runtime.
580-
// TODO(FW-1248): prevent attributes duplication in `i18nAttributes` and `elementStart`
581-
// arguments
582614
i18nAttrs.push(input);
583615
} else {
584616
allOtherInputs.push(input);
@@ -631,43 +663,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
631663
this.creationInstruction(element.sourceSpan, R3.disableBindings);
632664
}
633665

634-
// process i18n element attributes
635-
if (i18nAttrs.length) {
636-
let hasBindings: boolean = false;
637-
const i18nAttrArgs: o.Expression[] = [];
638-
const bindings: ChainableBindingInstruction[] = [];
639-
i18nAttrs.forEach(attr => {
640-
const message = attr.i18n !as i18n.Message;
641-
if (attr instanceof t.TextAttribute) {
642-
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message));
643-
} else {
644-
const converted = attr.value.visit(this._valueConverter);
645-
this.allocateBindingSlots(converted);
646-
if (converted instanceof Interpolation) {
647-
const placeholders = assembleBoundTextPlaceholders(message);
648-
const params = placeholdersToParams(placeholders);
649-
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
650-
converted.expressions.forEach(expression => {
651-
hasBindings = true;
652-
bindings.push({
653-
sourceSpan: element.sourceSpan,
654-
value: () => this.convertPropertyBinding(expression)
655-
});
656-
});
657-
}
658-
}
659-
});
660-
if (bindings.length) {
661-
this.updateInstructionChainWithAdvance(elementIndex, R3.i18nExp, bindings);
662-
}
663-
if (i18nAttrArgs.length) {
664-
const index: o.Expression = o.literal(this.allocateDataSlot());
665-
const args = this.constantPool.getConstLiteral(o.literalArr(i18nAttrArgs), true);
666-
this.creationInstruction(element.sourceSpan, R3.i18nAttributes, [index, args]);
667-
if (hasBindings) {
668-
this.updateInstruction(element.sourceSpan, R3.i18nApply, [index]);
669-
}
670-
}
666+
if (i18nAttrs.length > 0) {
667+
this.i18nAttributesInstruction(elementIndex, i18nAttrs, element.sourceSpan);
671668
}
672669

673670
// Generate Listeners (outputs)
@@ -850,6 +847,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
850847
this.matchDirectives(NG_TEMPLATE_TAG_NAME, template);
851848

852849
// prepare attributes parameter (including attributes used for directive matching)
850+
// TODO (FW-1942): exclude i18n attributes from the main attribute list and pass them
851+
// as an `i18nAttrs` argument of the `getAttributeExpressions` function below.
853852
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
854853
template.attributes, template.inputs, template.outputs, undefined, template.templateAttrs,
855854
undefined);
@@ -894,8 +893,17 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
894893
// handle property bindings e.g. ɵɵproperty('ngForOf', ctx.items), et al;
895894
this.templatePropertyBindings(templateIndex, template.templateAttrs);
896895

897-
// Only add normal input/output binding instructions on explicit ng-template elements.
896+
// Only add normal input/output binding instructions on explicit <ng-template> elements.
898897
if (template.tagName === NG_TEMPLATE_TAG_NAME) {
898+
// Add i18n attributes that may act as inputs to directives. If such attributes are present,
899+
// generate `i18nAttributes` instruction. Note: we generate it only for explicit <ng-template>
900+
// elements, in case of inline templates, corresponding instructions will be generated in the
901+
// nested template function.
902+
const i18nAttrs: t.TextAttribute[] = template.attributes.filter(attr => !!attr.i18n);
903+
if (i18nAttrs.length > 0) {
904+
this.i18nAttributesInstruction(templateIndex, i18nAttrs, template.sourceSpan);
905+
}
906+
899907
// Add the input bindings
900908
this.templatePropertyBindings(templateIndex, template.inputs);
901909
// Generate listeners for directive output

‎packages/core/test/acceptance/i18n_spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,71 @@ onlyInIvy('Ivy i18n logic').describe('runtime i18n', () => {
13581358
expect(element.title).toBe('Bonjour Angular');
13591359
});
13601360

1361+
it('should process i18n attributes on explicit <ng-template> elements', () => {
1362+
const titleDirInstances: TitleDir[] = [];
1363+
loadTranslations({[computeMsgId('Hello')]: 'Bonjour'});
1364+
1365+
@Directive({
1366+
selector: '[title]',
1367+
})
1368+
class TitleDir {
1369+
@Input() title = '';
1370+
constructor() { titleDirInstances.push(this); }
1371+
}
1372+
1373+
@Component({
1374+
selector: 'comp',
1375+
template: '<ng-template i18n-title title="Hello"></ng-template>',
1376+
})
1377+
class Comp {
1378+
}
1379+
1380+
TestBed.configureTestingModule({
1381+
declarations: [Comp, TitleDir],
1382+
});
1383+
1384+
const fixture = TestBed.createComponent(Comp);
1385+
fixture.detectChanges();
1386+
1387+
// make sure we only match `TitleDir` once
1388+
expect(titleDirInstances.length).toBe(1);
1389+
1390+
expect(titleDirInstances[0].title).toBe('Bonjour');
1391+
});
1392+
1393+
it('should match directive only once in case i18n attrs are present on inline template', () => {
1394+
const titleDirInstances: TitleDir[] = [];
1395+
loadTranslations({[computeMsgId('Hello')]: 'Bonjour'});
1396+
1397+
@Directive({selector: '[title]'})
1398+
class TitleDir {
1399+
@Input() title: string = '';
1400+
constructor(public elRef: ElementRef) { titleDirInstances.push(this); }
1401+
}
1402+
1403+
@Component({
1404+
selector: 'my-cmp',
1405+
template: `
1406+
<button *ngIf="true" i18n-title title="Hello"></button>
1407+
`,
1408+
})
1409+
class Cmp {
1410+
}
1411+
1412+
TestBed.configureTestingModule({
1413+
imports: [CommonModule],
1414+
declarations: [Cmp, TitleDir],
1415+
});
1416+
const fixture = TestBed.createComponent(Cmp);
1417+
fixture.detectChanges();
1418+
1419+
// make sure we only match `TitleDir` once and on the right element
1420+
expect(titleDirInstances.length).toBe(1);
1421+
expect(titleDirInstances[0].elRef.nativeElement instanceof HTMLButtonElement).toBeTruthy();
1422+
1423+
expect(titleDirInstances[0].title).toBe('Bonjour');
1424+
});
1425+
13611426
it('should apply i18n attributes during second template pass', () => {
13621427
loadTranslations({[computeMsgId('Set')]: 'Set'});
13631428
@Directive({

0 commit comments

Comments
 (0)