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);