diff --git a/packages/core/schematics/ng-generate/control-flow-migration/types.ts b/packages/core/schematics/ng-generate/control-flow-migration/types.ts index 8b37b4c488720..adfaa76cb0f4d 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/types.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/types.ts @@ -80,9 +80,11 @@ export class Template { count: number = 0; contents: string = ''; children: string = ''; + i18n: Attribute|null = null; - constructor(el: Element) { + constructor(el: Element, i18n: Attribute|null) { this.el = el; + this.i18n = i18n; } generateContents(tmpl: string) { @@ -155,12 +157,20 @@ export class TemplateCollector extends RecursiveVisitor { override visitElement(el: Element): void { if (el.name === ngtemplate) { + let i18n = null; + let templateAttr = null; for (const attr of el.attrs) { + if (attr.name === 'i18n') { + i18n = attr; + } if (attr.name.startsWith('#')) { - this.elements.push(new ElementToMigrate(el, attr)); - this.templates.set(attr.name, new Template(el)); + templateAttr = attr; } } + if (templateAttr !== null) { + this.elements.push(new ElementToMigrate(el, templateAttr)); + this.templates.set(templateAttr.name, new Template(el, i18n)); + } } super.visitElement(el, null); } diff --git a/packages/core/schematics/ng-generate/control-flow-migration/util.ts b/packages/core/schematics/ng-generate/control-flow-migration/util.ts index a2f069318eb48..d612c43817b62 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/util.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/util.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {HtmlParser, ParseTreeResult, visitAll} from '@angular/compiler'; +import {Attribute, HtmlParser, ParseTreeResult, visitAll} from '@angular/compiler'; import {dirname, join} from 'path'; import ts from 'typescript'; @@ -173,6 +173,11 @@ export function countTemplateUsage(template: string): Map { return new Map(); } +function wrapIntoI18nContainer(i18nAttr: Attribute, content: string) { + const i18n = i18nAttr.value === '' ? 'i18n' : `i18n="${i18nAttr.value}"`; + return `${content}`; +} + export function processNgTemplates(template: string): string { // count usage const templates = countTemplateUsage(template); @@ -182,7 +187,12 @@ export function processNgTemplates(template: string): string { const placeholder = `${name}|`; if (template.indexOf(placeholder) > -1) { - template = template.replace(placeholder, t.children); + if (t.i18n !== null) { + const container = wrapIntoI18nContainer(t.i18n, t.children); + template = template.replace(placeholder, container); + } else { + template = template.replace(placeholder, t.children); + } if (t.count <= 2) { template = template.replace(t.contents, ''); } @@ -213,6 +223,7 @@ export function getOriginals( export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number): {start: string, middle: string, end: string} { + const i18nAttr = etm.el.attrs.find(x => x.name === 'i18n'); if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') && etm.el.attrs.length === 1) { // this is the case where we're migrating and there's no need to keep the ng-container @@ -220,6 +231,11 @@ export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset; const middle = tmpl.slice(childStart, childEnd); return {start: '', middle, end: ''}; + } else if (etm.el.name === 'ng-template' && etm.el.attrs.length === 2 && i18nAttr !== undefined) { + const childStart = etm.el.children[0].sourceSpan.start.offset - offset; + const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset; + const middle = wrapIntoI18nContainer(i18nAttr, tmpl.slice(childStart, childEnd)); + return {start: '', middle, end: ''}; } const attrStart = etm.attr.keySpan!.start.offset - 1 - offset; diff --git a/packages/core/schematics/test/control_flow_migration_spec.ts b/packages/core/schematics/test/control_flow_migration_spec.ts index 0a89fe5781973..b7f876ffe6c6a 100644 --- a/packages/core/schematics/test/control_flow_migration_spec.ts +++ b/packages/core/schematics/test/control_flow_migration_spec.ts @@ -357,6 +357,130 @@ describe('control flow migration', () => { ].join('\n')); }); + it('should migrate an if case with an ng-template with i18n', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + show = false; + } + `); + + writeFile('/comp.html', [ + `
`, + `Content here`, + `
`, + ].join('\n')); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe([ + `
`, + `@if (show) {`, + `Content here`, + `}`, + `
`, + ].join('\n')); + }); + + it('should migrate an if case with an ng-template with empty i18n', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + show = false; + } + `); + + writeFile('/comp.html', [ + `
`, + `Content here`, + `
`, + ].join('\n')); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe([ + `
`, + `@if (show) {`, + `Content here`, + `}`, + `
`, + ].join('\n')); + }); + + it('should migrate an if case with an ng-container with i18n', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + show = false; + } + `); + + writeFile('/comp.html', [ + `
`, + `Content here`, + `
`, + ].join('\n')); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe([ + `
`, + `@if (show) {`, + `Content here`, + `}`, + `
`, + ].join('\n')); + }); + + it('should migrate an if case with an ng-container with empty i18n', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + show = false; + } + `); + + writeFile('/comp.html', [ + `
`, + `Content here`, + `
`, + ].join('\n')); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe([ + `
`, + `@if (show) {`, + `Content here`, + `}`, + `
`, + ].join('\n')); + }); + it('should migrate an if else case', async () => { writeFile('/comp.ts', ` import {Component} from '@angular/core'; @@ -2560,6 +2684,50 @@ describe('control flow migration', () => { expect(actual).toBe(expected); }); + + it('should preserve i18n attribute on ng-templates in an if/else', async () => { + writeFile('/comp.ts', ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + selector: 'declare-comp', + templateUrl: 'comp.html', + }) + class DeclareComp {} + `); + + writeFile('/comp.html', [ + `
`, + ` `, + ` bla bla`, + ` `, + `
`, + ``, + `
`, + `
`, + ].join('\n')); + + await runMigration(); + const actual = tree.readContent('/comp.html'); + + const expected = [ + `
`, + ` @if (cond) {\n`, + ` bla bla`, + ` `, + `} @else {`, + ``, + ` @for (item of items; track item) {`, + `
`, + `}`, + `
`, + `}`, + `
\n`, + ].join('\n'); + + expect(actual).toBe(expected); + }); }); describe('no migration needed', () => {