diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
index 195cae9f67009..e0a43348bfae8 100644
--- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
+++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
+import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript';
@@ -212,7 +212,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
visitReadVarExpr(ast: ReadVarExpr, context: Context): ts.Identifier {
const identifier = ts.createIdentifier(ast.name!);
- this.setSourceMapRange(identifier, ast);
+ this.setSourceMapRange(identifier, ast.sourceSpan);
return identifier;
}
@@ -244,7 +244,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
const call = ts.createCall(
ast.name !== null ? ts.createPropertyAccess(target, ast.name) : target, undefined,
ast.args.map(arg => arg.visitExpression(this, context)));
- this.setSourceMapRange(call, ast);
+ this.setSourceMapRange(call, ast.sourceSpan);
return call;
}
@@ -255,7 +255,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
if (ast.pure) {
ts.addSyntheticLeadingComment(expr, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false);
}
- this.setSourceMapRange(expr, ast);
+ this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
}
@@ -274,15 +274,15 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
} else {
expr = ts.createLiteral(ast.value);
}
- this.setSourceMapRange(expr, ast);
+ this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
}
visitLocalizedString(ast: LocalizedString, context: Context): ts.Expression {
const localizedString = this.scriptTarget >= ts.ScriptTarget.ES2015 ?
- createLocalizedStringTaggedTemplate(ast, context, this) :
- createLocalizedStringFunctionCall(ast, context, this, this.imports);
- this.setSourceMapRange(localizedString, ast);
+ this.createLocalizedStringTaggedTemplate(ast, context) :
+ this.createLocalizedStringFunctionCall(ast, context);
+ this.setSourceMapRange(localizedString, ast.sourceSpan);
return localizedString;
}
@@ -395,7 +395,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: Context): ts.ArrayLiteralExpression {
const expr =
ts.createArrayLiteral(ast.entries.map(expr => expr.visitExpression(this, context)));
- this.setSourceMapRange(expr, ast);
+ this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
}
@@ -405,7 +405,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
entry.quoted ? ts.createLiteral(entry.key) : ts.createIdentifier(entry.key),
entry.value.visitExpression(this, context)));
const expr = ts.createObjectLiteral(entries);
- this.setSourceMapRange(expr, ast);
+ this.setSourceMapRange(expr, ast.sourceSpan);
return expr;
}
@@ -424,9 +424,114 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
return ts.createTypeOf(ast.expr.visitExpression(this, context));
}
- private setSourceMapRange(expr: ts.Expression, ast: Expression) {
- if (ast.sourceSpan) {
- const {start, end} = ast.sourceSpan;
+ /**
+ * Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted
+ * output.
+ */
+ private createLocalizedStringTaggedTemplate(ast: LocalizedString, context: Context):
+ ts.TaggedTemplateExpression {
+ let template: ts.TemplateLiteral;
+ const length = ast.messageParts.length;
+ const metaBlock = ast.serializeI18nHead();
+ if (length === 1) {
+ template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw);
+ this.setSourceMapRange(template, ast.getMessagePartSourceSpan(0));
+ } else {
+ // Create the head part
+ const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw);
+ this.setSourceMapRange(head, ast.getMessagePartSourceSpan(0));
+ const spans: ts.TemplateSpan[] = [];
+ // Create the middle parts
+ for (let i = 1; i < length - 1; i++) {
+ const resolvedExpression = ast.expressions[i - 1].visitExpression(this, context);
+ this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(i - 1));
+ const templatePart = ast.serializeI18nTemplatePart(i);
+ const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw);
+
+ // this.setSourceMapRange(resolvedExpression, ast.getMessagePartSourceSpan(i));
+ this.setSourceMapRange(templateMiddle, ast.getMessagePartSourceSpan(i));
+ const templateSpan = ts.createTemplateSpan(resolvedExpression, templateMiddle);
+ // this.setSourceMapRange(templateSpan, ast.getMessagePartSourceSpan(i));
+ spans.push(templateSpan);
+ }
+ // Create the tail part
+ const resolvedExpression = ast.expressions[length - 2].visitExpression(this, context);
+ this.setSourceMapRange(resolvedExpression, ast.getPlaceholderSourceSpan(length - 2));
+ const templatePart = ast.serializeI18nTemplatePart(length - 1);
+ const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw);
+ this.setSourceMapRange(templateTail, ast.getMessagePartSourceSpan(length - 1));
+ spans.push(ts.createTemplateSpan(resolvedExpression, templateTail));
+ // Put it all together
+ template = ts.createTemplateExpression(head, spans);
+ }
+ const expression = ts.createTaggedTemplate(ts.createIdentifier('$localize'), template);
+ this.setSourceMapRange(expression, ast.sourceSpan);
+ return expression;
+ }
+
+ /**
+ * Translate the `LocalizedString` node into a `$localize` call using the imported
+ * `__makeTemplateObject` helper for ES5 formatted output.
+ */
+ private createLocalizedStringFunctionCall(ast: LocalizedString, context: Context) {
+ // A `$localize` message consists `messageParts` and `expressions`, which get interleaved
+ // together. The interleaved pieces look like:
+ // `[messagePart0, expression0, messagePart1, expression1, messagePart2]`
+ //
+ // Note that there is always a message part at the start and end, and so therefore
+ // `messageParts.length === expressions.length + 1`.
+ //
+ // Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters.
+ // The metadata is attached to the first and subsequent message parts by calls to
+ // `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively.
+
+ // The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts`
+ // array.
+ const messageParts = [ast.serializeI18nHead()];
+ const expressions: any[] = [];
+
+ // The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed
+ // into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]`
+ for (let i = 1; i < ast.messageParts.length; i++) {
+ expressions.push(ast.expressions[i - 1].visitExpression(this, context));
+ messageParts.push(ast.serializeI18nTemplatePart(i));
+ }
+
+ // The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()`
+ // helper, so we must ensure it has been imported.
+ const {moduleImport, symbol} =
+ this.imports.generateNamedImport('tslib', '__makeTemplateObject');
+ const __makeTemplateObjectHelper = (moduleImport === null) ?
+ ts.createIdentifier(symbol) :
+ ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
+
+ // Generate the call in the form:
+ // `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);`
+ const cookedLiterals = messageParts.map(
+ (messagePart, i) =>
+ this.createLiteral(messagePart.cooked, ast.getMessagePartSourceSpan(i)));
+ const rawLiterals = messageParts.map(
+ (messagePart, i) => this.createLiteral(messagePart.raw, ast.getMessagePartSourceSpan(i)));
+ return ts.createCall(
+ /* expression */ ts.createIdentifier('$localize'),
+ /* typeArguments */ undefined,
+ /* argumentsArray */[
+ ts.createCall(
+ /* expression */ __makeTemplateObjectHelper,
+ /* typeArguments */ undefined,
+ /* argumentsArray */
+ [
+ ts.createArrayLiteral(cookedLiterals),
+ ts.createArrayLiteral(rawLiterals),
+ ]),
+ ...expressions,
+ ]);
+ }
+
+
+ private setSourceMapRange(expr: ts.Node, sourceSpan: ParseSourceSpan|null) {
+ if (sourceSpan) {
+ const {start, end} = sourceSpan;
const {url, content} = start.file;
if (url) {
if (!this.externalSourceFiles.has(url)) {
@@ -437,6 +542,12 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
}
}
}
+
+ private createLiteral(text: string, span: ParseSourceSpan|null) {
+ const literal = ts.createStringLiteral(text);
+ this.setSourceMapRange(literal, span);
+ return literal;
+ }
}
export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
@@ -662,40 +773,6 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
}
}
-/**
- * Translate the `LocalizedString` node into a `TaggedTemplateExpression` for ES2015 formatted
- * output.
- */
-function createLocalizedStringTaggedTemplate(
- ast: LocalizedString, context: Context, visitor: ExpressionVisitor) {
- let template: ts.TemplateLiteral;
- const length = ast.messageParts.length;
- const metaBlock = ast.serializeI18nHead();
- if (length === 1) {
- template = ts.createNoSubstitutionTemplateLiteral(metaBlock.cooked, metaBlock.raw);
- } else {
- // Create the head part
- const head = ts.createTemplateHead(metaBlock.cooked, metaBlock.raw);
- const spans: ts.TemplateSpan[] = [];
- // Create the middle parts
- for (let i = 1; i < length - 1; i++) {
- const resolvedExpression = ast.expressions[i - 1].visitExpression(visitor, context);
- const templatePart = ast.serializeI18nTemplatePart(i);
- const templateMiddle = createTemplateMiddle(templatePart.cooked, templatePart.raw);
- spans.push(ts.createTemplateSpan(resolvedExpression, templateMiddle));
- }
- // Create the tail part
- const resolvedExpression = ast.expressions[length - 2].visitExpression(visitor, context);
- const templatePart = ast.serializeI18nTemplatePart(length - 1);
- const templateTail = createTemplateTail(templatePart.cooked, templatePart.raw);
- spans.push(ts.createTemplateSpan(resolvedExpression, templateTail));
- // Put it all together
- template = ts.createTemplateExpression(head, spans);
- }
- return ts.createTaggedTemplate(ts.createIdentifier('$localize'), template);
-}
-
-
// HACK: Use this in place of `ts.createTemplateMiddle()`.
// Revert once https://github.com/microsoft/TypeScript/issues/35374 is fixed
function createTemplateMiddle(cooked: string, raw: string): ts.TemplateMiddle {
@@ -710,59 +787,4 @@ function createTemplateTail(cooked: string, raw: string): ts.TemplateTail {
const node: ts.TemplateLiteralLikeNode = ts.createTemplateHead(cooked, raw);
(node.kind as ts.SyntaxKind) = ts.SyntaxKind.TemplateTail;
return node as ts.TemplateTail;
-}
-
-/**
- * Translate the `LocalizedString` node into a `$localize` call using the imported
- * `__makeTemplateObject` helper for ES5 formatted output.
- */
-function createLocalizedStringFunctionCall(
- ast: LocalizedString, context: Context, visitor: ExpressionVisitor, imports: ImportManager) {
- // A `$localize` message consists `messageParts` and `expressions`, which get interleaved
- // together. The interleaved pieces look like:
- // `[messagePart0, expression0, messagePart1, expression1, messagePart2]`
- //
- // Note that there is always a message part at the start and end, and so therefore
- // `messageParts.length === expressions.length + 1`.
- //
- // Each message part may be prefixed with "metadata", which is wrapped in colons (:) delimiters.
- // The metadata is attached to the first and subsequent message parts by calls to
- // `serializeI18nHead()` and `serializeI18nTemplatePart()` respectively.
-
- // The first message part (i.e. `ast.messageParts[0]`) is used to initialize `messageParts` array.
- const messageParts = [ast.serializeI18nHead()];
- const expressions: any[] = [];
-
- // The rest of the `ast.messageParts` and each of the expressions are `ast.expressions` pushed
- // into the arrays. Note that `ast.messagePart[i]` corresponds to `expressions[i-1]`
- for (let i = 1; i < ast.messageParts.length; i++) {
- expressions.push(ast.expressions[i - 1].visitExpression(visitor, context));
- messageParts.push(ast.serializeI18nTemplatePart(i));
- }
-
- // The resulting downlevelled tagged template string uses a call to the `__makeTemplateObject()`
- // helper, so we must ensure it has been imported.
- const {moduleImport, symbol} = imports.generateNamedImport('tslib', '__makeTemplateObject');
- const __makeTemplateObjectHelper = (moduleImport === null) ?
- ts.createIdentifier(symbol) :
- ts.createPropertyAccess(ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
-
- // Generate the call in the form:
- // `$localize(__makeTemplateObject(cookedMessageParts, rawMessageParts), ...expressions);`
- return ts.createCall(
- /* expression */ ts.createIdentifier('$localize'),
- /* typeArguments */ undefined,
- /* argumentsArray */[
- ts.createCall(
- /* expression */ __makeTemplateObjectHelper,
- /* typeArguments */ undefined,
- /* argumentsArray */
- [
- ts.createArrayLiteral(
- messageParts.map(messagePart => ts.createStringLiteral(messagePart.cooked))),
- ts.createArrayLiteral(
- messageParts.map(messagePart => ts.createStringLiteral(messagePart.raw))),
- ]),
- ...expressions,
- ]);
-}
+}
\ No newline at end of file
diff --git a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
index c6865f8efac6f..51611dfc75f5e 100644
--- a/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_mapping_spec.ts
@@ -24,7 +24,7 @@ runInEachFileSystem((os) => {
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
- env.tsconfig();
+ env.tsconfig({sourceMap: true, target: 'ES2015', enableI18nLegacyMessageIdFormat: false});
});
describe('Inline templates', () => {
@@ -360,6 +360,90 @@ runInEachFileSystem((os) => {
});
});
+ describe('$localize', () => {
+ it('should create simple i18n message source-mapping', () => {
+ const mappings = compileAndMap(`
',
+ generated: 'i0.ɵɵelementStart(0, "div")',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: 'Hello, World!',
+ generated: '`Hello, World!`',
+ sourceUrl: '../test.ts',
+ });
+ });
+
+ it('should create placeholder source-mappings', () => {
+ const mappings = compileAndMap(`
Hello, {{name}}!
`);
+ expect(mappings).toContain({
+ source: '
',
+ generated: 'i0.ɵɵelementStart(0, "div")',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '
',
+ generated: 'i0.ɵɵelementEnd()',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: 'Hello, ',
+ generated: '`Hello, ${',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '{{name}}',
+ generated: '"\\uFFFD0\\uFFFD"',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '!',
+ generated: '}:INTERPOLATION:!`',
+ sourceUrl: '../test.ts',
+ });
+ });
+
+ it('should create tag (container) placeholder source-mappings', () => {
+ const mappings = compileAndMap(`
Hello, World!
`);
+ expect(mappings).toContain({
+ source: '
',
+ generated: 'i0.ɵɵelementStart(0, "div")',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '
',
+ generated: 'i0.ɵɵelementEnd()',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: 'Hello, ',
+ generated: '`Hello, ${',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '
',
+ generated: '"\\uFFFD#2\\uFFFD"',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: 'World',
+ generated: '}:START_BOLD_TEXT:World${',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '',
+ generated: '"\\uFFFD/#2\\uFFFD"',
+ sourceUrl: '../test.ts',
+ });
+ expect(mappings).toContain({
+ source: '!',
+ generated: '}:CLOSE_BOLD_TEXT:!`',
+ sourceUrl: '../test.ts',
+ });
+ });
+ });
+
it('should create (simple string) inline template source-mapping', () => {
const mappings = compileAndMap('
this is a test
{{ 1 + 2 }}
');
@@ -520,7 +604,6 @@ runInEachFileSystem((os) => {
function compileAndMap(template: string, templateUrl: string|null = null) {
const templateConfig = templateUrl ? `templateUrl: '${templateUrl}'` :
('template: `' + template.replace(/`/g, '\\`') + '`');
- env.tsconfig({sourceMap: true});
env.write('test.ts', `
import {Component} from '@angular/core';
diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts
index f71fa40f5741e..090e236135184 100644
--- a/packages/compiler/src/expression_parser/parser.ts
+++ b/packages/compiler/src/expression_parser/parser.ts
@@ -14,7 +14,10 @@ import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe,
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
export class SplitInterpolation {
- constructor(public strings: string[], public expressions: string[], public offsets: number[]) {}
+ constructor(
+ public strings: string[], public stringSpans: {start: number, end: number}[],
+ public expressions: string[], public expressionsSpans: {start: number, end: number}[],
+ public offsets: number[]) {}
}
export class TemplateBindingParseResult {
@@ -194,18 +197,24 @@ export class Parser {
const strings: string[] = [];
const expressions: string[] = [];
const offsets: number[] = [];
+ const stringSpans: {start: number, end: number}[] = [];
+ const expressionSpans: {start: number, end: number}[] = [];
let offset = 0;
for (let i = 0; i < parts.length; i++) {
const part: string = parts[i];
if (i % 2 === 0) {
// fixed string
strings.push(part);
+ const start = offset;
offset += part.length;
+ stringSpans.push({start, end: offset});
} else if (part.trim().length > 0) {
+ const start = offset;
offset += interpolationConfig.start.length;
expressions.push(part);
offsets.push(offset);
offset += part.length + interpolationConfig.end.length;
+ expressionSpans.push({start, end: offset});
} else {
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
@@ -213,9 +222,10 @@ export class Parser {
location);
expressions.push('$implicit');
offsets.push(offset);
+ expressionSpans.push({start: offset, end: offset});
}
}
- return new SplitInterpolation(strings, expressions, offsets);
+ return new SplitInterpolation(strings, stringSpans, expressions, expressionSpans, offsets);
}
wrapLiteralPrimitive(input: string|null, location: any, absoluteOffset: number): ASTWithSource {
diff --git a/packages/compiler/src/i18n/i18n_ast.ts b/packages/compiler/src/i18n/i18n_ast.ts
index 0a7159bbc185c..0f7c412205bb0 100644
--- a/packages/compiler/src/i18n/i18n_ast.ts
+++ b/packages/compiler/src/i18n/i18n_ast.ts
@@ -87,7 +87,7 @@ export class TagPlaceholder implements Node {
constructor(
public tag: string, public attrs: {[k: string]: string}, public startName: string,
public closeName: string, public children: Node[], public isVoid: boolean,
- public sourceSpan: ParseSourceSpan) {}
+ public sourceSpan: ParseSourceSpan, public closeSourceSpan: ParseSourceSpan|null) {}
visit(visitor: Visitor, context?: any): any {
return visitor.visitTagPlaceholder(this, context);
@@ -151,7 +151,8 @@ export class CloneVisitor implements Visitor {
visitTagPlaceholder(ph: TagPlaceholder, context?: any): TagPlaceholder {
const children = ph.children.map(n => n.visit(this, context));
return new TagPlaceholder(
- ph.tag, ph.attrs, ph.startName, ph.closeName, children, ph.isVoid, ph.sourceSpan);
+ ph.tag, ph.attrs, ph.startName, ph.closeName, children, ph.isVoid, ph.sourceSpan,
+ ph.closeSourceSpan);
}
visitPlaceholder(ph: Placeholder, context?: any): Placeholder {
diff --git a/packages/compiler/src/i18n/i18n_parser.ts b/packages/compiler/src/i18n/i18n_parser.ts
index 1a1f7f35bd179..923a9d8dff628 100644
--- a/packages/compiler/src/i18n/i18n_parser.ts
+++ b/packages/compiler/src/i18n/i18n_parser.ts
@@ -93,7 +93,8 @@ class _I18nVisitor implements html.Visitor {
}
const node = new i18n.TagPlaceholder(
- el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan!);
+ el.name, attrs, startPhName, closePhName, children, isVoid, el.startSourceSpan!,
+ el.endSourceSpan);
return context.visitNodeFn(el, node);
}
@@ -167,22 +168,32 @@ class _I18nVisitor implements html.Visitor {
if (splitInterpolation.strings[i].length) {
// No need to add empty strings
- nodes.push(new i18n.Text(splitInterpolation.strings[i], sourceSpan));
+ const stringSpan = getOffsetSourceSpan(sourceSpan, splitInterpolation.stringSpans[i]);
+ nodes.push(new i18n.Text(splitInterpolation.strings[i], stringSpan));
}
- nodes.push(new i18n.Placeholder(expression, phName, sourceSpan));
+ const expressionSpan =
+ getOffsetSourceSpan(sourceSpan, splitInterpolation.expressionsSpans[i]);
+ nodes.push(new i18n.Placeholder(expression, phName, expressionSpan));
context.placeholderToContent[phName] = sDelimiter + expression + eDelimiter;
}
// The last index contains no expression
const lastStringIdx = splitInterpolation.strings.length - 1;
if (splitInterpolation.strings[lastStringIdx].length) {
- nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], sourceSpan));
+ const stringSpan =
+ getOffsetSourceSpan(sourceSpan, splitInterpolation.stringSpans[lastStringIdx]);
+ nodes.push(new i18n.Text(splitInterpolation.strings[lastStringIdx], stringSpan));
}
return container;
}
}
+function getOffsetSourceSpan(
+ sourceSpan: ParseSourceSpan, {start, end}: {start: number, end: number}): ParseSourceSpan {
+ return new ParseSourceSpan(sourceSpan.start.moveBy(start), sourceSpan.start.moveBy(end));
+}
+
const _CUSTOM_PH_EXP =
/\/\/[\s\S]*i18n[\s\S]*\([\s\S]*ph[\s\S]*=[\s\S]*("|')([\s\S]*?)\1[\s\S]*\)/g;
diff --git a/packages/compiler/src/i18n/message_bundle.ts b/packages/compiler/src/i18n/message_bundle.ts
index 37fb284604d53..036b6d45fb175 100644
--- a/packages/compiler/src/i18n/message_bundle.ts
+++ b/packages/compiler/src/i18n/message_bundle.ts
@@ -94,7 +94,8 @@ class MapPlaceholderNames extends i18n.CloneVisitor {
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName)! : ph.closeName;
const children = ph.children.map(n => n.visit(this, mapper));
return new i18n.TagPlaceholder(
- ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan);
+ ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan,
+ ph.closeSourceSpan);
}
visitPlaceholder(ph: i18n.Placeholder, mapper: PlaceholderMapper): i18n.Placeholder {
diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts
index f718565ce32f4..f73e65db32750 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 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 127a3f6d94699..4c4f29eff27db 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/i18n/serializers/i18n_ast_spec.ts b/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
index 3ee5872d416f7..910bb735334f2 100644
--- a/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
+++ b/packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
@@ -41,7 +41,7 @@ import {_extractMessages} from '../i18n_parser_spec';
new i18n.IcuPlaceholder(null!, '', null!),
],
null!);
- const tag = new i18n.TagPlaceholder('', {}, '', '', [container], false, null!);
+ const tag = new i18n.TagPlaceholder('', {}, '', '', [container], false, null!, null);
const icu = new i18n.Icu('', '', {tag}, null!);
icu.visit(visitor);
diff --git a/packages/compiler/test/output/js_emitter_spec.ts b/packages/compiler/test/output/js_emitter_spec.ts
index c21297c4ee823..b6ff7deab5a81 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 78da4b697fb9c..e20b136f38b90 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 328cb7b7f3d32..4a02bf1e39834 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))
+ expect(o.localizedString(meta('', '', ''), [literal(''), literal('')], [literal('')], [])
+ .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('')],
+ [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,107 @@ 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
BC
D')).toEqual({
- messageParts: ['A ', 'B', 'C', '', ' D'],
- placeHolders: ['START_TAG_SPAN', 'START_TAG_DIV', 'CLOSE_TAG_DIV', 'CLOSE_TAG_SPAN']
+ messageParts: [literal('A '), literal('B'), literal('C'), literal(''), literal(' D')],
+ placeHolders: [
+ placeholder('START_TAG_SPAN'), placeholder('START_TAG_DIV'), placeholder('CLOSE_TAG_DIV'),
+ placeholder('CLOSE_TAG_SPAN')
+ ]
});
});
+ it('should compute source-spans when serializing content with HTML tags for `$localize()`',
+ () => {
+ debugger;
+ const {messageParts, placeHolders} = serialize('A
BC
D');
+
+ expect(messageParts[0].text).toEqual('A ');
+ expect(messageParts[0].sourceSpan.toString()).toEqual('A ');
+ expect(messageParts[1].text).toEqual('B');
+ expect(messageParts[1].sourceSpan.toString()).toEqual('B');
+ expect(messageParts[2].text).toEqual('C');
+ expect(messageParts[2].sourceSpan.toString()).toEqual('C');
+ expect(messageParts[3].text).toEqual('');
+ expect(messageParts[3].sourceSpan.toString()).toEqual('');
+ expect(messageParts[4].text).toEqual(' D');
+ expect(messageParts[4].sourceSpan.toString()).toEqual(' D');
+
+ expect(placeHolders[0].text).toEqual('START_TAG_SPAN');
+ expect(placeHolders[0].sourceSpan.toString()).toEqual('
');
+ expect(placeHolders[1].text).toEqual('START_TAG_DIV');
+ expect(placeHolders[1].sourceSpan.toString()).toEqual('');
+ expect(placeHolders[2].text).toEqual('CLOSE_TAG_DIV');
+ expect(placeHolders[2].sourceSpan.toString()).toEqual('
');
+ expect(placeHolders[3].text).toEqual('CLOSE_TAG_SPAN');
+ expect(placeHolders[3].sourceSpan.toString()).toEqual('');
+ });
+
it('should serialize simple ICU for `$localize()`', () => {
expect(serialize('{age, plural, 10 {ten} other {other}}')).toEqual({
- messageParts: ['{VAR_PLURAL, plural, 10 {ten} other {other}}'],
+ messageParts: [literal('{VAR_PLURAL, plural, 10 {ten} other {other}}')],
placeHolders: []
});
});
@@ -408,7 +472,8 @@ describe('serializeI18nMessageForLocalize', () => {
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}'))
.toEqual({
messageParts: [
- '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'
+ literal(
+ '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}')
],
placeHolders: []
});
@@ -418,7 +483,8 @@ describe('serializeI18nMessageForLocalize', () => {
it('should serialize ICU with embedded HTML for `$localize()`', () => {
expect(serialize('{age, plural, 10 {
ten} other {
other
}}')).toEqual({
messageParts: [
- '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'
+ literal(
+ '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}')
],
placeHolders: []
});
@@ -427,7 +493,8 @@ describe('serializeI18nMessageForLocalize', () => {
it('should serialize ICU with embedded interpolation for `$localize()`', () => {
expect(serialize('{age, plural, 10 {
ten} other {{{age}} years old}}')).toEqual({
messageParts: [
- '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{INTERPOLATION} years old}}'
+ literal(
+ '{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{INTERPOLATION} years old}}')
],
placeHolders: []
});
@@ -438,8 +505,11 @@ describe('serializeI18nMessageForLocalize', () => {
serialize(
'{gender, select, male {male} female {female} other {other}}
{gender, select, male {male} female {female} other {other}}
'))
.toEqual({
- messageParts: ['', '', '', '', ''],
- placeHolders: ['ICU', 'START_TAG_DIV', 'ICU', 'CLOSE_TAG_DIV']
+ messageParts: [literal(''), literal(''), literal(''), literal(''), literal('')],
+ placeHolders: [
+ placeholder('ICU'), placeholder('START_TAG_DIV'), placeholder('ICU'),
+ placeholder('CLOSE_TAG_DIV')
+ ]
});
});
@@ -449,7 +519,8 @@ describe('serializeI18nMessageForLocalize', () => {
'{age, plural, 10 {ten {size, select, 1 {{{ varOne }}} 2 {{{ varTwo }}} other {2+}}} other {other}}'))
.toEqual({
messageParts: [
- '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {{INTERPOLATION}} 2 {{INTERPOLATION_1}} other {2+}}} other {other}}'
+ literal(
+ '{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {{INTERPOLATION}} 2 {{INTERPOLATION_1}} other {2+}}} other {other}}')
],
placeHolders: []
});
@@ -486,3 +557,11 @@ describe('serializeIcuNode', () => {
.toEqual('{VAR_SELECT, select, 10 {ten} other {{INTERPOLATION} years old}}');
});
});
+
+function literal(text: string, span: any = jasmine.any(ParseSourceSpan)): o.LiteralPiece {
+ return new o.LiteralPiece(text, span);
+}
+
+function placeholder(name: string, span: any = jasmine.any(ParseSourceSpan)): o.PlaceholderPiece {
+ return new o.PlaceholderPiece(name, span);
+}