refactor(compiler): move handling of translations to the ConstantPool (#22942)
PR Close #22942
This commit is contained in:
parent
d98e9e7c7f
commit
bcaa07b0ac
|
@ -11,8 +11,16 @@ import {OutputContext, error} from './util';
|
||||||
|
|
||||||
const CONSTANT_PREFIX = '_c';
|
const CONSTANT_PREFIX = '_c';
|
||||||
|
|
||||||
|
// Closure variables holding messages must be named `MSG_[A-Z0-9]+`
|
||||||
|
const TRANSLATION_PREFIX = 'MSG_';
|
||||||
|
|
||||||
export const enum DefinitionKind {Injector, Directive, Component, Pipe}
|
export const enum DefinitionKind {Injector, Directive, Component, Pipe}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closure uses `goog.getMsg(message)` to lookup translations
|
||||||
|
*/
|
||||||
|
const GOOG_GET_MSG = 'goog.getMsg';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context to use when producing a key.
|
* Context to use when producing a key.
|
||||||
*
|
*
|
||||||
|
@ -68,6 +76,7 @@ class FixupExpression extends o.Expression {
|
||||||
*/
|
*/
|
||||||
export class ConstantPool {
|
export class ConstantPool {
|
||||||
statements: o.Statement[] = [];
|
statements: o.Statement[] = [];
|
||||||
|
private translations = new Map<string, o.Expression>();
|
||||||
private literals = new Map<string, FixupExpression>();
|
private literals = new Map<string, FixupExpression>();
|
||||||
private literalFactories = new Map<string, o.Expression>();
|
private literalFactories = new Map<string, o.Expression>();
|
||||||
private injectorDefinitions = new Map<any, FixupExpression>();
|
private injectorDefinitions = new Map<any, FixupExpression>();
|
||||||
|
@ -103,6 +112,40 @@ export class ConstantPool {
|
||||||
return fixup;
|
return fixup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generates closure specific code for translation.
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// /**
|
||||||
|
// * @desc description?
|
||||||
|
// * @meaning meaning?
|
||||||
|
// */
|
||||||
|
// const MSG_XYZ = goog.getMsg('message');
|
||||||
|
// ```
|
||||||
|
getTranslation(message: string, meta: {description?: string, meaning?: string}): o.Expression {
|
||||||
|
// The identity of an i18n message depends on the message and its meaning
|
||||||
|
const key = meta.meaning ? `${message}\u0000\u0000${meta.meaning}` : message;
|
||||||
|
|
||||||
|
const exp = this.translations.get(key);
|
||||||
|
|
||||||
|
if (exp) {
|
||||||
|
return exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docStmt = i18nMetaToDocStmt(meta);
|
||||||
|
if (docStmt) {
|
||||||
|
this.statements.push(docStmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call closure to get the translation
|
||||||
|
const variable = o.variable(this.freshTranslationName());
|
||||||
|
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]);
|
||||||
|
const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||||
|
this.statements.push(msgStmt);
|
||||||
|
|
||||||
|
this.translations.set(key, variable);
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext, forceShared: boolean = false):
|
getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext, forceShared: boolean = false):
|
||||||
o.Expression {
|
o.Expression {
|
||||||
const definitions = this.definitionsOf(kind);
|
const definitions = this.definitionsOf(kind);
|
||||||
|
@ -213,25 +256,36 @@ export class ConstantPool {
|
||||||
|
|
||||||
private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }
|
private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }
|
||||||
|
|
||||||
|
private freshTranslationName(): string {
|
||||||
|
return this.uniqueName(TRANSLATION_PREFIX).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
private keyOf(expression: o.Expression) {
|
private keyOf(expression: o.Expression) {
|
||||||
return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT);
|
return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor used to determine if 2 expressions are equivalent and can be shared in the
|
||||||
|
* `ConstantPool`.
|
||||||
|
*
|
||||||
|
* When the id (string) generated by the visitor is equal, expressions are considered equivalent.
|
||||||
|
*/
|
||||||
class KeyVisitor implements o.ExpressionVisitor {
|
class KeyVisitor implements o.ExpressionVisitor {
|
||||||
visitLiteralExpr(ast: o.LiteralExpr): string {
|
visitLiteralExpr(ast: o.LiteralExpr): string {
|
||||||
return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`;
|
return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string {
|
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string {
|
||||||
return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`;
|
return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string {
|
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string {
|
||||||
const mapKey =
|
const mapKey = (entry: o.LiteralMapEntry) => {
|
||||||
(entry: o.LiteralMapEntry) => {
|
|
||||||
const quote = entry.quoted ? '"' : '';
|
const quote = entry.quoted ? '"' : '';
|
||||||
return `${quote}${entry.key}${quote}`;
|
return `${quote}${entry.key}${quote}`;
|
||||||
} const mapEntry = (entry: o.LiteralMapEntry) =>
|
};
|
||||||
|
const mapEntry = (entry: o.LiteralMapEntry) =>
|
||||||
`${mapKey(entry)}:${entry.value.visitExpression(this, context)}`;
|
`${mapKey(entry)}:${entry.value.visitExpression(this, context)}`;
|
||||||
return `{${ast.entries.map(mapEntry).join(',')}`;
|
return `{${ast.entries.map(mapEntry).join(',')}`;
|
||||||
}
|
}
|
||||||
|
@ -241,13 +295,7 @@ class KeyVisitor implements o.ExpressionVisitor {
|
||||||
`EX:${ast.value.runtime.name}`;
|
`EX:${ast.value.runtime.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitReadVarExpr(ast: o.ReadVarExpr): string {
|
visitReadVarExpr = invalid;
|
||||||
if (!ast.name) {
|
|
||||||
invalid(ast);
|
|
||||||
}
|
|
||||||
return ast.name as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
visitWriteVarExpr = invalid;
|
visitWriteVarExpr = invalid;
|
||||||
visitWriteKeyExpr = invalid;
|
visitWriteKeyExpr = invalid;
|
||||||
visitWritePropExpr = invalid;
|
visitWritePropExpr = invalid;
|
||||||
|
@ -273,3 +321,20 @@ function invalid<T>(arg: o.Expression | o.Statement): never {
|
||||||
function isVariable(e: o.Expression): e is o.ReadVarExpr {
|
function isVariable(e: o.Expression): e is o.ReadVarExpr {
|
||||||
return e instanceof o.ReadVarExpr;
|
return e instanceof o.ReadVarExpr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts i18n meta informations for a message (description, meaning) to a JsDoc statement
|
||||||
|
// formatted as expected by the Closure compiler.
|
||||||
|
function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}):
|
||||||
|
o.JSDocCommentStmt|null {
|
||||||
|
const tags: o.JSDocTag[] = [];
|
||||||
|
|
||||||
|
if (meta.description) {
|
||||||
|
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.meaning) {
|
||||||
|
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||||
|
}
|
||||||
|
|
|
@ -46,9 +46,6 @@ const I18N_ATTR_PREFIX = 'i18n-';
|
||||||
const MEANING_SEPARATOR = '|';
|
const MEANING_SEPARATOR = '|';
|
||||||
const ID_SEPARATOR = '@@';
|
const ID_SEPARATOR = '@@';
|
||||||
|
|
||||||
/** Closure functions **/
|
|
||||||
const GOOG_GET_MSG = 'goog.getMsg';
|
|
||||||
|
|
||||||
export function compileDirective(
|
export function compileDirective(
|
||||||
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
|
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
|
||||||
bindingParser: BindingParser, mode: OutputMode) {
|
bindingParser: BindingParser, mode: OutputMode) {
|
||||||
|
@ -317,12 +314,6 @@ class BindingScope {
|
||||||
const ref = `${REFERENCE_PREFIX}${current.referenceNameIndex++}`;
|
const ref = `${REFERENCE_PREFIX}${current.referenceNameIndex++}`;
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
// closure variables holding i18n messages are name `MSG_[A-Z0-9]+`
|
|
||||||
freshI18nName(): string {
|
|
||||||
const name = this.freshReferenceName();
|
|
||||||
return `MSG_${name}`.toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROOT_SCOPE = new BindingScope(null).set('$event', o.variable('$event'));
|
const ROOT_SCOPE = new BindingScope(null).set('$event', o.variable('$event'));
|
||||||
|
@ -573,8 +564,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
attributes.push(o.literal(name));
|
attributes.push(o.literal(name));
|
||||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||||
hasI18nAttr = true;
|
hasI18nAttr = true;
|
||||||
const {statements, variable} = this.genI18nMessageStmts(value, attrI18nMetas[name]);
|
const meta = parseI18nMeta(attrI18nMetas[name]);
|
||||||
i18nMessages.push(...statements);
|
const variable = this.constantPool.getTranslation(value, meta);
|
||||||
attributes.push(variable);
|
attributes.push(variable);
|
||||||
} else {
|
} else {
|
||||||
attributes.push(o.literal(value));
|
attributes.push(o.literal(value));
|
||||||
|
@ -790,8 +781,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
// i0.ɵT(1, MSG_XYZ);
|
// i0.ɵT(1, MSG_XYZ);
|
||||||
// ```
|
// ```
|
||||||
visitSingleI18nTextChild(text: TextAst, i18nMeta: string) {
|
visitSingleI18nTextChild(text: TextAst, i18nMeta: string) {
|
||||||
const {statements, variable} = this.genI18nMessageStmts(text.value, i18nMeta);
|
const meta = parseI18nMeta(i18nMeta);
|
||||||
this._creationMode.push(...statements);
|
const variable = this.constantPool.getTranslation(text.value, meta);
|
||||||
this.instruction(
|
this.instruction(
|
||||||
this._creationMode, text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), variable);
|
this._creationMode, text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), variable);
|
||||||
}
|
}
|
||||||
|
@ -835,35 +826,6 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression {
|
private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression {
|
||||||
return this.convertPropertyBinding(implicit, value);
|
return this.convertPropertyBinding(implicit, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transforms an i18n message into a const declaration.
|
|
||||||
//
|
|
||||||
// `message`
|
|
||||||
// becomes
|
|
||||||
// ```
|
|
||||||
// /**
|
|
||||||
// * @desc description?
|
|
||||||
// * @meaning meaning?
|
|
||||||
// */
|
|
||||||
// const MSG_XYZ = goog.getMsg('message');
|
|
||||||
// ```
|
|
||||||
private genI18nMessageStmts(msg: string, meta: string):
|
|
||||||
{statements: o.Statement[], variable: o.ReadVarExpr} {
|
|
||||||
const statements: o.Statement[] = [];
|
|
||||||
const m = parseI18nMeta(meta);
|
|
||||||
const docStmt = i18nMetaToDocStmt(m);
|
|
||||||
if (docStmt) {
|
|
||||||
statements.push(docStmt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call closure to get the translation
|
|
||||||
const variable = o.variable(this.bindingScope.freshI18nName());
|
|
||||||
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(msg)]);
|
|
||||||
const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
|
||||||
statements.push(msgStmt);
|
|
||||||
|
|
||||||
return {statements, variable};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryPredicate(query: CompileQueryMetadata, outputCtx: OutputContext): o.Expression {
|
function getQueryPredicate(query: CompileQueryMetadata, outputCtx: OutputContext): o.Expression {
|
||||||
|
@ -1221,20 +1183,3 @@ function parseI18nMeta(i18n?: string): {description?: string, id?: string, meani
|
||||||
|
|
||||||
return {description, id, meaning};
|
return {description, id, meaning};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts i18n meta informations for a message (description, meaning) to a JsDoc statement
|
|
||||||
// formatted as expected by the Closure compiler.
|
|
||||||
function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}):
|
|
||||||
o.JSDocCommentStmt|null {
|
|
||||||
const tags: o.JSDocTag[] = [];
|
|
||||||
|
|
||||||
if (meta.description) {
|
|
||||||
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.meaning) {
|
|
||||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
|
||||||
}
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ describe('i18n support in the view compiler', () => {
|
||||||
<div i18n>Hello world</div>
|
<div i18n>Hello world</div>
|
||||||
<div>&</div>
|
<div>&</div>
|
||||||
<div i18n>farewell</div>
|
<div i18n>farewell</div>
|
||||||
|
<div i18n>farewell</div>
|
||||||
\`
|
\`
|
||||||
})
|
})
|
||||||
export class MyComponent {}
|
export class MyComponent {}
|
||||||
|
@ -40,16 +41,19 @@ describe('i18n support in the view compiler', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
|
const $msg_1$ = goog.getMsg('Hello world');
|
||||||
|
const $msg_2$ = goog.getMsg('farewell');
|
||||||
|
…
|
||||||
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
||||||
if (cm) {
|
if (cm) {
|
||||||
…
|
…
|
||||||
const $g2$ = goog.getMsg('Hello world');
|
$r3$.ɵT(1, $msg_1$);
|
||||||
$r3$.ɵT(1, $g2$);
|
|
||||||
…
|
…
|
||||||
$r3$.ɵT(3,'&');
|
$r3$.ɵT(3,'&');
|
||||||
…
|
…
|
||||||
const $g3$ = goog.getMsg('farewell');
|
$r3$.ɵT(5, $msg_2$);
|
||||||
$r3$.ɵT(5, $g3$);
|
…
|
||||||
|
$r3$.ɵT(7, $msg_2$);
|
||||||
…
|
…
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,23 +84,25 @@ describe('i18n support in the view compiler', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
|
/**
|
||||||
|
* @desc desc
|
||||||
|
*/
|
||||||
|
const $msg_1$ = goog.getMsg('introduction');
|
||||||
|
…
|
||||||
const $c1$ = ($a1$:any) => {
|
const $c1$ = ($a1$:any) => {
|
||||||
return ['title', $a1$];
|
return ['title', $a1$];
|
||||||
};
|
};
|
||||||
…
|
…
|
||||||
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
|
||||||
if (cm) {
|
|
||||||
/**
|
|
||||||
* @desc desc
|
|
||||||
*/
|
|
||||||
const $g1$ = goog.getMsg('introduction');
|
|
||||||
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $g1$));
|
|
||||||
/**
|
/**
|
||||||
* @desc desc
|
* @desc desc
|
||||||
* @meaning meaning
|
* @meaning meaning
|
||||||
*/
|
*/
|
||||||
const $g2$ = goog.getMsg('Hello world');
|
const $msg_2$ = goog.getMsg('Hello world');
|
||||||
$r3$.ɵT(1, $g2$);
|
…
|
||||||
|
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
||||||
|
if (cm) {
|
||||||
|
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $msg_1$));
|
||||||
|
$r3$.ɵT(1, $msg_2$);
|
||||||
$r3$.ɵe();
|
$r3$.ɵe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,18 +135,19 @@ describe('i18n support in the view compiler', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
|
/**
|
||||||
|
* @desc d
|
||||||
|
* @meaning m
|
||||||
|
*/
|
||||||
|
const $msg_1$ = goog.getMsg('introduction');
|
||||||
|
…
|
||||||
const $c1$ = ($a1$:any) => {
|
const $c1$ = ($a1$:any) => {
|
||||||
return ['id', 'static', 'title', $a1$];
|
return ['id', 'static', 'title', $a1$];
|
||||||
};
|
};
|
||||||
…
|
…
|
||||||
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
||||||
if (cm) {
|
if (cm) {
|
||||||
/**
|
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $msg_1$));
|
||||||
* @desc d
|
|
||||||
* @meaning m
|
|
||||||
*/
|
|
||||||
const $g1$ = goog.getMsg('introduction');
|
|
||||||
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $g1$));
|
|
||||||
$r3$.ɵe();
|
$r3$.ɵe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue