diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts
index f718565ce3..c0e06637f0 100644
--- a/packages/compiler/src/output/output_ast.ts
+++ b/packages/compiler/src/output/output_ast.ts
@@ -9,7 +9,6 @@
import {ParseSourceSpan} from '../parse_util';
import {I18nMeta} from '../render3/view/i18n/meta';
-import {error} from '../util';
//// Types
export enum TypeModifier {
@@ -516,11 +515,16 @@ export class LiteralExpr extends Expression {
}
}
+export abstract class MessagePiece {
+ constructor(public text: string, public sourceSpan: ParseSourceSpan) {}
+}
+export class LiteralPiece extends MessagePiece {}
+export class PlaceholderPiece extends MessagePiece {}
export class LocalizedString extends Expression {
constructor(
- readonly metaBlock: I18nMeta, readonly messageParts: string[],
- readonly placeHolderNames: string[], readonly expressions: Expression[],
+ readonly metaBlock: I18nMeta, readonly messageParts: LiteralPiece[],
+ readonly placeHolderNames: PlaceholderPiece[], readonly expressions: Expression[],
sourceSpan?: ParseSourceSpan|null) {
super(STRING_TYPE, sourceSpan);
}
@@ -563,7 +567,16 @@ export class LocalizedString extends Expression {
metaBlock = `${metaBlock}${LEGACY_ID_INDICATOR}${legacyId}`;
});
}
- return createCookedRawString(metaBlock, this.messageParts[0]);
+ return createCookedRawString(metaBlock, this.messageParts[0].text);
+ }
+
+ getMessagePartSourceSpan(i: number): ParseSourceSpan|null {
+ return this.messageParts[i]?.sourceSpan ?? this.sourceSpan;
+ }
+
+ getPlaceholderSourceSpan(i: number): ParseSourceSpan {
+ return this.placeHolderNames[i]?.sourceSpan ?? this.expressions[i]?.sourceSpan ??
+ this.sourceSpan;
}
/**
@@ -574,9 +587,9 @@ export class LocalizedString extends Expression {
* @param messagePart The following message string after this placeholder
*/
serializeI18nTemplatePart(partIndex: number): {cooked: string, raw: string} {
- const placeholderName = this.placeHolderNames[partIndex - 1];
+ const placeholderName = this.placeHolderNames[partIndex - 1].text;
const messagePart = this.messageParts[partIndex];
- return createCookedRawString(placeholderName, messagePart);
+ return createCookedRawString(placeholderName, messagePart.text);
}
}
@@ -1799,7 +1812,7 @@ export function literal(
}
export function localizedString(
- metaBlock: I18nMeta, messageParts: string[], placeholderNames: string[],
+ metaBlock: I18nMeta, messageParts: LiteralPiece[], placeholderNames: PlaceholderPiece[],
expressions: Expression[], sourceSpan?: ParseSourceSpan|null): LocalizedString {
return new LocalizedString(metaBlock, messageParts, placeholderNames, expressions, sourceSpan);
}
diff --git a/packages/compiler/src/render3/view/i18n/localize_utils.ts b/packages/compiler/src/render3/view/i18n/localize_utils.ts
index 127a3f6d94..4c4f29eff2 100644
--- a/packages/compiler/src/render3/view/i18n/localize_utils.ts
+++ b/packages/compiler/src/render3/view/i18n/localize_utils.ts
@@ -7,7 +7,7 @@
*/
import * as i18n from '../../../i18n/i18n_ast';
import * as o from '../../../output/output_ast';
-import {ParseSourceSpan} from '../../../parse_util';
+import {ParseLocation, ParseSourceSpan} from '../../../parse_util';
import {serializeIcuNode} from './icu_serializer';
import {formatI18nPlaceholderName} from './util';
@@ -17,60 +17,55 @@ export function createLocalizeStatements(
params: {[name: string]: o.Expression}): o.Statement[] {
const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message);
const sourceSpan = getSourceSpan(message);
- const expressions = placeHolders.map(ph => params[ph]);
+ const expressions = placeHolders.map(ph => params[ph.text]);
const localizedString =
o.localizedString(message, messageParts, placeHolders, expressions, sourceSpan);
const variableInitialization = variable.set(localizedString);
return [new o.ExpressionStatement(variableInitialization)];
}
-class MessagePiece {
- constructor(public text: string) {}
-}
-class LiteralPiece extends MessagePiece {}
-class PlaceholderPiece extends MessagePiece {
- constructor(name: string) {
- super(formatI18nPlaceholderName(name, /* useCamelCase */ false));
- }
-}
-
/**
* This visitor walks over an i18n tree, capturing literal strings and placeholders.
*
* The result can be used for generating the `$localize` tagged template literals.
*/
class LocalizeSerializerVisitor implements i18n.Visitor {
- visitText(text: i18n.Text, context: MessagePiece[]): any {
- if (context[context.length - 1] instanceof LiteralPiece) {
+ visitText(text: i18n.Text, context: o.MessagePiece[]): any {
+ if (context[context.length - 1] instanceof o.LiteralPiece) {
// Two literal pieces in a row means that there was some comment node in-between.
context[context.length - 1].text += text.value;
} else {
- context.push(new LiteralPiece(text.value));
+ context.push(new o.LiteralPiece(text.value, text.sourceSpan));
}
}
- visitContainer(container: i18n.Container, context: MessagePiece[]): any {
+ visitContainer(container: i18n.Container, context: o.MessagePiece[]): any {
container.children.forEach(child => child.visit(this, context));
}
- visitIcu(icu: i18n.Icu, context: MessagePiece[]): any {
- context.push(new LiteralPiece(serializeIcuNode(icu)));
+ visitIcu(icu: i18n.Icu, context: o.MessagePiece[]): any {
+ context.push(new o.LiteralPiece(serializeIcuNode(icu), icu.sourceSpan));
}
- visitTagPlaceholder(ph: i18n.TagPlaceholder, context: MessagePiece[]): any {
- context.push(new PlaceholderPiece(ph.startName));
+ visitTagPlaceholder(ph: i18n.TagPlaceholder, context: o.MessagePiece[]): any {
+ context.push(this.createPlaceholderPiece(ph.startName, ph.sourceSpan));
if (!ph.isVoid) {
ph.children.forEach(child => child.visit(this, context));
- context.push(new PlaceholderPiece(ph.closeName));
+ context.push(this.createPlaceholderPiece(ph.closeName, ph.closeSourceSpan ?? ph.sourceSpan));
}
}
- visitPlaceholder(ph: i18n.Placeholder, context: MessagePiece[]): any {
- context.push(new PlaceholderPiece(ph.name));
+ visitPlaceholder(ph: i18n.Placeholder, context: o.MessagePiece[]): any {
+ context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
- context.push(new PlaceholderPiece(ph.name));
+ context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
+ }
+
+ private createPlaceholderPiece(name: string, sourceSpan: ParseSourceSpan): o.PlaceholderPiece {
+ return new o.PlaceholderPiece(
+ formatI18nPlaceholderName(name, /* useCamelCase */ false), sourceSpan);
}
}
@@ -85,8 +80,8 @@ const serializerVisitor = new LocalizeSerializerVisitor();
* @returns an object containing the messageParts and placeholders.
*/
export function serializeI18nMessageForLocalize(message: i18n.Message):
- {messageParts: string[], placeHolders: string[]} {
- const pieces: MessagePiece[] = [];
+ {messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
+ const pieces: o.MessagePiece[] = [];
message.nodes.forEach(node => node.visit(serializerVisitor, pieces));
return processMessagePieces(pieces);
}
@@ -107,31 +102,35 @@ function getSourceSpan(message: i18n.Message): ParseSourceSpan {
* @param pieces The pieces to process.
* @returns an object containing the messageParts and placeholders.
*/
-function processMessagePieces(pieces: MessagePiece[]):
- {messageParts: string[], placeHolders: string[]} {
- const messageParts: string[] = [];
- const placeHolders: string[] = [];
+function processMessagePieces(pieces: o.MessagePiece[]):
+ {messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
+ const messageParts: o.LiteralPiece[] = [];
+ const placeHolders: o.PlaceholderPiece[] = [];
- if (pieces[0] instanceof PlaceholderPiece) {
+ if (pieces[0] instanceof o.PlaceholderPiece) {
// The first piece was a placeholder so we need to add an initial empty message part.
- messageParts.push('');
+ messageParts.push(createEmptyMessagePart(pieces[0].sourceSpan.start));
}
for (let i = 0; i < pieces.length; i++) {
const part = pieces[i];
- if (part instanceof LiteralPiece) {
- messageParts.push(part.text);
+ if (part instanceof o.LiteralPiece) {
+ messageParts.push(part);
} else {
- placeHolders.push(part.text);
- if (pieces[i - 1] instanceof PlaceholderPiece) {
+ placeHolders.push(part);
+ if (pieces[i - 1] instanceof o.PlaceholderPiece) {
// There were two placeholders in a row, so we need to add an empty message part.
- messageParts.push('');
+ messageParts.push(createEmptyMessagePart(part.sourceSpan.end));
}
}
}
- if (pieces[pieces.length - 1] instanceof PlaceholderPiece) {
+ if (pieces[pieces.length - 1] instanceof o.PlaceholderPiece) {
// The last piece was a placeholder so we need to add a final empty message part.
- messageParts.push('');
+ messageParts.push(createEmptyMessagePart(pieces[pieces.length - 1].sourceSpan.end));
}
return {messageParts, placeHolders};
}
+
+function createEmptyMessagePart(location: ParseLocation): o.LiteralPiece {
+ return new o.LiteralPiece('', new ParseSourceSpan(location, location));
+}
diff --git a/packages/compiler/test/output/js_emitter_spec.ts b/packages/compiler/test/output/js_emitter_spec.ts
index c21297c4ee..b6ff7deab5 100644
--- a/packages/compiler/test/output/js_emitter_spec.ts
+++ b/packages/compiler/test/output/js_emitter_spec.ts
@@ -205,9 +205,12 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
});
it('should support ES5 localized strings', () => {
- expect(emitStmt(new o.ExpressionStatement(o.localizedString(
- {}, ['ab\\:c', 'd"e\'f'], ['ph1'],
- [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))]))))
+ const messageParts =
+ [new o.LiteralPiece('ab\\:c', {} as any), new o.LiteralPiece('d"e\'f', {} as any)];
+ const placeholders = [new o.PlaceholderPiece('ph1', {} as any)];
+ const expressions = [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))];
+ const localizedString = o.localizedString({}, messageParts, placeholders, expressions);
+ expect(emitStmt(new o.ExpressionStatement(localizedString)))
.toEqual(
String.raw
`$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})(['ab\\:c', ':ph1:d"e\'f'], ['ab\\\\:c', ':ph1:d"e\'f']), (7 + 8));`);
diff --git a/packages/compiler/test/output/ts_emitter_spec.ts b/packages/compiler/test/output/ts_emitter_spec.ts
index 78da4b697f..e20b136f38 100644
--- a/packages/compiler/test/output/ts_emitter_spec.ts
+++ b/packages/compiler/test/output/ts_emitter_spec.ts
@@ -254,9 +254,12 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
});
it('should support localized strings', () => {
- expect(emitStmt(new o.ExpressionStatement(o.localizedString(
- {}, ['ab\\:c', 'd"e\'f'], ['ph1'],
- [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))]))))
+ const messageParts =
+ [new o.LiteralPiece('ab\\:c', {} as any), new o.LiteralPiece('d"e\'f', {} as any)];
+ const placeholders = [new o.PlaceholderPiece('ph1', {} as any)];
+ const expressions = [o.literal(7, o.NUMBER_TYPE).plus(o.literal(8, o.NUMBER_TYPE))];
+ const localizedString = o.localizedString({}, messageParts, placeholders, expressions);
+ expect(emitStmt(new o.ExpressionStatement(localizedString)))
.toEqual('$localize `ab\\\\:c${(7 + 8)}:ph1:d"e\'f`;');
});
diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts
index 328cb7b7f3..50d534c64e 100644
--- a/packages/compiler/test/render3/view/i18n_spec.ts
+++ b/packages/compiler/test/render3/view/i18n_spec.ts
@@ -10,6 +10,7 @@ import {Lexer} from '../../../src/expression_parser/lexer';
import {Parser} from '../../../src/expression_parser/parser';
import * as i18n from '../../../src/i18n/i18n_ast';
import * as o from '../../../src/output/output_ast';
+import {ParseSourceSpan} from '../../../src/parse_util';
import * as t from '../../../src/render3/r3_ast';
import {I18nContext} from '../../../src/render3/view/i18n/context';
import {serializeI18nMessageForGetMsg} from '../../../src/render3/view/i18n/get_msg_utils';
@@ -221,64 +222,75 @@ describe('Utils', () => {
});
it('serializeI18nHead()', () => {
- expect(o.localizedString(meta(), [''], [], []).serializeI18nHead())
+ expect(o.localizedString(meta(), [literal('')], [], []).serializeI18nHead())
.toEqual({cooked: '', raw: ''});
- expect(o.localizedString(meta('', '', 'desc'), [''], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('', '', 'desc'), [literal('')], [], []).serializeI18nHead())
.toEqual({cooked: ':desc:', raw: ':desc:'});
- expect(o.localizedString(meta('id', '', 'desc'), [''], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('id', '', 'desc'), [literal('')], [], []).serializeI18nHead())
.toEqual({cooked: ':desc@@id:', raw: ':desc@@id:'});
- expect(o.localizedString(meta('', 'meaning', 'desc'), [''], [], []).serializeI18nHead())
+ expect(
+ o.localizedString(meta('', 'meaning', 'desc'), [literal('')], [], []).serializeI18nHead())
.toEqual({cooked: ':meaning|desc:', raw: ':meaning|desc:'});
- expect(o.localizedString(meta('id', 'meaning', 'desc'), [''], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('id', 'meaning', 'desc'), [literal('')], [], [])
+ .serializeI18nHead())
.toEqual({cooked: ':meaning|desc@@id:', raw: ':meaning|desc@@id:'});
- expect(o.localizedString(meta('id', '', ''), [''], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('id', '', ''), [literal('')], [], []).serializeI18nHead())
.toEqual({cooked: ':@@id:', raw: ':@@id:'});
// Escaping colons (block markers)
- expect(
- o.localizedString(meta('id:sub_id', 'meaning', 'desc'), [''], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('id:sub_id', 'meaning', 'desc'), [literal('')], [], [])
+ .serializeI18nHead())
.toEqual({cooked: ':meaning|desc@@id:sub_id:', raw: ':meaning|desc@@id\\:sub_id:'});
- expect(o.localizedString(meta('id', 'meaning:sub_meaning', 'desc'), [''], [], [])
+ expect(o.localizedString(meta('id', 'meaning:sub_meaning', 'desc'), [literal('')], [], [])
.serializeI18nHead())
.toEqual(
{cooked: ':meaning:sub_meaning|desc@@id:', raw: ':meaning\\:sub_meaning|desc@@id:'});
- expect(o.localizedString(meta('id', 'meaning', 'desc:sub_desc'), [''], [], [])
+ expect(o.localizedString(meta('id', 'meaning', 'desc:sub_desc'), [literal('')], [], [])
.serializeI18nHead())
.toEqual({cooked: ':meaning|desc:sub_desc@@id:', raw: ':meaning|desc\\:sub_desc@@id:'});
- expect(o.localizedString(meta('id', 'meaning', 'desc'), ['message source'], [], [])
+ expect(o.localizedString(meta('id', 'meaning', 'desc'), [literal('message source')], [], [])
.serializeI18nHead())
.toEqual({
cooked: ':meaning|desc@@id:message source',
raw: ':meaning|desc@@id:message source'
});
- expect(o.localizedString(meta('id', 'meaning', 'desc'), [':message source'], [], [])
+ expect(o.localizedString(meta('id', 'meaning', 'desc'), [literal(':message source')], [], [])
.serializeI18nHead())
.toEqual({
cooked: ':meaning|desc@@id::message source',
raw: ':meaning|desc@@id::message source'
});
- expect(o.localizedString(meta('', '', ''), ['message source'], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('', '', ''), [literal('message source')], [], [])
+ .serializeI18nHead())
.toEqual({cooked: 'message source', raw: 'message source'});
- expect(o.localizedString(meta('', '', ''), [':message source'], [], []).serializeI18nHead())
+ expect(o.localizedString(meta('', '', ''), [literal(':message source')], [], [])
+ .serializeI18nHead())
.toEqual({cooked: ':message source', raw: '\\:message source'});
});
it('serializeI18nPlaceholderBlock()', () => {
- expect(o.localizedString(meta('', '', ''), ['', ''], [''], []).serializeI18nTemplatePart(1))
- .toEqual({cooked: '', raw: ''});
- expect(
- o.localizedString(meta('', '', ''), ['', ''], ['abc'], []).serializeI18nTemplatePart(1))
- .toEqual({cooked: ':abc:', raw: ':abc:'});
- expect(o.localizedString(meta('', '', ''), ['', 'message'], [''], [])
+ expect(o.localizedString(meta('', '', ''), [literal(''), literal('')], [literal('')], [])
.serializeI18nTemplatePart(1))
+ .toEqual({cooked: '', raw: ''});
+ expect(o.localizedString(
+ meta('', '', ''), [literal(''), literal('')],
+ [new o.LiteralPiece('abc', {} as any)], [])
+ .serializeI18nTemplatePart(1))
+ .toEqual({cooked: ':abc:', raw: ':abc:'});
+ expect(
+ o.localizedString(meta('', '', ''), [literal(''), literal('message')], [literal('')], [])
+ .serializeI18nTemplatePart(1))
.toEqual({cooked: 'message', raw: 'message'});
- expect(o.localizedString(meta('', '', ''), ['', 'message'], ['abc'], [])
+ expect(o.localizedString(
+ meta('', '', ''), [literal(''), literal('message')], [literal('abc')], [])
.serializeI18nTemplatePart(1))
.toEqual({cooked: ':abc:message', raw: ':abc:message'});
- expect(o.localizedString(meta('', '', ''), ['', ':message'], [''], [])
- .serializeI18nTemplatePart(1))
+ expect(
+ o.localizedString(meta('', '', ''), [literal(''), literal(':message')], [literal('')], [])
+ .serializeI18nTemplatePart(1))
.toEqual({cooked: ':message', raw: '\\:message'});
- expect(o.localizedString(meta('', '', ''), ['', ':message'], ['abc'], [])
+ expect(o.localizedString(
+ meta('', '', ''), [literal(''), literal(':message')], [literal('abc')], [])
.serializeI18nTemplatePart(1))
.toEqual({cooked: ':abc::message', raw: ':abc::message'});
});
@@ -349,55 +361,106 @@ describe('serializeI18nMessageForLocalize', () => {
};
it('should serialize plain text for `$localize()`', () => {
- expect(serialize('Some text')).toEqual({messageParts: ['Some text'], placeHolders: []});
+ expect(serialize('Some text'))
+ .toEqual({messageParts: [literal('Some text')], placeHolders: []});
});
it('should serialize text with interpolation for `$localize()`', () => {
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }} done')).toEqual({
- messageParts: ['Some text ', ' and ', ' done'],
- placeHolders: ['INTERPOLATION', 'INTERPOLATION_1']
+ messageParts: [literal('Some text '), literal(' and '), literal(' done')],
+ placeHolders: [placeholder('INTERPOLATION'), placeholder('INTERPOLATION_1')],
});
});
+ it('should compute source-spans when serializing text with interpolation for `$localize()`',
+ () => {
+ const {messageParts, placeHolders} =
+ serialize('Some text {{ valueA }} and {{ valueB + valueC }} done');
+
+ expect(messageParts[0].text).toEqual('Some text ');
+ expect(messageParts[0].sourceSpan.toString()).toEqual('Some text ');
+ expect(messageParts[1].text).toEqual(' and ');
+ expect(messageParts[1].sourceSpan.toString()).toEqual(' and ');
+ expect(messageParts[2].text).toEqual(' done');
+ expect(messageParts[2].sourceSpan.toString()).toEqual(' done');
+
+ expect(placeHolders[0].text).toEqual('INTERPOLATION');
+ expect(placeHolders[0].sourceSpan.toString()).toEqual('{{ valueA }}');
+ expect(placeHolders[1].text).toEqual('INTERPOLATION_1');
+ expect(placeHolders[1].sourceSpan.toString()).toEqual('{{ valueB + valueC }}');
+ });
+
it('should serialize text with interpolation at start for `$localize()`', () => {
expect(serialize('{{ valueA }} and {{ valueB + valueC }} done')).toEqual({
- messageParts: ['', ' and ', ' done'],
- placeHolders: ['INTERPOLATION', 'INTERPOLATION_1']
+ messageParts: [literal(''), literal(' and '), literal(' done')],
+ placeHolders: [placeholder('INTERPOLATION'), placeholder('INTERPOLATION_1')],
});
});
it('should serialize text with interpolation at end for `$localize()`', () => {
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')).toEqual({
- messageParts: ['Some text ', ' and ', ''],
- placeHolders: ['INTERPOLATION', 'INTERPOLATION_1']
+ messageParts: [literal('Some text '), literal(' and '), literal('')],
+ placeHolders: [placeholder('INTERPOLATION'), placeholder('INTERPOLATION_1')],
});
});
it('should serialize only interpolation for `$localize()`', () => {
- expect(serialize('{{ valueB + valueC }}'))
- .toEqual({messageParts: ['', ''], placeHolders: ['INTERPOLATION']});
+ expect(serialize('{{ valueB + valueC }}')).toEqual({
+ messageParts: [literal(''), literal('')],
+ placeHolders: [placeholder('INTERPOLATION')]
+ });
});
it('should serialize interpolation with named placeholder for `$localize()`', () => {
- expect(serialize('{{ valueB + valueC // i18n(ph="PLACEHOLDER NAME") }}'))
- .toEqual({messageParts: ['', ''], placeHolders: ['PLACEHOLDER_NAME']});
+ expect(serialize('{{ valueB + valueC // i18n(ph="PLACEHOLDER NAME") }}')).toEqual({
+ messageParts: [literal(''), literal('')],
+ placeHolders: [placeholder('PLACEHOLDER_NAME')]
+ });
});
it('should serialize content with HTML tags for `$localize()`', () => {
expect(serialize('A B