diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
index 105731277c..738b965499 100644
--- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
@@ -2179,6 +2179,23 @@ runInEachFileSystem(os => {
expect(jsContents).not.toContain(':Some text');
});
+ it('should also render legacy id for ICUs when normal messages are using legacy ids', () => {
+ env.tsconfig({i18nInFormat: 'xliff'});
+ env.write(`test.ts`, `
+ import {Component} from '@angular/core';
+ @Component({
+ selector: 'test',
+ template: '
Some text {age, plural, 10 {ten} other {other}}
'
+ })
+ class FooCmp {}`);
+ env.driveMain();
+ const jsContents = env.getContents('test.js');
+ expect(jsContents)
+ .toContain(
+ ':@@720ba589d043a0497ac721ff972f41db0c919efb:{VAR_PLURAL, plural, 10 {ten} other {other}}');
+ 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/i18n_ast.ts b/packages/compiler/src/i18n/i18n_ast.ts
index 6e41f09a79..0476214f41 100644
--- a/packages/compiler/src/i18n/i18n_ast.ts
+++ b/packages/compiler/src/i18n/i18n_ast.ts
@@ -93,6 +93,8 @@ export class Placeholder implements Node {
}
export class IcuPlaceholder implements Node {
+ /** Used to capture a message computed from a previous processing pass (see `setI18nRefs()`). */
+ previousMessage?: Message;
constructor(public value: Icu, public name: string, public sourceSpan: ParseSourceSpan) {}
visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); }
diff --git a/packages/compiler/src/i18n/i18n_parser.ts b/packages/compiler/src/i18n/i18n_parser.ts
index 92da672346..f1be0895c8 100644
--- a/packages/compiler/src/i18n/i18n_parser.ts
+++ b/packages/compiler/src/i18n/i18n_parser.ts
@@ -21,8 +21,8 @@ const _expParser = new ExpressionParser(new ExpressionLexer());
export type VisitNodeFn = (html: html.Node, i18n: i18n.Node) => i18n.Node;
export interface I18nMessageFactory {
- (nodes: html.Node[], meaning: string, description: string, customId: string,
- visitNodeFn?: VisitNodeFn): i18n.Message;
+ (nodes: html.Node[], meaning: string|undefined, description: string|undefined,
+ customId: string|undefined, visitNodeFn?: VisitNodeFn): i18n.Message;
}
/**
@@ -54,7 +54,7 @@ class _I18nVisitor implements html.Visitor {
private _interpolationConfig: InterpolationConfig) {}
public toI18nMessage(
- nodes: html.Node[], meaning: string, description: string, customId: string,
+ nodes: html.Node[], meaning = '', description = '', customId = '',
visitNodeFn: VisitNodeFn|undefined): i18n.Message {
const context: I18nMessageVisitorContext = {
isIcu: nodes.length == 1 && nodes[0] instanceof html.Expansion,
diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts
index 6e6b67cb0c..3fed00ce11 100644
--- a/packages/compiler/src/render3/view/i18n/meta.ts
+++ b/packages/compiler/src/render3/view/i18n/meta.ts
@@ -23,10 +23,20 @@ export type I18nMeta = {
meaning?: string
};
-function setI18nRefs(html: html.Node & {i18n?: i18n.I18nMeta}, i18n: i18n.Node): i18n.Node {
- html.i18n = i18n;
- return i18n;
-}
+
+const setI18nRefs: VisitNodeFn = (htmlNode, i18nNode) => {
+ if (htmlNode instanceof html.NodeWithI18n) {
+ if (i18nNode instanceof i18n.IcuPlaceholder && htmlNode.i18n instanceof i18n.Message) {
+ // This html node represents an ICU but this is a second processing pass, and the legacy id
+ // was computed in the previous pass and stored in the `i18n` property as a message.
+ // We are about to wipe out that property so capture the previous message to be reused when
+ // generating the message for this ICU later. See `_generateI18nMessage()`.
+ i18nNode.previousMessage = htmlNode.i18n;
+ }
+ htmlNode.i18n = i18nNode;
+ }
+ return i18nNode;
+};
/**
* This visitor walks over HTML parse tree and converts information stored in
@@ -44,28 +54,10 @@ export class I18nMetaVisitor implements html.Visitor {
private _generateI18nMessage(
nodes: html.Node[], meta: string|i18n.I18nMeta = '',
visitNodeFn?: VisitNodeFn): i18n.Message {
- const parsed: I18nMeta =
- typeof meta === 'string' ? parseI18nMeta(meta) : metaFromI18nMessage(meta as i18n.Message);
- const message = this._createI18nMessage(
- nodes, parsed.meaning || '', parsed.description || '', parsed.customId || '', visitNodeFn);
- if (!message.id) {
- // 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' || this.i18nLegacyMessageIdFormat === 'xliff') {
- message.legacyId = computeDigest(message);
- } else if (
- this.i18nLegacyMessageIdFormat === 'xlf2' || this.i18nLegacyMessageIdFormat === 'xliff2' ||
- 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;
- }
-
+ const {meaning, description, customId} = this._parseMetadata(meta);
+ const message = this._createI18nMessage(nodes, meaning, description, customId, visitNodeFn);
+ this._setMessageId(message, meta);
+ this._setLegacyId(message, meta);
return message;
}
@@ -141,6 +133,57 @@ export class I18nMetaVisitor implements html.Visitor {
visitAttribute(attribute: html.Attribute): any { return attribute; }
visitComment(comment: html.Comment): any { return comment; }
visitExpansionCase(expansionCase: html.ExpansionCase): any { return expansionCase; }
+
+ /**
+ * Parse the general form `meta` passed into extract the explicit metadata needed to create a
+ * `Message`.
+ *
+ * There are three possibilities for the `meta` variable
+ * 1) a string from an `i18n` template attribute: parse it to extract the metadata values.
+ * 2) a `Message` from a previous processing pass: reuse the metadata values in the message.
+ * 4) other: ignore this and just process the message metadata as normal
+ *
+ * @param meta the bucket that holds information about the message
+ * @returns the parsed metadata.
+ */
+ private _parseMetadata(meta: string|i18n.I18nMeta): I18nMeta {
+ return typeof meta === 'string' ? parseI18nMeta(meta) :
+ meta instanceof i18n.Message ? metaFromI18nMessage(meta) : {};
+ }
+
+ /**
+ * Generate (or restore) message id if not specified already.
+ */
+ private _setMessageId(message: i18n.Message, meta: string|i18n.I18nMeta): void {
+ if (!message.id) {
+ message.id = meta instanceof i18n.Message && meta.id || decimalDigest(message);
+ }
+ }
+
+ /**
+ * Update the `message` with a `legacyId` if necessary.
+ *
+ * @param message the message whose legacy id should be set
+ * @param meta information about the message being processed
+ */
+ private _setLegacyId(message: i18n.Message, meta: string|i18n.I18nMeta): void {
+ if (this.i18nLegacyMessageIdFormat === 'xlf' || this.i18nLegacyMessageIdFormat === 'xliff') {
+ message.legacyId = computeDigest(message);
+ } else if (
+ this.i18nLegacyMessageIdFormat === 'xlf2' || this.i18nLegacyMessageIdFormat === 'xliff2' ||
+ this.i18nLegacyMessageIdFormat === 'xmb') {
+ message.legacyId = computeDecimalDigest(message);
+ } else if (typeof meta !== 'string') {
+ // This occurs if we are doing the 2nd pass after whitespace removal (see `parseTemplate()` in
+ // `packages/compiler/src/render3/view/template.ts`).
+ // In that case we want to reuse the legacy message generated in the 1st pass (see
+ // `setI18nRefs()`).
+ const previousMessage = meta instanceof i18n.Message ?
+ meta :
+ meta instanceof i18n.IcuPlaceholder ? meta.previousMessage : undefined;
+ message.legacyId = previousMessage && previousMessage.legacyId;
+ }
+ }
}
export function metaFromI18nMessage(message: i18n.Message, id: string | null = null): I18nMeta {
diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts
index c69ad86693..b9b77c1ca3 100644
--- a/packages/compiler/test/render3/view/i18n_spec.ts
+++ b/packages/compiler/test/render3/view/i18n_spec.ts
@@ -21,7 +21,7 @@ import {formatI18nPlaceholderName} from '../../../src/render3/view/i18n/util';
import {parseR3 as parse} from './util';
const expressionParser = new Parser(new Lexer());
-const i18nOf = (element: t.Node & {i18n?: i18n.AST}) => element.i18n !;
+const i18nOf = (element: t.Node & {i18n?: i18n.I18nMeta}) => element.i18n !;
describe('I18nContext', () => {
it('should support i18n content collection', () => {