diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts index 99dbc96717..61539b698b 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_i18n_spec.ts @@ -24,17 +24,22 @@ const angularFiles = setup({ const htmlParser = new HtmlParser(); +// TODO: update translation extraction RegExp to support i18nLocalize calls once #28689 lands. +const EXTRACT_GENERATED_TRANSLATIONS_REGEXP = + /const\s*(.*?)\s*=\s*goog\.getMsg\("(.*?)",?\s*(.*?)\)/g; + const diff = (a: Set, b: Set): Set => new Set([...Array.from(a)].filter(x => !b.has(x))); -const extract = (from: string, regex: any, transformFn: (match: any[]) => any) => { - const result = new Set(); - let item; - while ((item = regex.exec(from)) !== null) { - result.add(transformFn(item)); - } - return result; -}; +const extract = + (from: string, regex: any, transformFn: (match: any[], state?: Set) => any) => { + const result = new Set(); + let item; + while ((item = regex.exec(from)) !== null) { + result.add(transformFn(item, result)); + } + return result; + }; // verify that we extracted all the necessary translations // and their ids match the ones extracted via 'ng xi18n' @@ -73,8 +78,7 @@ const verifyTranslationIds = // placeholders object defined as goog.getMsg function argument const verifyPlaceholdersIntegrity = (output: string) => { const extactTranslations = (from: string) => { - const regex = /const\s*(.*?)\s*=\s*goog\.getMsg\("(.*?)",?\s*(.*?)\)/g; - return extract(from, regex, v => [v[2], v[3]]); + return extract(from, EXTRACT_GENERATED_TRANSLATIONS_REGEXP, v => [v[2], v[3]]); }; const extractPlaceholdersFromBody = (body: string) => { const regex = /{\$(.*?)}/g; @@ -95,6 +99,19 @@ const verifyPlaceholdersIntegrity = (output: string) => { return true; }; +const verifyUniqueConsts = (output: string) => { + extract( + output, EXTRACT_GENERATED_TRANSLATIONS_REGEXP, + (current: string[], state: Set): string => { + const key = current[1]; + if (state.has(key)) { + throw new Error(`Duplicate const ${key} found in generated output!`); + } + return key; + }); + return true; +}; + const getAppFilesWithTemplate = (template: string, args: any = {}) => ({ app: { 'spec.ts': ` @@ -134,6 +151,7 @@ const verify = (input: string, output: string, extra: any = {}): void => { const result = compile(files, angularFiles, opts(false)); maybePrint(result.source, extra.verbose); expect(verifyPlaceholdersIntegrity(result.source)).toBe(true); + expect(verifyUniqueConsts(result.source)).toBe(true); expectEmit(result.source, output, 'Incorrect template'); } @@ -147,6 +165,7 @@ const verify = (input: string, output: string, extra: any = {}): void => { expect(verifyTranslationIds(input, result.source, extra.exceptions, interpolationConfig)) .toBe(true); expect(verifyPlaceholdersIntegrity(result.source)).toBe(true); + expect(verifyUniqueConsts(result.source)).toBe(true); expectEmit(result.source, output, 'Incorrect template'); } }; @@ -1627,6 +1646,45 @@ describe('i18n support in the view compiler', () => { verify(input, output); }); + + it('should not emit duplicate i18n consts for nested s', () => { + const input = ` + + Root content + + Nested content + + + `; + + const output = String.raw ` + const $MSG_EXTERNAL_8537814667662432133$$APP_SPEC_TS__0$ = goog.getMsg(" Root content {$startTagNgContainer} Nested content {$closeTagNgContainer}", { + "startTagNgContainer": "\uFFFD*1:1\uFFFD\uFFFD#1:1\uFFFD", + "closeTagNgContainer": "\uFFFD/#1:1\uFFFD\uFFFD/*1:1\uFFFD" + }); + … + `; + + verify(input, output); + }); + + it('should not emit duplicate i18n consts for elements with the same content', () => { + const input = ` +
Test
+
Test
+ `; + + // TODO(FW-635): currently we generate unique consts for each i18n block even though it might + // contain the same content. This should be optimized by translation statements caching, that + // can be implemented in the future within FW-635. + const output = String.raw ` + const $MSG_EXTERNAL_6563391987554512024$$APP_SPEC_TS_0$ = goog.getMsg("Test"); + const $MSG_EXTERNAL_6563391987554512024$$APP_SPEC_TS_1$ = goog.getMsg("Test"); + … + `; + + verify(input, output); + }); }); describe('whitespace preserving mode', () => { diff --git a/packages/compiler/src/render3/view/i18n/context.ts b/packages/compiler/src/render3/view/i18n/context.ts index 87c50ac6f8..6be118d8d3 100644 --- a/packages/compiler/src/render3/view/i18n/context.ts +++ b/packages/compiler/src/render3/view/i18n/context.ts @@ -42,6 +42,7 @@ export class I18nContext { public readonly id: number; public bindings = new Set(); public placeholders = new Map(); + public isEmitted: boolean = false; private _registry !: any; private _unresolvedCtxCount: number = 0; diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index fb0af532dc..e5048daf71 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -373,8 +373,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } i18nUpdateRef(context: I18nContext): void { - const {icus, meta, isRoot, isResolved} = context; - if (isRoot && isResolved && !isSingleI18nIcu(meta)) { + const {icus, meta, isRoot, isResolved, isEmitted} = context; + if (isRoot && isResolved && !isEmitted && !isSingleI18nIcu(meta)) { + context.isEmitted = true; const placeholders = context.getSerializedPlaceholders(); let icuMapping: {[name: string]: o.Expression} = {}; let params: {[name: string]: o.Expression} =