refactor(compiler): move handling of translations to the ConstantPool (#22942)

PR Close #22942
This commit is contained in:
Victor Berchet 2018-03-22 15:03:06 -07:00 committed by Matias Niemelä
parent d98e9e7c7f
commit bcaa07b0ac
3 changed files with 111 additions and 94 deletions

View File

@ -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,26 +256,37 @@ 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);
}

View File

@ -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);
}

View File

@ -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,27 +84,29 @@ 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$];
}; };
/**
* @desc desc
* @meaning meaning
*/
const $msg_2$ = goog.getMsg('Hello world');
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 desc $r3$.ɵT(1, $msg_2$);
*/
const $g1$ = goog.getMsg('introduction');
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $g1$));
/**
* @desc desc
* @meaning meaning
*/
const $g2$ = goog.getMsg('Hello world');
$r3$.ɵT(1, $g2$);
$r3$.ɵe(); $r3$.ɵe();
} }
} }
`; `;
const result = compile(files, angularFiles); const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
@ -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();
} }
} }