diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index c13b909e13..794ba9e240 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -11,8 +11,16 @@ import {OutputContext, error} from './util'; 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} +/** + * Closure uses `goog.getMsg(message)` to lookup translations + */ +const GOOG_GET_MSG = 'goog.getMsg'; + /** * Context to use when producing a key. * @@ -68,6 +76,7 @@ class FixupExpression extends o.Expression { */ export class ConstantPool { statements: o.Statement[] = []; + private translations = new Map(); private literals = new Map(); private literalFactories = new Map(); private injectorDefinitions = new Map(); @@ -103,6 +112,40 @@ export class ConstantPool { 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): o.Expression { const definitions = this.definitionsOf(kind); @@ -213,26 +256,37 @@ export class ConstantPool { private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); } + private freshTranslationName(): string { + return this.uniqueName(TRANSLATION_PREFIX).toUpperCase(); + } + private keyOf(expression: o.Expression) { 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 { visitLiteralExpr(ast: o.LiteralExpr): string { return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`; } + visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string { return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`; } visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string { - const mapKey = - (entry: o.LiteralMapEntry) => { - const quote = entry.quoted ? '"' : ''; - return `${quote}${entry.key}${quote}`; - } const mapEntry = (entry: o.LiteralMapEntry) => - `${mapKey(entry)}:${entry.value.visitExpression(this, context)}`; + const mapKey = (entry: o.LiteralMapEntry) => { + const quote = entry.quoted ? '"' : ''; + return `${quote}${entry.key}${quote}`; + }; + const mapEntry = (entry: o.LiteralMapEntry) => + `${mapKey(entry)}:${entry.value.visitExpression(this, context)}`; return `{${ast.entries.map(mapEntry).join(',')}`; } @@ -241,13 +295,7 @@ class KeyVisitor implements o.ExpressionVisitor { `EX:${ast.value.runtime.name}`; } - visitReadVarExpr(ast: o.ReadVarExpr): string { - if (!ast.name) { - invalid(ast); - } - return ast.name as string; - } - + visitReadVarExpr = invalid; visitWriteVarExpr = invalid; visitWriteKeyExpr = invalid; visitWritePropExpr = invalid; @@ -273,3 +321,20 @@ function invalid(arg: o.Expression | o.Statement): never { function isVariable(e: o.Expression): e is 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); +} diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index 6f372768b1..1dfafd5f5a 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -46,9 +46,6 @@ const I18N_ATTR_PREFIX = 'i18n-'; const MEANING_SEPARATOR = '|'; const ID_SEPARATOR = '@@'; -/** Closure functions **/ -const GOOG_GET_MSG = 'goog.getMsg'; - export function compileDirective( outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector, bindingParser: BindingParser, mode: OutputMode) { @@ -317,12 +314,6 @@ class BindingScope { const ref = `${REFERENCE_PREFIX}${current.referenceNameIndex++}`; 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')); @@ -573,8 +564,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { attributes.push(o.literal(name)); if (attrI18nMetas.hasOwnProperty(name)) { hasI18nAttr = true; - const {statements, variable} = this.genI18nMessageStmts(value, attrI18nMetas[name]); - i18nMessages.push(...statements); + const meta = parseI18nMeta(attrI18nMetas[name]); + const variable = this.constantPool.getTranslation(value, meta); attributes.push(variable); } else { attributes.push(o.literal(value)); @@ -790,8 +781,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver { // i0.ɵT(1, MSG_XYZ); // ``` visitSingleI18nTextChild(text: TextAst, i18nMeta: string) { - const {statements, variable} = this.genI18nMessageStmts(text.value, i18nMeta); - this._creationMode.push(...statements); + const meta = parseI18nMeta(i18nMeta); + const variable = this.constantPool.getTranslation(text.value, meta); this.instruction( 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 { 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 { @@ -1221,20 +1183,3 @@ function parseI18nMeta(i18n?: string): {description?: string, id?: string, meani 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); -} diff --git a/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts b/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts index 5f5f5ada68..082907958d 100644 --- a/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts @@ -29,6 +29,7 @@ describe('i18n support in the view compiler', () => {
Hello world
&
farewell
+
farewell
\` }) export class MyComponent {} @@ -40,16 +41,19 @@ describe('i18n support in the view compiler', () => { }; const template = ` + const $msg_1$ = goog.getMsg('Hello world'); + const $msg_2$ = goog.getMsg('farewell'); + … template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { … - const $g2$ = goog.getMsg('Hello world'); - $r3$.ɵT(1, $g2$); + $r3$.ɵT(1, $msg_1$); … $r3$.ɵT(3,'&'); … - const $g3$ = goog.getMsg('farewell'); - $r3$.ɵT(5, $g3$); + $r3$.ɵT(5, $msg_2$); + … + $r3$.ɵT(7, $msg_2$); … } } @@ -80,27 +84,29 @@ describe('i18n support in the view compiler', () => { }; const template = ` + /** + * @desc desc + */ + const $msg_1$ = goog.getMsg('introduction'); + … const $c1$ = ($a1$:any) => { return ['title', $a1$]; }; … + /** + * @desc desc + * @meaning meaning + */ + const $msg_2$ = goog.getMsg('Hello world'); + … 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 - * @meaning meaning - */ - const $g2$ = goog.getMsg('Hello world'); - $r3$.ɵT(1, $g2$); + $r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $msg_1$)); + $r3$.ɵT(1, $msg_2$); $r3$.ɵe(); } } - `; + `; const result = compile(files, angularFiles); expectEmit(result.source, template, 'Incorrect template'); @@ -129,18 +135,19 @@ describe('i18n support in the view compiler', () => { }; const template = ` + /** + * @desc d + * @meaning m + */ + const $msg_1$ = goog.getMsg('introduction'); + … const $c1$ = ($a1$:any) => { return ['id', 'static', 'title', $a1$]; }; … template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - /** - * @desc d - * @meaning m - */ - const $g1$ = goog.getMsg('introduction'); - $r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $g1$)); + $r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $msg_1$)); $r3$.ɵe(); } }