From f01b7183d2064f41c0f5e30ee976cc91c15e06c5 Mon Sep 17 00:00:00 2001
From: Kristiyan Kostadinov
Date: Thu, 16 Nov 2023 12:52:23 +0100
Subject: [PATCH] fix(compiler): produce placeholder for blocks in i18n bundles
(#52958)
When blocks were initially implemented, they were represented as containers in the i18n AST. This is problematic, because block affect the structure of the message.
These changes introduce a new `BlockPlaceholder` AST node and integrate it into the i18n pipeline. With the new node blocks are represented with the `START_BLOCK_` and `CLOSE_BLOCK_` placeholders.
PR Close #52958
---
packages/compiler/src/compiler.ts | 3 +-
.../compiler/src/expression_parser/parser.ts | 2 +-
packages/compiler/src/i18n/digest.ts | 5 ++
.../compiler/src/i18n/extractor_merger.ts | 5 +-
packages/compiler/src/i18n/i18n_ast.ts | 28 ++++++++++
.../compiler/src/i18n/i18n_html_parser.ts | 2 +-
packages/compiler/src/i18n/i18n_parser.ts | 40 +++++++++++---
packages/compiler/src/i18n/message_bundle.ts | 12 ++++-
.../src/i18n/serializers/placeholder.ts | 36 +++++++++++++
.../src/i18n/serializers/serializer.ts | 6 +++
.../compiler/src/i18n/serializers/xliff.ts | 9 ++++
.../compiler/src/i18n/serializers/xliff2.ts | 19 +++++++
packages/compiler/src/i18n/serializers/xmb.ts | 14 +++++
.../compiler/src/i18n/translation_bundle.ts | 6 +++
packages/compiler/src/jit_compiler_facade.ts | 2 +-
packages/compiler/src/ml_parser/ast.ts | 11 ++--
.../{interpolation_config.ts => defaults.ts} | 2 +
packages/compiler/src/ml_parser/lexer.ts | 2 +-
.../compiler/src/render3/partial/component.ts | 2 +-
packages/compiler/src/render3/view/api.ts | 2 +-
.../src/render3/view/i18n/get_msg_utils.ts | 5 ++
.../src/render3/view/i18n/icu_serializer.ts | 5 ++
.../src/render3/view/i18n/localize_utils.ts | 7 +++
.../compiler/src/render3/view/i18n/meta.ts | 10 ++--
.../compiler/src/render3/view/template.ts | 2 +-
.../phases/resolve_i18n_icu_placeholders.ts | 18 +++++--
.../src/template_parser/binding_parser.ts | 4 +-
.../test/expression_parser/utils/unparser.ts | 2 +-
.../test/i18n/extractor_merger_spec.ts | 8 ++-
packages/compiler/test/i18n/i18n_ast_spec.ts | 14 ++++-
.../compiler/test/i18n/i18n_parser_spec.ts | 2 +-
.../compiler/test/i18n/integration_common.ts | 2 +-
.../compiler/test/i18n/message_bundle_spec.ts | 2 +-
.../test/i18n/serializers/placeholder_spec.ts | 12 +++++
.../test/i18n/serializers/xliff2_spec.ts | 52 +++++++++++++------
.../test/i18n/serializers/xliff_spec.ts | 50 ++++++++++++------
.../test/i18n/serializers/xmb_spec.ts | 6 ++-
packages/compiler/test/render3/view/util.ts | 2 +-
38 files changed, 333 insertions(+), 78 deletions(-)
rename packages/compiler/src/ml_parser/{interpolation_config.ts => defaults.ts} (92%)
diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts
index 3b315da5f7d9b..7b6c8508fe0d4 100644
--- a/packages/compiler/src/compiler.ts
+++ b/packages/compiler/src/compiler.ts
@@ -38,7 +38,7 @@ export * from './version';
export {CompilerConfig, preserveWhitespacesDefault} from './config';
export * from './resource_loader';
export {ConstantPool} from './constant_pool';
-export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
+export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/defaults';
export * from './schema/element_schema_registry';
export * from './i18n/index';
export * from './expression_parser/ast';
@@ -47,7 +47,6 @@ export * from './expression_parser/parser';
export * from './ml_parser/ast';
export * from './ml_parser/html_parser';
export * from './ml_parser/html_tags';
-export * from './ml_parser/interpolation_config';
export * from './ml_parser/tags';
export {ParseTreeResult, TreeError} from './ml_parser/parser';
export {LexerRange} from './ml_parser/lexer';
diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts
index a83db2e26b4ce..a60c08efd84cf 100644
--- a/packages/compiler/src/expression_parser/parser.ts
+++ b/packages/compiler/src/expression_parser/parser.ts
@@ -7,7 +7,7 @@
*/
import * as chars from '../chars';
-import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
+import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/defaults';
import {InterpolatedAttributeToken, InterpolatedTextToken, TokenType as MlParserTokenType} from '../ml_parser/tokens';
import {AbsoluteSourceSpan, AST, ASTWithSource, Binary, BindingPipe, Call, Chain, Conditional, EmptyExpr, ExpressionBinding, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, RecursiveAstVisitor, SafeCall, SafeKeyedRead, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, ThisReceiver, Unary, VariableBinding} from './ast';
diff --git a/packages/compiler/src/i18n/digest.ts b/packages/compiler/src/i18n/digest.ts
index 276515c609844..68b8c18e4e0bd 100644
--- a/packages/compiler/src/i18n/digest.ts
+++ b/packages/compiler/src/i18n/digest.ts
@@ -81,6 +81,11 @@ class _SerializerVisitor implements i18n.Visitor {
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
return `${ph.value.visit(this)}`;
}
+
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context: any): any {
+ return `${
+ ph.children.map(child => child.visit(this)).join(', ')}`;
+ }
}
const serializerVisitor = new _SerializerVisitor();
diff --git a/packages/compiler/src/i18n/extractor_merger.ts b/packages/compiler/src/i18n/extractor_merger.ts
index 264427b7827c1..33f951c66963c 100644
--- a/packages/compiler/src/i18n/extractor_merger.ts
+++ b/packages/compiler/src/i18n/extractor_merger.ts
@@ -7,7 +7,7 @@
*/
import * as html from '../ml_parser/ast';
-import {InterpolationConfig} from '../ml_parser/interpolation_config';
+import {DEFAULT_CONTAINER_BLOCKS, InterpolationConfig} from '../ml_parser/defaults';
import {ParseTreeResult} from '../ml_parser/parser';
import * as i18n from './i18n_ast';
@@ -308,7 +308,8 @@ class _Visitor implements html.Visitor {
this._errors = [];
this._messages = [];
this._inImplicitNode = false;
- this._createI18nMessage = createI18nMessageFactory(interpolationConfig);
+ this._createI18nMessage =
+ createI18nMessageFactory(interpolationConfig, DEFAULT_CONTAINER_BLOCKS);
}
// looks for translatable attributes
diff --git a/packages/compiler/src/i18n/i18n_ast.ts b/packages/compiler/src/i18n/i18n_ast.ts
index a8b5f77d16d36..94d86128d78d8 100644
--- a/packages/compiler/src/i18n/i18n_ast.ts
+++ b/packages/compiler/src/i18n/i18n_ast.ts
@@ -129,6 +129,17 @@ export class IcuPlaceholder implements Node {
}
}
+export class BlockPlaceholder implements Node {
+ constructor(
+ public name: string, public parameters: string[], public startName: string,
+ public closeName: string, public children: Node[], public sourceSpan: ParseSourceSpan,
+ public startSourceSpan: ParseSourceSpan|null, public endSourceSpan: ParseSourceSpan|null) {}
+
+ visit(visitor: Visitor, context?: any): any {
+ return visitor.visitBlockPlaceholder(this, context);
+ }
+}
+
/**
* Each HTML node that is affect by an i18n tag will also have an `i18n` property that is of type
* `I18nMeta`.
@@ -144,6 +155,7 @@ export interface Visitor {
visitTagPlaceholder(ph: TagPlaceholder, context?: any): any;
visitPlaceholder(ph: Placeholder, context?: any): any;
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any;
+ visitBlockPlaceholder(ph: BlockPlaceholder, context?: any): any;
}
// Clone the AST
@@ -178,6 +190,13 @@ export class CloneVisitor implements Visitor {
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): IcuPlaceholder {
return new IcuPlaceholder(ph.value, ph.name, ph.sourceSpan);
}
+
+ visitBlockPlaceholder(ph: BlockPlaceholder, context?: any): BlockPlaceholder {
+ const children = ph.children.map(n => n.visit(this, context));
+ return new BlockPlaceholder(
+ ph.name, ph.parameters, ph.startName, ph.closeName, children, ph.sourceSpan,
+ ph.startSourceSpan, ph.endSourceSpan);
+ }
}
// Visit all the nodes recursively
@@ -201,6 +220,10 @@ export class RecurseVisitor implements Visitor {
visitPlaceholder(ph: Placeholder, context?: any): any {}
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any {}
+
+ visitBlockPlaceholder(ph: BlockPlaceholder, context?: any): any {
+ ph.children.forEach(child => child.visit(this));
+ }
}
@@ -240,4 +263,9 @@ class LocalizeMessageStringVisitor implements Visitor {
visitIcuPlaceholder(ph: IcuPlaceholder): any {
return `{$${ph.name}}`;
}
+
+ visitBlockPlaceholder(ph: BlockPlaceholder): any {
+ const children = ph.children.map(child => child.visit(this)).join('');
+ return `{$${ph.startName}}${children}{$${ph.closeName}}`;
+ }
}
diff --git a/packages/compiler/src/i18n/i18n_html_parser.ts b/packages/compiler/src/i18n/i18n_html_parser.ts
index 41029bfb8e937..cf54abd543918 100644
--- a/packages/compiler/src/i18n/i18n_html_parser.ts
+++ b/packages/compiler/src/i18n/i18n_html_parser.ts
@@ -7,8 +7,8 @@
*/
import {MissingTranslationStrategy} from '../core';
+import {DEFAULT_INTERPOLATION_CONFIG} from '../ml_parser/defaults';
import {HtmlParser} from '../ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../ml_parser/interpolation_config';
import {TokenizeOptions} from '../ml_parser/lexer';
import {ParseTreeResult} from '../ml_parser/parser';
import {Console} from '../util';
diff --git a/packages/compiler/src/i18n/i18n_parser.ts b/packages/compiler/src/i18n/i18n_parser.ts
index c5ef54aa94459..ee2977751ec26 100644
--- a/packages/compiler/src/i18n/i18n_parser.ts
+++ b/packages/compiler/src/i18n/i18n_parser.ts
@@ -9,8 +9,8 @@
import {Lexer as ExpressionLexer} from '../expression_parser/lexer';
import {Parser as ExpressionParser} from '../expression_parser/parser';
import * as html from '../ml_parser/ast';
+import {InterpolationConfig} from '../ml_parser/defaults';
import {getHtmlTagDefinition} from '../ml_parser/html_tags';
-import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {InterpolatedAttributeToken, InterpolatedTextToken, TokenType} from '../ml_parser/tokens';
import {ParseSourceSpan} from '../parse_util';
@@ -29,9 +29,9 @@ export interface I18nMessageFactory {
/**
* Returns a function converting html nodes to an i18n Message given an interpolationConfig
*/
-export function createI18nMessageFactory(interpolationConfig: InterpolationConfig):
- I18nMessageFactory {
- const visitor = new _I18nVisitor(_expParser, interpolationConfig);
+export function createI18nMessageFactory(
+ interpolationConfig: InterpolationConfig, containerBlocks: Set): I18nMessageFactory {
+ const visitor = new _I18nVisitor(_expParser, interpolationConfig, containerBlocks);
return (nodes, meaning, description, customId, visitNodeFn) =>
visitor.toI18nMessage(nodes, meaning, description, customId, visitNodeFn);
}
@@ -52,7 +52,9 @@ function noopVisitNodeFn(_html: html.Node, i18n: i18n.Node): i18n.Node {
class _I18nVisitor implements html.Visitor {
constructor(
private _expressionParser: ExpressionParser,
- private _interpolationConfig: InterpolationConfig) {}
+ private _interpolationConfig: InterpolationConfig,
+ private _containerBlocks: Set,
+ ) {}
public toI18nMessage(
nodes: html.Node[], meaning = '', description = '', customId = '',
@@ -164,11 +166,35 @@ class _I18nVisitor implements html.Visitor {
visitBlock(block: html.Block, context: I18nMessageVisitorContext) {
const children = html.visitAll(this, block.children, context);
- const node = new i18n.Container(children, block.sourceSpan);
+
+ if (this._containerBlocks.has(block.name)) {
+ return new i18n.Container(children, block.sourceSpan);
+ }
+
+ const parameters = block.parameters.map(param => param.expression);
+ const startPhName =
+ context.placeholderRegistry.getStartBlockPlaceholderName(block.name, parameters);
+ const closePhName = context.placeholderRegistry.getCloseBlockPlaceholderName(block.name);
+
+ context.placeholderToContent[startPhName] = {
+ text: block.startSourceSpan.toString(),
+ sourceSpan: block.startSourceSpan,
+ };
+
+ context.placeholderToContent[closePhName] = {
+ text: block.endSourceSpan ? block.endSourceSpan.toString() : '}',
+ sourceSpan: block.endSourceSpan ?? block.sourceSpan,
+ };
+
+ const node = new i18n.BlockPlaceholder(
+ block.name, parameters, startPhName, closePhName, children, block.sourceSpan,
+ block.startSourceSpan, block.endSourceSpan);
return context.visitNodeFn(block, node);
}
- visitBlockParameter(_parameter: html.BlockParameter, _context: I18nMessageVisitorContext) {}
+ visitBlockParameter(_parameter: html.BlockParameter, _context: I18nMessageVisitorContext) {
+ throw new Error('Unreachable code');
+ }
/**
* Convert, text and interpolated tokens up into text and placeholder pieces.
diff --git a/packages/compiler/src/i18n/message_bundle.ts b/packages/compiler/src/i18n/message_bundle.ts
index 3bf6d90a98493..070452cd8282a 100644
--- a/packages/compiler/src/i18n/message_bundle.ts
+++ b/packages/compiler/src/i18n/message_bundle.ts
@@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {InterpolationConfig} from '../ml_parser/defaults';
import {HtmlParser} from '../ml_parser/html_parser';
-import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseError} from '../parse_util';
import {extractMessages} from './extractor_merger';
@@ -99,6 +99,16 @@ class MapPlaceholderNames extends i18n.CloneVisitor {
ph.startSourceSpan, ph.endSourceSpan);
}
+ override visitBlockPlaceholder(ph: i18n.BlockPlaceholder, mapper: PlaceholderMapper):
+ i18n.BlockPlaceholder {
+ const startName = mapper.toPublicName(ph.startName)!;
+ const closeName = ph.closeName ? mapper.toPublicName(ph.closeName)! : ph.closeName;
+ const children = ph.children.map(n => n.visit(this, mapper));
+ return new i18n.BlockPlaceholder(
+ ph.name, ph.parameters, startName, closeName, children, ph.sourceSpan, ph.startSourceSpan,
+ ph.endSourceSpan);
+ }
+
override visitPlaceholder(ph: i18n.Placeholder, mapper: PlaceholderMapper): i18n.Placeholder {
return new i18n.Placeholder(ph.value, mapper.toPublicName(ph.name)!, ph.sourceSpan);
}
diff --git a/packages/compiler/src/i18n/serializers/placeholder.ts b/packages/compiler/src/i18n/serializers/placeholder.ts
index 17f4b72dbf67c..55831fbf6f3e9 100644
--- a/packages/compiler/src/i18n/serializers/placeholder.ts
+++ b/packages/compiler/src/i18n/serializers/placeholder.ts
@@ -97,6 +97,29 @@ export class PlaceholderRegistry {
return this._generateUniqueName(name.toUpperCase());
}
+ getStartBlockPlaceholderName(name: string, parameters: string[]): string {
+ const signature = this._hashBlock(name, parameters);
+ if (this._signatureToName[signature]) {
+ return this._signatureToName[signature];
+ }
+
+ const placeholder = this._generateUniqueName(`START_BLOCK_${this._toSnakeCase(name)}`);
+ this._signatureToName[signature] = placeholder;
+ return placeholder;
+ }
+
+ getCloseBlockPlaceholderName(name: string): string {
+ const signature = this._hashClosingBlock(name);
+ if (this._signatureToName[signature]) {
+ return this._signatureToName[signature];
+ }
+
+ const placeholder = this._generateUniqueName(`CLOSE_BLOCK_${this._toSnakeCase(name)}`);
+ this._signatureToName[signature] = placeholder;
+ return placeholder;
+ }
+
+
// Generate a hash for a tag - does not take attribute order into account
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
const start = `<${tag}`;
@@ -110,6 +133,19 @@ export class PlaceholderRegistry {
return this._hashTag(`/${tag}`, {}, false);
}
+ private _hashBlock(name: string, parameters: string[]): string {
+ const params = parameters.length === 0 ? '' : ` (${parameters.sort().join('; ')})`;
+ return `@${name}${params} {}`;
+ }
+
+ private _hashClosingBlock(name: string): string {
+ return this._hashBlock(`close_${name}`, []);
+ }
+
+ private _toSnakeCase(name: string) {
+ return name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
+ }
+
private _generateUniqueName(base: string): string {
const seen = this._placeHolderNameCounts.hasOwnProperty(base);
if (!seen) {
diff --git a/packages/compiler/src/i18n/serializers/serializer.ts b/packages/compiler/src/i18n/serializers/serializer.ts
index effcfa6091f4f..e7a4090545d8a 100644
--- a/packages/compiler/src/i18n/serializers/serializer.ts
+++ b/packages/compiler/src/i18n/serializers/serializer.ts
@@ -77,6 +77,12 @@ export class SimplePlaceholderMapper extends i18n.RecurseVisitor implements Plac
this.visitPlaceholderName(ph.name);
}
+ override visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): any {
+ this.visitPlaceholderName(ph.startName);
+ super.visitBlockPlaceholder(ph, context);
+ this.visitPlaceholderName(ph.closeName);
+ }
+
override visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
this.visitPlaceholderName(ph.name);
}
diff --git a/packages/compiler/src/i18n/serializers/xliff.ts b/packages/compiler/src/i18n/serializers/xliff.ts
index 7c9b5a1a5dc38..54aabbd252c45 100644
--- a/packages/compiler/src/i18n/serializers/xliff.ts
+++ b/packages/compiler/src/i18n/serializers/xliff.ts
@@ -164,6 +164,15 @@ class _WriteVisitor implements i18n.Visitor {
return [new xml.Tag(_PLACEHOLDER_TAG, {id: ph.name, 'equiv-text': `{{${ph.value}}}`})];
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): xml.Node[] {
+ const ctype = `x-${ph.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
+ const startTagPh =
+ new xml.Tag(_PLACEHOLDER_TAG, {id: ph.startName, ctype, 'equiv-text': `@${ph.name}`});
+ const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {id: ph.closeName, ctype, 'equiv-text': `}`});
+
+ return [startTagPh, ...this.serialize(ph.children), closeTagPh];
+ }
+
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
const equivText = `{${ph.value.expression}, ${ph.value.type}, ${
Object.keys(ph.value.cases).map((value: string) => value + ' {...}').join(' ')}}`;
diff --git a/packages/compiler/src/i18n/serializers/xliff2.ts b/packages/compiler/src/i18n/serializers/xliff2.ts
index 370a055c8663a..c6ba2f12cd5de 100644
--- a/packages/compiler/src/i18n/serializers/xliff2.ts
+++ b/packages/compiler/src/i18n/serializers/xliff2.ts
@@ -178,6 +178,25 @@ class _WriteVisitor implements i18n.Visitor {
})];
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): xml.Node[] {
+ const tagPc = new xml.Tag(_PLACEHOLDER_SPANNING_TAG, {
+ id: (this._nextPlaceholderId++).toString(),
+ equivStart: ph.startName,
+ equivEnd: ph.closeName,
+ type: 'other',
+ dispStart: `@${ph.name}`,
+ dispEnd: `}`,
+ });
+ const nodes: xml.Node[] = [].concat(...ph.children.map(node => node.visit(this)));
+ if (nodes.length) {
+ nodes.forEach((node: xml.Node) => tagPc.children.push(node));
+ } else {
+ tagPc.children.push(new xml.Text(''));
+ }
+
+ return [tagPc];
+ }
+
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
const cases = Object.keys(ph.value.cases).map((value: string) => value + ' {...}').join(' ');
const idStr = (this._nextPlaceholderId++).toString();
diff --git a/packages/compiler/src/i18n/serializers/xmb.ts b/packages/compiler/src/i18n/serializers/xmb.ts
index 3688454d308d4..28bb0974716e4 100644
--- a/packages/compiler/src/i18n/serializers/xmb.ts
+++ b/packages/compiler/src/i18n/serializers/xmb.ts
@@ -148,6 +148,20 @@ class _Visitor implements i18n.Visitor {
];
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): xml.Node[] {
+ const startAsText = new xml.Text(`@${ph.name}`);
+ const startEx = new xml.Tag(_EXAMPLE_TAG, {}, [startAsText]);
+ // TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
+ const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.startName}, [startEx, startAsText]);
+
+ const closeAsText = new xml.Text(`}`);
+ const closeEx = new xml.Tag(_EXAMPLE_TAG, {}, [closeAsText]);
+ // TC requires PH to have a non empty EX, and uses the text node to show the "original" value.
+ const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.closeName}, [closeEx, closeAsText]);
+
+ return [startTagPh, ...this.serialize(ph.children), closeTagPh];
+ }
+
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
const icuExpression = ph.value.expression;
const icuType = ph.value.type;
diff --git a/packages/compiler/src/i18n/translation_bundle.ts b/packages/compiler/src/i18n/translation_bundle.ts
index 6d5b19be4b9af..f6cd6e305a391 100644
--- a/packages/compiler/src/i18n/translation_bundle.ts
+++ b/packages/compiler/src/i18n/translation_bundle.ts
@@ -149,6 +149,12 @@ class I18nToHtmlVisitor implements i18n.Visitor {
return this._convertToText(this._srcMsg.placeholderToMessage[ph.name]);
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): string {
+ const params = ph.parameters.length === 0 ? '' : ` (${ph.parameters.join('; ')})`;
+ const children = ph.children.map((c: i18n.Node) => c.visit(this)).join('');
+ return `@${ph.name}${params} {${children}}`;
+ }
+
/**
* Convert a source message to a translated text string:
* - text nodes are replaced with their translation,
diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts
index 42c6559bc1047..fb4dd7b02c7be 100644
--- a/packages/compiler/src/jit_compiler_facade.ts
+++ b/packages/compiler/src/jit_compiler_facade.ts
@@ -10,7 +10,7 @@ import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, InputMap, Input
import {ConstantPool} from './constant_pool';
import {ChangeDetectionStrategy, HostBinding, HostListener, Input, Output, ViewEncapsulation} from './core';
import {compileInjectable} from './injectable_compiler_2';
-import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
+import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/defaults';
import {DeclareVarStmt, Expression, literal, LiteralExpr, Statement, StmtModifier, WrappedNodeExpr} from './output/output_ast';
import {JitEvaluator} from './output/output_jit';
import {ParseError, ParseSourceSpan, r3JitTypeSourceSpan} from './parse_util';
diff --git a/packages/compiler/src/ml_parser/ast.ts b/packages/compiler/src/ml_parser/ast.ts
index 1d608a28861a4..b509728e408a5 100644
--- a/packages/compiler/src/ml_parser/ast.ts
+++ b/packages/compiler/src/ml_parser/ast.ts
@@ -86,13 +86,16 @@ export class Comment implements BaseNode {
}
}
-export class Block implements BaseNode {
+export class Block extends NodeWithI18n {
constructor(
public name: string, public parameters: BlockParameter[], public children: Node[],
- public sourceSpan: ParseSourceSpan, public nameSpan: ParseSourceSpan,
- public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null = null) {}
+ sourceSpan: ParseSourceSpan, public nameSpan: ParseSourceSpan,
+ public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null = null,
+ i18n?: I18nMeta) {
+ super(sourceSpan, i18n);
+ }
- visit(visitor: Visitor, context: any) {
+ override visit(visitor: Visitor, context: any) {
return visitor.visitBlock(this, context);
}
}
diff --git a/packages/compiler/src/ml_parser/interpolation_config.ts b/packages/compiler/src/ml_parser/defaults.ts
similarity index 92%
rename from packages/compiler/src/ml_parser/interpolation_config.ts
rename to packages/compiler/src/ml_parser/defaults.ts
index 70854b9f8de0e..90078a26a8e45 100644
--- a/packages/compiler/src/ml_parser/interpolation_config.ts
+++ b/packages/compiler/src/ml_parser/defaults.ts
@@ -23,3 +23,5 @@ export class InterpolationConfig {
export const DEFAULT_INTERPOLATION_CONFIG: InterpolationConfig =
new InterpolationConfig('{{', '}}');
+
+export const DEFAULT_CONTAINER_BLOCKS = new Set(['switch']);
diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts
index f2842341809ff..74ef678dea6b1 100644
--- a/packages/compiler/src/ml_parser/lexer.ts
+++ b/packages/compiler/src/ml_parser/lexer.ts
@@ -9,8 +9,8 @@
import * as chars from '../chars';
import {ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan} from '../parse_util';
+import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './defaults';
import {NAMED_ENTITIES} from './entities';
-import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './interpolation_config';
import {TagContentType, TagDefinition} from './tags';
import {IncompleteTagOpenToken, TagOpenStartToken, Token, TokenType} from './tokens';
diff --git a/packages/compiler/src/render3/partial/component.ts b/packages/compiler/src/render3/partial/component.ts
index 23913ed141eb6..c1f89aef9a80f 100644
--- a/packages/compiler/src/render3/partial/component.ts
+++ b/packages/compiler/src/render3/partial/component.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as core from '../../core';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
+import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/defaults';
import * as o from '../../output/output_ast';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../parse_util';
import {RecursiveVisitor, visitAll} from '../r3_ast';
diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts
index 74363df18e8d7..103eb707f7067 100644
--- a/packages/compiler/src/render3/view/api.ts
+++ b/packages/compiler/src/render3/view/api.ts
@@ -7,7 +7,7 @@
*/
import {ChangeDetectionStrategy, ViewEncapsulation} from '../../core';
-import {InterpolationConfig} from '../../ml_parser/interpolation_config';
+import {InterpolationConfig} from '../../ml_parser/defaults';
import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import * as t from '../r3_ast';
diff --git a/packages/compiler/src/render3/view/i18n/get_msg_utils.ts b/packages/compiler/src/render3/view/i18n/get_msg_utils.ts
index f17b5ac0fbf79..2a38d2eaa7f57 100644
--- a/packages/compiler/src/render3/view/i18n/get_msg_utils.ts
+++ b/packages/compiler/src/render3/view/i18n/get_msg_utils.ts
@@ -128,6 +128,11 @@ class GetMsgSerializerVisitor implements i18n.Visitor {
return this.formatPh(ph.name);
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder): any {
+ return `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${
+ this.formatPh(ph.closeName)}`;
+ }
+
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
return this.formatPh(ph.name);
}
diff --git a/packages/compiler/src/render3/view/i18n/icu_serializer.ts b/packages/compiler/src/render3/view/i18n/icu_serializer.ts
index 2466c7bfb393b..0454c145e74ec 100644
--- a/packages/compiler/src/render3/view/i18n/icu_serializer.ts
+++ b/packages/compiler/src/render3/view/i18n/icu_serializer.ts
@@ -37,6 +37,11 @@ class IcuSerializerVisitor implements i18n.Visitor {
return this.formatPh(ph.name);
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder): any {
+ return `${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${
+ this.formatPh(ph.closeName)}`;
+ }
+
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
return this.formatPh(ph.name);
}
diff --git a/packages/compiler/src/render3/view/i18n/localize_utils.ts b/packages/compiler/src/render3/view/i18n/localize_utils.ts
index 436e353dd57ff..70006561ba808 100644
--- a/packages/compiler/src/render3/view/i18n/localize_utils.ts
+++ b/packages/compiler/src/render3/view/i18n/localize_utils.ts
@@ -68,6 +68,13 @@ class LocalizeSerializerVisitor implements i18n.Visitor {
this.pieces.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
+ visitBlockPlaceholder(ph: i18n.BlockPlaceholder): any {
+ this.pieces.push(
+ this.createPlaceholderPiece(ph.startName, ph.startSourceSpan ?? ph.sourceSpan));
+ ph.children.forEach(child => child.visit(this));
+ this.pieces.push(this.createPlaceholderPiece(ph.closeName, ph.endSourceSpan ?? ph.sourceSpan));
+ }
+
visitIcuPlaceholder(ph: i18n.IcuPlaceholder): any {
this.pieces.push(
this.createPlaceholderPiece(ph.name, ph.sourceSpan, this.placeholderToMessage[ph.name]));
diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts
index f70bb498cd8a8..30c4943545253 100644
--- a/packages/compiler/src/render3/view/i18n/meta.ts
+++ b/packages/compiler/src/render3/view/i18n/meta.ts
@@ -8,10 +8,10 @@
import {computeDecimalDigest, computeDigest, decimalDigest} from '../../../i18n/digest';
import * as i18n from '../../../i18n/i18n_ast';
-import {createI18nMessageFactory, I18nMessageFactory, VisitNodeFn} from '../../../i18n/i18n_parser';
+import {createI18nMessageFactory, VisitNodeFn} from '../../../i18n/i18n_parser';
import {I18nError} from '../../../i18n/parse_util';
import * as html from '../../../ml_parser/ast';
-import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../ml_parser/interpolation_config';
+import {DEFAULT_CONTAINER_BLOCKS, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../ml_parser/defaults';
import {ParseTreeResult} from '../../../ml_parser/parser';
import * as o from '../../../output/output_ast';
import {isTrustedTypesSink} from '../../../schema/trusted_types_sinks';
@@ -53,13 +53,15 @@ export class I18nMetaVisitor implements html.Visitor {
constructor(
private interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG,
- private keepI18nAttrs = false, private enableI18nLegacyMessageIdFormat = false) {}
+ private keepI18nAttrs = false, private enableI18nLegacyMessageIdFormat = false,
+ private containerBlocks: Set = DEFAULT_CONTAINER_BLOCKS) {}
private _generateI18nMessage(
nodes: html.Node[], meta: string|i18n.I18nMeta = '',
visitNodeFn?: VisitNodeFn): i18n.Message {
const {meaning, description, customId} = this._parseMetadata(meta);
- const createI18nMessage = createI18nMessageFactory(this.interpolationConfig);
+ const createI18nMessage =
+ createI18nMessageFactory(this.interpolationConfig, this.containerBlocks);
const message = createI18nMessage(nodes, meaning, description, customId, visitNodeFn);
this._setMessageId(message, meta);
this._setLegacyIds(message, meta);
diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts
index 3854cadb8329a..c320f471759d6 100644
--- a/packages/compiler/src/render3/view/template.ts
+++ b/packages/compiler/src/render3/view/template.ts
@@ -14,9 +14,9 @@ import {Lexer} from '../../expression_parser/lexer';
import {Parser} from '../../expression_parser/parser';
import * as i18n from '../../i18n/i18n_ast';
import * as html from '../../ml_parser/ast';
+import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../ml_parser/defaults';
import {HtmlParser} from '../../ml_parser/html_parser';
import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces';
-import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../ml_parser/interpolation_config';
import {LexerRange} from '../../ml_parser/lexer';
import {isNgContainer as checkIsNgContainer, splitNsName} from '../../ml_parser/tags';
import {mapLiteral} from '../../output/map_util';
diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts
index 595660908f3e9..d9fdfa474087c 100644
--- a/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts
+++ b/packages/compiler/src/template/pipeline/src/phases/resolve_i18n_icu_placeholders.ts
@@ -50,11 +50,9 @@ class ResolveIcuPlaceholdersVisitor extends i18n.RecurseVisitor {
super();
}
- override visitTagPlaceholder(placeholder: i18n.TagPlaceholder) {
- super.visitTagPlaceholder(placeholder);
-
- // Add the start and end source span for tag placeholders. These need to be recorded for
- // elements inside ICUs. The slots for the elements were recorded separately under the i18n
+ private visitContainerPlaceholder(placeholder: i18n.TagPlaceholder|i18n.BlockPlaceholder) {
+ // Add the start and end source span for container placeholders. These need to be recorded for
+ // elements inside ICUs. The slots for the nodes were recorded separately under the i18n
// block's context as part of the `resolveI18nElementPlaceholders` phase.
if (placeholder.startName && placeholder.startSourceSpan &&
!this.params.has(placeholder.startName)) {
@@ -73,4 +71,14 @@ class ResolveIcuPlaceholdersVisitor extends i18n.RecurseVisitor {
}]);
}
}
+
+ override visitTagPlaceholder(placeholder: i18n.TagPlaceholder) {
+ super.visitTagPlaceholder(placeholder);
+ this.visitContainerPlaceholder(placeholder);
+ }
+
+ override visitBlockPlaceholder(placeholder: i18n.BlockPlaceholder) {
+ super.visitBlockPlaceholder(placeholder);
+ this.visitContainerPlaceholder(placeholder);
+ }
}
diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts
index d640a9cf8eea3..d905e24cdd3ba 100644
--- a/packages/compiler/src/template_parser/binding_parser.ts
+++ b/packages/compiler/src/template_parser/binding_parser.ts
@@ -9,10 +9,10 @@
import {SecurityContext} from '../core';
import {AbsoluteSourceSpan, ASTWithSource, BindingPipe, BindingType, BoundElementProperty, EmptyExpr, ParsedEvent, ParsedEventType, ParsedProperty, ParsedPropertyType, ParsedVariable, ParserError, RecursiveAstVisitor, TemplateBinding, VariableBinding} from '../expression_parser/ast';
import {Parser} from '../expression_parser/parser';
-import {InterpolationConfig} from '../ml_parser/interpolation_config';
+import {InterpolationConfig} from '../ml_parser/defaults';
import {mergeNsAndName} from '../ml_parser/tags';
import {InterpolatedAttributeToken, InterpolatedTextToken} from '../ml_parser/tokens';
-import {ParseError, ParseErrorLevel, ParseLocation, ParseSourceSpan} from '../parse_util';
+import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../selector';
import {splitAtColon, splitAtPeriod} from '../util';
diff --git a/packages/compiler/test/expression_parser/utils/unparser.ts b/packages/compiler/test/expression_parser/utils/unparser.ts
index d4488498089f1..4cf474595f3dc 100644
--- a/packages/compiler/test/expression_parser/utils/unparser.ts
+++ b/packages/compiler/test/expression_parser/utils/unparser.ts
@@ -7,7 +7,7 @@
*/
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Call, Chain, Conditional, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, RecursiveAstVisitor, SafeCall, SafeKeyedRead, SafePropertyRead, ThisReceiver, Unary} from '../../../src/expression_parser/ast';
-import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
+import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/defaults';
class Unparser implements AstVisitor {
private static _quoteRegExp = /"/g;
diff --git a/packages/compiler/test/i18n/extractor_merger_spec.ts b/packages/compiler/test/i18n/extractor_merger_spec.ts
index 3d0fa6df06101..27b7071c936fb 100644
--- a/packages/compiler/test/i18n/extractor_merger_spec.ts
+++ b/packages/compiler/test/i18n/extractor_merger_spec.ts
@@ -168,7 +168,13 @@ describe('Extractor', () => {
it('should handle blocks inside of translated elements', () => {
expect(extract('@if (cond) {main content} @else {else content}`'))
- .toEqual([[['[main content]', ' ', '[else content]'], 'a', 'b|c', '']]);
+ .toEqual([[
+ [
+ 'main content', ' ',
+ 'else content'
+ ],
+ 'a', 'b|c', ''
+ ]]);
});
});
diff --git a/packages/compiler/test/i18n/i18n_ast_spec.ts b/packages/compiler/test/i18n/i18n_ast_spec.ts
index 0ef344da2a2c8..59933948afdb4 100644
--- a/packages/compiler/test/i18n/i18n_ast_spec.ts
+++ b/packages/compiler/test/i18n/i18n_ast_spec.ts
@@ -8,11 +8,12 @@
import {createI18nMessageFactory} from '../../src/i18n/i18n_parser';
import {Node} from '../../src/ml_parser/ast';
+import {DEFAULT_CONTAINER_BLOCKS, DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/defaults';
import {HtmlParser} from '../../src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
describe('Message', () => {
- const messageFactory = createI18nMessageFactory(DEFAULT_INTERPOLATION_CONFIG);
+ const messageFactory =
+ createI18nMessageFactory(DEFAULT_INTERPOLATION_CONFIG, DEFAULT_CONTAINER_BLOCKS);
describe('messageText()', () => {
it('should serialize simple text', () => {
const message = messageFactory(parseHtml('abc\ndef'), '', '', '');
@@ -58,6 +59,15 @@ describe('Message', () => {
.toEqual(
`{VAR_SELECT_1, select, male {male of age: {VAR_SELECT, select, 10 {ten} 20 {twenty} 30 {thirty} other {other}}} female {female} other {other}}`);
});
+
+ it('should serialize blocks', () => {
+ const message = messageFactory(
+ parseHtml('abc @if (foo) {foo} @else if (bar) {bar} @else {baz} def'), '', '', '');
+
+ expect(message.messageString)
+ .toEqual(
+ 'abc {$START_BLOCK_IF}foo{$CLOSE_BLOCK_IF} {$START_BLOCK_ELSE_IF}bar{$CLOSE_BLOCK_ELSE_IF} {$START_BLOCK_ELSE}baz{$CLOSE_BLOCK_ELSE} def');
+ });
});
});
diff --git a/packages/compiler/test/i18n/i18n_parser_spec.ts b/packages/compiler/test/i18n/i18n_parser_spec.ts
index 0e2f29fb8f07f..881b9bcbaf32b 100644
--- a/packages/compiler/test/i18n/i18n_parser_spec.ts
+++ b/packages/compiler/test/i18n/i18n_parser_spec.ts
@@ -9,8 +9,8 @@
import {digest, serializeNodes} from '@angular/compiler/src/i18n/digest';
import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger';
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
+import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/defaults';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/interpolation_config';
describe('I18nParser', () => {
describe('elements', () => {
diff --git a/packages/compiler/test/i18n/integration_common.ts b/packages/compiler/test/i18n/integration_common.ts
index 53073f1162c28..4411ebb6ca261 100644
--- a/packages/compiler/test/i18n/integration_common.ts
+++ b/packages/compiler/test/i18n/integration_common.ts
@@ -9,8 +9,8 @@
import {NgLocalization} from '@angular/common';
import {Serializer} from '@angular/compiler/src/i18n';
import {MessageBundle} from '@angular/compiler/src/i18n/message_bundle';
+import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/defaults';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/interpolation_config';
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {Component, DebugElement, TRANSLATIONS, TRANSLATIONS_FORMAT} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
diff --git a/packages/compiler/test/i18n/message_bundle_spec.ts b/packages/compiler/test/i18n/message_bundle_spec.ts
index ddaa1b63b8386..926cf340ec7ff 100644
--- a/packages/compiler/test/i18n/message_bundle_spec.ts
+++ b/packages/compiler/test/i18n/message_bundle_spec.ts
@@ -10,8 +10,8 @@ import {serializeNodes} from '../../src/i18n/digest';
import * as i18n from '../../src/i18n/i18n_ast';
import {MessageBundle} from '../../src/i18n/message_bundle';
import {Serializer} from '../../src/i18n/serializers/serializer';
+import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/defaults';
import {HtmlParser} from '../../src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
describe('MessageBundle', () => {
describe('Messages', () => {
diff --git a/packages/compiler/test/i18n/serializers/placeholder_spec.ts b/packages/compiler/test/i18n/serializers/placeholder_spec.ts
index 5ca889f613c24..de40a0dcc414c 100644
--- a/packages/compiler/test/i18n/serializers/placeholder_spec.ts
+++ b/packages/compiler/test/i18n/serializers/placeholder_spec.ts
@@ -86,4 +86,16 @@ describe('PlaceholderRegistry', () => {
expect(reg.getPlaceholderName('name2', 'content')).toEqual('NAME2');
});
});
+
+ describe('block placeholders', () => {
+ it('should generate placeholders for a plain block', () => {
+ expect(reg.getStartBlockPlaceholderName('if', [])).toBe('START_BLOCK_IF');
+ expect(reg.getCloseBlockPlaceholderName('if')).toBe('CLOSE_BLOCK_IF');
+ });
+
+ it('should generate placeholders for a block with spaces in its name', () => {
+ expect(reg.getStartBlockPlaceholderName('else if', [])).toBe('START_BLOCK_ELSE_IF');
+ expect(reg.getCloseBlockPlaceholderName('else if')).toBe('CLOSE_BLOCK_ELSE_IF');
+ });
+ });
});
diff --git a/packages/compiler/test/i18n/serializers/xliff2_spec.ts b/packages/compiler/test/i18n/serializers/xliff2_spec.ts
index f89a416d50385..edd1d54f6a761 100644
--- a/packages/compiler/test/i18n/serializers/xliff2_spec.ts
+++ b/packages/compiler/test/i18n/serializers/xliff2_spec.ts
@@ -11,8 +11,8 @@ import {escapeRegExp} from '@angular/compiler/src/util';
import {serializeNodes} from '../../../src/i18n/digest';
import {MessageBundle} from '../../../src/i18n/message_bundle';
import {Xliff2} from '../../../src/i18n/serializers/xliff2';
+import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/defaults';
import {HtmlParser} from '../../../src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
const HTML = `
not translatable
@@ -26,6 +26,7 @@ const HTML = `
Test: { count, plural, =0 { { sex, select, other {
deeply nested
}} } =other {a lot}}
multi
lines
+translatable element @if (foo) {with} @else if (bar) {blocks}
`;
const WRITE_XLIFF = `
@@ -125,6 +126,14 @@ const WRITE_XLIFF = `
lines
+
+
+ file.ts:13
+
+
+
+
+
`;
@@ -238,6 +247,15 @@ lines
lignes
+
+
+ file.ts:13
+
+
+
+ élément traduisible avec des blocs
+
+
Please check the translation for 'namespace'. On also can use 'espace de nom',but I think most technical manuals use the English term.
@@ -260,26 +278,25 @@ lignes
`;
-(function() {
-const serializer = new Xliff2();
+describe('XLIFF 2.0 serializer', () => {
+ const serializer = new Xliff2();
-function toXliff(html: string, locale: string|null = null): string {
- const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
- catalog.updateFromTemplate(html, 'file.ts', DEFAULT_INTERPOLATION_CONFIG);
- return catalog.write(serializer);
-}
+ function toXliff(html: string, locale: string|null = null): string {
+ const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
+ catalog.updateFromTemplate(html, 'file.ts', DEFAULT_INTERPOLATION_CONFIG);
+ return catalog.write(serializer);
+ }
-function loadAsMap(xliff: string): {[id: string]: string} {
- const {i18nNodesByMsgId} = serializer.load(xliff, 'url');
+ function loadAsMap(xliff: string): {[id: string]: string} {
+ const {i18nNodesByMsgId} = serializer.load(xliff, 'url');
- const msgMap: {[id: string]: string} = {};
- Object.keys(i18nNodesByMsgId)
- .forEach(id => msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join(''));
+ const msgMap: {[id: string]: string} = {};
+ Object.keys(i18nNodesByMsgId)
+ .forEach(id => msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join(''));
- return msgMap;
-}
+ return msgMap;
+ }
-describe('XLIFF 2.0 serializer', () => {
describe('write', () => {
it('should write a valid xliff 2.0 file', () => {
expect(toXliff(HTML)).toEqual(WRITE_XLIFF);
@@ -309,6 +326,8 @@ describe('XLIFF 2.0 serializer', () => {
' name="START_PARAGRAPH"/>, profondément imbriqué, ]}}, ]}, =other {[beaucoup]}}',
'2340165783990709777': `multi
lignes`,
+ '6618832065070552029':
+ 'élément traduisible avec des blocs',
'mrk-test': 'Vous pouvez utiliser votre propre namespace.',
'mrk-test2': 'Vous pouvez utiliser votre propre namespace.'
});
@@ -439,4 +458,3 @@ lignes`,
});
});
});
-})();
diff --git a/packages/compiler/test/i18n/serializers/xliff_spec.ts b/packages/compiler/test/i18n/serializers/xliff_spec.ts
index 9acf397aa8686..dbc4c470a658d 100644
--- a/packages/compiler/test/i18n/serializers/xliff_spec.ts
+++ b/packages/compiler/test/i18n/serializers/xliff_spec.ts
@@ -11,8 +11,8 @@ import {escapeRegExp} from '@angular/compiler/src/util';
import {serializeNodes} from '../../../src/i18n/digest';
import {MessageBundle} from '../../../src/i18n/message_bundle';
import {Xliff} from '../../../src/i18n/serializers/xliff';
+import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/defaults';
import {HtmlParser} from '../../../src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
const HTML = `
not translatable
@@ -27,6 +27,7 @@ const HTML = `
Test: { count, plural, =0 { { sex, select, other {
deeply nested
}} } =other {a lot}}
multi
lines
+translatable element @if (foo) {with} @else if (bar) {blocks}
`;
const WRITE_XLIFF = `
@@ -120,6 +121,13 @@ lines
12
+
+
+
+ file.ts
+ 14
+
+