diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 13bf8ba0e5..3996fa0711 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -76,8 +76,8 @@ export class DecorationAnalyzer { this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader, this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, /* defaultPreserveWhitespaces */ false, - /* i18nUseExternalIds */ true, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, - NOOP_DEFAULT_IMPORT_RECORDER), + /* i18nUseExternalIds */ true, /* i18nLegacyMessageIdFormat */ '', this.moduleResolver, + this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER), new DirectiveDecoratorHandler( this.reflectionHost, this.evaluator, this.fullRegistry, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index aad3add832..047b620738 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -32,6 +32,7 @@ export function main( if (configErrors.length) { return reportErrorsAndExit(configErrors, /*options*/ undefined, consoleError); } + warnForDeprecatedOptions(options); if (watch) { const result = watchMode(project, options, consoleError); return reportErrorsAndExit(result.firstCompileResult, options, consoleError); @@ -226,6 +227,14 @@ export function watchMode( }, options, options => createEmitCallback(options))); } +function warnForDeprecatedOptions(options: api.CompilerOptions) { + if (options.i18nLegacyMessageIdFormat !== undefined) { + console.warn( + 'The `i18nLegacyMessageIdFormat` option is deprecated.\n' + + 'Migrate your legacy translation files to the new `$localize` message id format and remove this option.'); + } +} + // CLI entry point if (require.main === module) { const args = process.argv.slice(2); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 374390f6e5..a0205bd27d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -52,8 +52,9 @@ export class ComponentDecoratorHandler implements private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean, private resourceLoader: ResourceLoader, private rootDirs: string[], private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean, - private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer, - private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder, + private i18nLegacyMessageIdFormat: string, private moduleResolver: ModuleResolver, + private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter, + private defaultImportRecorder: DefaultImportRecorder, private resourceDependencies: ResourceDependencyRecorder = new NoopResourceDependencyRecorder()) {} @@ -697,7 +698,8 @@ export class ComponentDecoratorHandler implements ...parseTemplate(templateStr, templateUrl, { preserveWhitespaces, interpolationConfig: interpolation, - range: templateRange, escapedString, ...options, + range: templateRange, escapedString, + i18nLegacyMessageIdFormat: this.i18nLegacyMessageIdFormat, ...options, }), template: templateStr, templateUrl, isInline: component.has('template'), diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index b1481d11ea..8156c77e71 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -61,8 +61,8 @@ runInEachFileSystem(() => { const handler = new ComponentDecoratorHandler( reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry, false, - new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer, refEmitter, - NOOP_DEFAULT_IMPORT_RECORDER); + new NoopResourceLoader(), [''], false, true, '', moduleResolver, cycleAnalyzer, + refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); if (detected === undefined) { diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 4c53cc12f2..19126f8b05 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -509,8 +509,8 @@ export class NgtscProgram implements api.Program { this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader, scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false, - this.moduleResolver, this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker, - this.incrementalState), + this.options.i18nLegacyMessageIdFormat || '', this.moduleResolver, this.cycleAnalyzer, + this.refEmitter, this.defaultImportTracker, this.incrementalState), new DirectiveDecoratorHandler( this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), new InjectableDecoratorHandler( diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 86b77464fb..4d30349b7c 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -161,6 +161,19 @@ export interface CompilerOptions extends ts.CompilerOptions { // (used by Closure Compiler's output of `goog.getMsg` for transition period) i18nUseExternalIds?: boolean; + /** + * Render `$localize` message ids with the specified legacy format (xlf, xlf2 or xmb). + * + * Use this option when use are using the `$localize` based localization messages but + * have not migrated the translation files to use the new `$localize` message id format. + * + * @deprecated + * `i18nLegacyMessageIdFormat` should only be used while migrating from legacy message id + * formatted translation files and will be removed at the same time as ViewEngine support is + * removed. + */ + i18nLegacyMessageIdFormat?: string; + // Whether to remove blank text nodes from compiled templates. It is `false` by default starting // from Angular 6. preserveWhitespaces?: boolean; diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index f55217066d..b4b720ed14 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -2052,6 +2052,62 @@ runInEachFileSystem(os => { expect(jsContents).not.toContain('MSG_EXTERNAL_'); }); + it('should render legacy id when i18nLegacyMessageIdFormat config is set to xlf', () => { + env.tsconfig({i18nLegacyMessageIdFormat: 'xlf'}); + env.write(`test.ts`, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '
Some text
' + }) + class FooCmp {}`); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(':@@5dbba0a3da8dff890e20cf76eb075d58900fbcd3:Some text'); + }); + + it('should render legacy id when i18nLegacyMessageIdFormat config is set to xlf2', () => { + env.tsconfig({i18nLegacyMessageIdFormat: 'xlf2'}); + env.write(`test.ts`, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '
Some text
' + }) + class FooCmp {}`); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(':@@8321000940098097247:Some text'); + }); + + it('should render legacy id when i18nLegacyMessageIdFormat config is set to xmb', () => { + env.tsconfig({i18nLegacyMessageIdFormat: 'xmb'}); + env.write(`test.ts`, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '
Some text
' + }) + class FooCmp {}`); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(':@@8321000940098097247:Some text'); + }); + + it('should render custom id even if i18nLegacyMessageIdFormat config is set', () => { + env.tsconfig({i18nLegacyMessageIdFormat: 'xlf'}); + env.write(`test.ts`, ` + import {Component} from '@angular/core'; + @Component({ + selector: 'test', + template: '
Some text
' + }) + class FooCmp {}`); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain(':@@custom:Some text'); + }); + it('@Component\'s `interpolation` should override default interpolation config', () => { env.write(`test.ts`, ` import {Component} from '@angular/core'; diff --git a/packages/compiler/src/i18n/digest.ts b/packages/compiler/src/i18n/digest.ts index 6c735e2a20..eec617a81b 100644 --- a/packages/compiler/src/i18n/digest.ts +++ b/packages/compiler/src/i18n/digest.ts @@ -10,15 +10,31 @@ import {newArray, utf8Encode} from '../util'; import * as i18n from './i18n_ast'; +/** + * Return the message id or compute it using the XLIFF1 digest. + */ export function digest(message: i18n.Message): string { - return message.id || sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`); + return message.id || computeDigest(message); } -export function decimalDigest(message: i18n.Message): string { - if (message.id) { - return message.id; - } +/** + * Compute the message id using the XLIFF1 digest. + */ +export function computeDigest(message: i18n.Message): string { + return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`); +} +/** + * Return the message id or compute it using the XLIFF2/XMB/$localize digest. + */ +export function decimalDigest(message: i18n.Message): string { + return message.id || computeDecimalDigest(message); +} + +/** + * Compute the message id using the XLIFF2/XMB/$localize digest. + */ +export function computeDecimalDigest(message: i18n.Message): string { const visitor = new _SerializerIgnoreIcuExpVisitor(); const parts = message.nodes.map(a => a.visit(visitor, null)); return computeMsgId(parts.join(''), message.meaning); diff --git a/packages/compiler/src/i18n/i18n_ast.ts b/packages/compiler/src/i18n/i18n_ast.ts index 03663ab030..395e2e8510 100644 --- a/packages/compiler/src/i18n/i18n_ast.ts +++ b/packages/compiler/src/i18n/i18n_ast.ts @@ -11,6 +11,8 @@ import {ParseSourceSpan} from '../parse_util'; export class Message { sources: MessageSpan[]; id: string = this.customId; + /** The id to use if there is no custom id and if `i18nLegacyMessageIdFormat` is true */ + legacyId?: string = ''; /** * @param nodes message AST @@ -18,7 +20,7 @@ export class Message { * @param placeholderToMessage maps placeholder names to messages (used for nested ICU messages) * @param meaning * @param description - * @param id + * @param customId */ constructor( public nodes: Node[], public placeholders: {[phName: string]: string}, diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts index 52c84a72ac..46a873082b 100644 --- a/packages/compiler/src/render3/view/i18n/meta.ts +++ b/packages/compiler/src/render3/view/i18n/meta.ts @@ -6,12 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {decimalDigest} from '../../../i18n/digest'; +import {computeDecimalDigest, computeDigest, decimalDigest} from '../../../i18n/digest'; import * as i18n from '../../../i18n/i18n_ast'; import {createI18nMessageFactory} from '../../../i18n/i18n_parser'; import * as html from '../../../ml_parser/ast'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../ml_parser/interpolation_config'; -import {ParseTreeResult} from '../../../ml_parser/parser'; import * as o from '../../../output/output_ast'; import {I18N_ATTR, I18N_ATTR_PREFIX, hasI18nAttrs, icuFromI18nMessage} from './util'; @@ -19,6 +18,7 @@ import {I18N_ATTR, I18N_ATTR_PREFIX, hasI18nAttrs, icuFromI18nMessage} from './u export type I18nMeta = { id?: string, customId?: string, + legacyId?: string, description?: string, meaning?: string }; @@ -51,6 +51,19 @@ export class I18nMetaVisitor implements html.Visitor { // generate (or restore) message id if not specified in template message.id = typeof meta !== 'string' && (meta as i18n.Message).id || decimalDigest(message); } + + if (this.i18nLegacyMessageIdFormat === 'xlf') { + message.legacyId = computeDigest(message); + } else if ( + this.i18nLegacyMessageIdFormat === 'xlf2' || this.i18nLegacyMessageIdFormat === 'xmb') { + message.legacyId = computeDecimalDigest(message); + } else if (typeof meta !== 'string') { + // This occurs if we are doing the 2nd pass after whitespace removal + // In that case we want to reuse the legacy message generated in the 1st pass + // See `parseTemplate()` in `packages/compiler/src/render3/view/template.ts` + message.legacyId = (meta as i18n.Message).legacyId; + } + return message; } @@ -130,6 +143,7 @@ export function metaFromI18nMessage(message: i18n.Message, id: string | null = n return { id: typeof id === 'string' ? id : message.id || '', customId: message.customId, + legacyId: message.legacyId, meaning: message.meaning || '', description: message.description || '' }; @@ -180,8 +194,8 @@ export function serializeI18nHead(meta: I18nMeta, messagePart: string): string { if (meta.meaning) { metaBlock = `${meta.meaning}|${metaBlock}`; } - if (meta.customId) { - metaBlock = `${metaBlock}@@${meta.customId}`; + if (meta.customId || meta.legacyId) { + metaBlock = `${metaBlock}@@${meta.customId || meta.legacyId}`; } if (metaBlock === '') { // There is no metaBlock, so we must ensure that any starting colon is escaped. diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 59ef9a94b5..4036f7bc32 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -1905,6 +1905,19 @@ export interface ParseTemplateOptions { * included in source-map segments. A common example is whitespace. */ leadingTriviaChars?: string[]; + + /** + * Render `$localize` message ids with the specified legacy format (xlf, xlf2 or xmb). + * + * Use this option when use are using the `$localize` based localization messages but + * have not migrated the translation files to use the new `$localize` message id format. + * + * @deprecated + * `i18nLegacyMessageIdFormat` should only be used while migrating from legacy message id + * formatted translation files and will be removed at the same time as ViewEngine support is + * removed. + */ + i18nLegacyMessageIdFormat?: string; } /** @@ -1917,7 +1930,7 @@ export interface ParseTemplateOptions { export function parseTemplate( template: string, templateUrl: string, options: ParseTemplateOptions = {}): {errors?: ParseError[], nodes: t.Node[], styleUrls: string[], styles: string[]} { - const {interpolationConfig, preserveWhitespaces} = options; + const {interpolationConfig, preserveWhitespaces, i18nLegacyMessageIdFormat} = options; const bindingParser = makeBindingParser(interpolationConfig); const htmlParser = new HtmlParser(); const parseResult = htmlParser.parse( @@ -1934,8 +1947,9 @@ export function parseTemplate( // before we run whitespace removal process, because existing i18n // extraction process (ng xi18n) relies on a raw content to generate // message ids - rootNodes = - html.visitAll(new I18nMetaVisitor(interpolationConfig, !preserveWhitespaces), rootNodes); + rootNodes = html.visitAll( + new I18nMetaVisitor(interpolationConfig, !preserveWhitespaces, i18nLegacyMessageIdFormat), + rootNodes); if (!preserveWhitespaces) { rootNodes = html.visitAll(new WhitespaceVisitor(), rootNodes);