Skip to content

Commit f01b718

Browse files
crisbetoAndrewKushnir
authored andcommitted
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_<name>` and `CLOSE_BLOCK_<name>` placeholders. PR Close #52958
1 parent ac9cd61 commit f01b718

38 files changed

+333
-78
lines changed

packages/compiler/src/compiler.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export * from './version';
3838
export {CompilerConfig, preserveWhitespacesDefault} from './config';
3939
export * from './resource_loader';
4040
export {ConstantPool} from './constant_pool';
41-
export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
41+
export {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/defaults';
4242
export * from './schema/element_schema_registry';
4343
export * from './i18n/index';
4444
export * from './expression_parser/ast';
@@ -47,7 +47,6 @@ export * from './expression_parser/parser';
4747
export * from './ml_parser/ast';
4848
export * from './ml_parser/html_parser';
4949
export * from './ml_parser/html_tags';
50-
export * from './ml_parser/interpolation_config';
5150
export * from './ml_parser/tags';
5251
export {ParseTreeResult, TreeError} from './ml_parser/parser';
5352
export {LexerRange} from './ml_parser/lexer';

packages/compiler/src/expression_parser/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import * as chars from '../chars';
10-
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
10+
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/defaults';
1111
import {InterpolatedAttributeToken, InterpolatedTextToken, TokenType as MlParserTokenType} from '../ml_parser/tokens';
1212

1313
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';

packages/compiler/src/i18n/digest.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ class _SerializerVisitor implements i18n.Visitor {
8181
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
8282
return `<ph icu name="${ph.name}">${ph.value.visit(this)}</ph>`;
8383
}
84+
85+
visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context: any): any {
86+
return `<ph block name="${ph.startName}">${
87+
ph.children.map(child => child.visit(this)).join(', ')}</ph name="${ph.closeName}">`;
88+
}
8489
}
8590

8691
const serializerVisitor = new _SerializerVisitor();

packages/compiler/src/i18n/extractor_merger.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import * as html from '../ml_parser/ast';
10-
import {InterpolationConfig} from '../ml_parser/interpolation_config';
10+
import {DEFAULT_CONTAINER_BLOCKS, InterpolationConfig} from '../ml_parser/defaults';
1111
import {ParseTreeResult} from '../ml_parser/parser';
1212

1313
import * as i18n from './i18n_ast';
@@ -308,7 +308,8 @@ class _Visitor implements html.Visitor {
308308
this._errors = [];
309309
this._messages = [];
310310
this._inImplicitNode = false;
311-
this._createI18nMessage = createI18nMessageFactory(interpolationConfig);
311+
this._createI18nMessage =
312+
createI18nMessageFactory(interpolationConfig, DEFAULT_CONTAINER_BLOCKS);
312313
}
313314

314315
// looks for translatable attributes

packages/compiler/src/i18n/i18n_ast.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,17 @@ export class IcuPlaceholder implements Node {
129129
}
130130
}
131131

132+
export class BlockPlaceholder implements Node {
133+
constructor(
134+
public name: string, public parameters: string[], public startName: string,
135+
public closeName: string, public children: Node[], public sourceSpan: ParseSourceSpan,
136+
public startSourceSpan: ParseSourceSpan|null, public endSourceSpan: ParseSourceSpan|null) {}
137+
138+
visit(visitor: Visitor, context?: any): any {
139+
return visitor.visitBlockPlaceholder(this, context);
140+
}
141+
}
142+
132143
/**
133144
* Each HTML node that is affect by an i18n tag will also have an `i18n` property that is of type
134145
* `I18nMeta`.
@@ -144,6 +155,7 @@ export interface Visitor {
144155
visitTagPlaceholder(ph: TagPlaceholder, context?: any): any;
145156
visitPlaceholder(ph: Placeholder, context?: any): any;
146157
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any;
158+
visitBlockPlaceholder(ph: BlockPlaceholder, context?: any): any;
147159
}
148160

149161
// Clone the AST
@@ -178,6 +190,13 @@ export class CloneVisitor implements Visitor {
178190
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): IcuPlaceholder {
179191
return new IcuPlaceholder(ph.value, ph.name, ph.sourceSpan);
180192
}
193+
194+
visitBlockPlaceholder(ph: BlockPlaceholder, context?: any): BlockPlaceholder {
195+
const children = ph.children.map(n => n.visit(this, context));
196+
return new BlockPlaceholder(
197+
ph.name, ph.parameters, ph.startName, ph.closeName, children, ph.sourceSpan,
198+
ph.startSourceSpan, ph.endSourceSpan);
199+
}
181200
}
182201

183202
// Visit all the nodes recursively
@@ -201,6 +220,10 @@ export class RecurseVisitor implements Visitor {
201220
visitPlaceholder(ph: Placeholder, context?: any): any {}
202221

203222
visitIcuPlaceholder(ph: IcuPlaceholder, context?: any): any {}
223+
224+
visitBlockPlaceholder(ph: BlockPlaceholder, context?: any): any {
225+
ph.children.forEach(child => child.visit(this));
226+
}
204227
}
205228

206229

@@ -240,4 +263,9 @@ class LocalizeMessageStringVisitor implements Visitor {
240263
visitIcuPlaceholder(ph: IcuPlaceholder): any {
241264
return `{$${ph.name}}`;
242265
}
266+
267+
visitBlockPlaceholder(ph: BlockPlaceholder): any {
268+
const children = ph.children.map(child => child.visit(this)).join('');
269+
return `{$${ph.startName}}${children}{$${ph.closeName}}`;
270+
}
243271
}

packages/compiler/src/i18n/i18n_html_parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
import {MissingTranslationStrategy} from '../core';
10+
import {DEFAULT_INTERPOLATION_CONFIG} from '../ml_parser/defaults';
1011
import {HtmlParser} from '../ml_parser/html_parser';
11-
import {DEFAULT_INTERPOLATION_CONFIG} from '../ml_parser/interpolation_config';
1212
import {TokenizeOptions} from '../ml_parser/lexer';
1313
import {ParseTreeResult} from '../ml_parser/parser';
1414
import {Console} from '../util';

packages/compiler/src/i18n/i18n_parser.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import {Lexer as ExpressionLexer} from '../expression_parser/lexer';
1010
import {Parser as ExpressionParser} from '../expression_parser/parser';
1111
import * as html from '../ml_parser/ast';
12+
import {InterpolationConfig} from '../ml_parser/defaults';
1213
import {getHtmlTagDefinition} from '../ml_parser/html_tags';
13-
import {InterpolationConfig} from '../ml_parser/interpolation_config';
1414
import {InterpolatedAttributeToken, InterpolatedTextToken, TokenType} from '../ml_parser/tokens';
1515
import {ParseSourceSpan} from '../parse_util';
1616

@@ -29,9 +29,9 @@ export interface I18nMessageFactory {
2929
/**
3030
* Returns a function converting html nodes to an i18n Message given an interpolationConfig
3131
*/
32-
export function createI18nMessageFactory(interpolationConfig: InterpolationConfig):
33-
I18nMessageFactory {
34-
const visitor = new _I18nVisitor(_expParser, interpolationConfig);
32+
export function createI18nMessageFactory(
33+
interpolationConfig: InterpolationConfig, containerBlocks: Set<string>): I18nMessageFactory {
34+
const visitor = new _I18nVisitor(_expParser, interpolationConfig, containerBlocks);
3535
return (nodes, meaning, description, customId, visitNodeFn) =>
3636
visitor.toI18nMessage(nodes, meaning, description, customId, visitNodeFn);
3737
}
@@ -52,7 +52,9 @@ function noopVisitNodeFn(_html: html.Node, i18n: i18n.Node): i18n.Node {
5252
class _I18nVisitor implements html.Visitor {
5353
constructor(
5454
private _expressionParser: ExpressionParser,
55-
private _interpolationConfig: InterpolationConfig) {}
55+
private _interpolationConfig: InterpolationConfig,
56+
private _containerBlocks: Set<string>,
57+
) {}
5658

5759
public toI18nMessage(
5860
nodes: html.Node[], meaning = '', description = '', customId = '',
@@ -164,11 +166,35 @@ class _I18nVisitor implements html.Visitor {
164166

165167
visitBlock(block: html.Block, context: I18nMessageVisitorContext) {
166168
const children = html.visitAll(this, block.children, context);
167-
const node = new i18n.Container(children, block.sourceSpan);
169+
170+
if (this._containerBlocks.has(block.name)) {
171+
return new i18n.Container(children, block.sourceSpan);
172+
}
173+
174+
const parameters = block.parameters.map(param => param.expression);
175+
const startPhName =
176+
context.placeholderRegistry.getStartBlockPlaceholderName(block.name, parameters);
177+
const closePhName = context.placeholderRegistry.getCloseBlockPlaceholderName(block.name);
178+
179+
context.placeholderToContent[startPhName] = {
180+
text: block.startSourceSpan.toString(),
181+
sourceSpan: block.startSourceSpan,
182+
};
183+
184+
context.placeholderToContent[closePhName] = {
185+
text: block.endSourceSpan ? block.endSourceSpan.toString() : '}',
186+
sourceSpan: block.endSourceSpan ?? block.sourceSpan,
187+
};
188+
189+
const node = new i18n.BlockPlaceholder(
190+
block.name, parameters, startPhName, closePhName, children, block.sourceSpan,
191+
block.startSourceSpan, block.endSourceSpan);
168192
return context.visitNodeFn(block, node);
169193
}
170194

171-
visitBlockParameter(_parameter: html.BlockParameter, _context: I18nMessageVisitorContext) {}
195+
visitBlockParameter(_parameter: html.BlockParameter, _context: I18nMessageVisitorContext) {
196+
throw new Error('Unreachable code');
197+
}
172198

173199
/**
174200
* Convert, text and interpolated tokens up into text and placeholder pieces.

packages/compiler/src/i18n/message_bundle.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {InterpolationConfig} from '../ml_parser/defaults';
910
import {HtmlParser} from '../ml_parser/html_parser';
10-
import {InterpolationConfig} from '../ml_parser/interpolation_config';
1111
import {ParseError} from '../parse_util';
1212

1313
import {extractMessages} from './extractor_merger';
@@ -99,6 +99,16 @@ class MapPlaceholderNames extends i18n.CloneVisitor {
9999
ph.startSourceSpan, ph.endSourceSpan);
100100
}
101101

102+
override visitBlockPlaceholder(ph: i18n.BlockPlaceholder, mapper: PlaceholderMapper):
103+
i18n.BlockPlaceholder {
104+
const startName = mapper.toPublicName(ph.startName)!;
105+
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName)! : ph.closeName;
106+
const children = ph.children.map(n => n.visit(this, mapper));
107+
return new i18n.BlockPlaceholder(
108+
ph.name, ph.parameters, startName, closeName, children, ph.sourceSpan, ph.startSourceSpan,
109+
ph.endSourceSpan);
110+
}
111+
102112
override visitPlaceholder(ph: i18n.Placeholder, mapper: PlaceholderMapper): i18n.Placeholder {
103113
return new i18n.Placeholder(ph.value, mapper.toPublicName(ph.name)!, ph.sourceSpan);
104114
}

packages/compiler/src/i18n/serializers/placeholder.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,29 @@ export class PlaceholderRegistry {
9797
return this._generateUniqueName(name.toUpperCase());
9898
}
9999

100+
getStartBlockPlaceholderName(name: string, parameters: string[]): string {
101+
const signature = this._hashBlock(name, parameters);
102+
if (this._signatureToName[signature]) {
103+
return this._signatureToName[signature];
104+
}
105+
106+
const placeholder = this._generateUniqueName(`START_BLOCK_${this._toSnakeCase(name)}`);
107+
this._signatureToName[signature] = placeholder;
108+
return placeholder;
109+
}
110+
111+
getCloseBlockPlaceholderName(name: string): string {
112+
const signature = this._hashClosingBlock(name);
113+
if (this._signatureToName[signature]) {
114+
return this._signatureToName[signature];
115+
}
116+
117+
const placeholder = this._generateUniqueName(`CLOSE_BLOCK_${this._toSnakeCase(name)}`);
118+
this._signatureToName[signature] = placeholder;
119+
return placeholder;
120+
}
121+
122+
100123
// Generate a hash for a tag - does not take attribute order into account
101124
private _hashTag(tag: string, attrs: {[k: string]: string}, isVoid: boolean): string {
102125
const start = `<${tag}`;
@@ -110,6 +133,19 @@ export class PlaceholderRegistry {
110133
return this._hashTag(`/${tag}`, {}, false);
111134
}
112135

136+
private _hashBlock(name: string, parameters: string[]): string {
137+
const params = parameters.length === 0 ? '' : ` (${parameters.sort().join('; ')})`;
138+
return `@${name}${params} {}`;
139+
}
140+
141+
private _hashClosingBlock(name: string): string {
142+
return this._hashBlock(`close_${name}`, []);
143+
}
144+
145+
private _toSnakeCase(name: string) {
146+
return name.toUpperCase().replace(/[^A-Z0-9]/g, '_');
147+
}
148+
113149
private _generateUniqueName(base: string): string {
114150
const seen = this._placeHolderNameCounts.hasOwnProperty(base);
115151
if (!seen) {

packages/compiler/src/i18n/serializers/serializer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ export class SimplePlaceholderMapper extends i18n.RecurseVisitor implements Plac
7777
this.visitPlaceholderName(ph.name);
7878
}
7979

80+
override visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): any {
81+
this.visitPlaceholderName(ph.startName);
82+
super.visitBlockPlaceholder(ph, context);
83+
this.visitPlaceholderName(ph.closeName);
84+
}
85+
8086
override visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
8187
this.visitPlaceholderName(ph.name);
8288
}

0 commit comments

Comments
 (0)