Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 77 additions & 8 deletions packages/compiler/src/constant_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -68,6 +76,7 @@ class FixupExpression extends o.Expression {
*/
export class ConstantPool {
statements: o.Statement[] = [];
private translations = new Map<string, o.Expression>();
private literals = new Map<string, FixupExpression>();
private literalFactories = new Map<string, o.Expression>();
private injectorDefinitions = new Map<any, FixupExpression>();
Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about id? Did you ask the closure team?

// 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);
Expand Down Expand Up @@ -213,22 +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) =>
`${entry.key}:${entry.value.visitExpression(this, context)}`;
`${mapKey(entry)}:${entry.value.visitExpression(this, context)}`;
return `{${ast.entries.map(mapEntry).join(',')}`;
}

Expand All @@ -237,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;
Expand All @@ -269,3 +321,20 @@ function invalid<T>(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);
}
63 changes: 4 additions & 59 deletions packages/compiler/src/render3/r3_view_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
51 changes: 29 additions & 22 deletions packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('i18n support in the view compiler', () => {
<div i18n>Hello world</div>
<div>&</div>
<div i18n>farewell</div>
<div i18n>farewell</div>
\`
})
export class MyComponent {}
Expand All @@ -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$);
}
}
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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();
}
}
Expand Down