feat(ivy): i18n compiler support for i18nStart and i18nEnd instructions (#26442)

PR Close #26442
This commit is contained in:
Andrew Kushnir
2018-10-17 11:03:52 -07:00
committed by Misko Hevery
parent 8024857d4c
commit 8a3fd58cad
8 changed files with 913 additions and 232 deletions
File diff suppressed because it is too large Load Diff
+39 -18
View File
@@ -7,6 +7,7 @@
*/
import * as o from './output/output_ast';
import {I18nMeta, parseI18nMeta} from './render3/view/i18n';
import {OutputContext, error} from './util';
const CONSTANT_PREFIX = '_c';
@@ -78,6 +79,7 @@ class FixupExpression extends o.Expression {
export class ConstantPool {
statements: o.Statement[] = [];
private translations = new Map<string, o.Expression>();
private deferredTranslations = new Map<o.ReadVarExpr, number>();
private literals = new Map<string, FixupExpression>();
private literalFactories = new Map<string, o.Expression>();
private injectorDefinitions = new Map<any, FixupExpression>();
@@ -113,6 +115,31 @@ export class ConstantPool {
return fixup;
}
getDeferredTranslationConst(suffix: string): o.ReadVarExpr {
const index = this.statements.push(new o.ExpressionStatement(o.NULL_EXPR)) - 1;
const variable = o.variable(this.freshTranslationName(suffix));
this.deferredTranslations.set(variable, index);
return variable;
}
setDeferredTranslationConst(variable: o.ReadVarExpr, message: string): void {
const index = this.deferredTranslations.get(variable) !;
this.statements[index] = this.getTranslationDeclStmt(variable, message);
}
getTranslationDeclStmt(variable: o.ReadVarExpr, message: string): o.DeclareVarStmt {
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(message)]);
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
}
appendTranslationMeta(meta: string|I18nMeta) {
const parsedMeta = typeof meta === 'string' ? parseI18nMeta(meta) : meta;
const docStmt = i18nMetaToDocStmt(parsedMeta);
if (docStmt) {
this.statements.push(docStmt);
}
}
// Generates closure specific code for translation.
//
// ```
@@ -122,10 +149,11 @@ export class ConstantPool {
// */
// const MSG_XYZ = goog.getMsg('message');
// ```
getTranslation(message: string, meta: {description?: string, meaning?: string}, suffix: string):
o.Expression {
getTranslation(message: string, meta: string, suffix: string): o.Expression {
const parsedMeta = parseI18nMeta(meta);
// The identity of an i18n message depends on the message and its meaning
const key = meta.meaning ? `${message}\u0000\u0000${meta.meaning}` : message;
const key = parsedMeta.meaning ? `${message}\u0000\u0000${parsedMeta.meaning}` : message;
const exp = this.translations.get(key);
@@ -133,16 +161,9 @@ export class ConstantPool {
return exp;
}
const docStmt = i18nMetaToDocStmt(meta);
if (docStmt) {
this.statements.push(docStmt);
}
// Call closure to get the translation
const variable = o.variable(this.freshTranslationName(suffix));
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.appendTranslationMeta(parsedMeta);
this.statements.push(this.getTranslationDeclStmt(variable, message));
this.translations.set(key, variable);
return variable;
@@ -330,14 +351,14 @@ 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 {
// Converts i18n meta informations for a message (id, description, meaning)
// to a JsDoc statement formatted as expected by the Closure compiler.
function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
const tags: o.JSDocTag[] = [];
if (meta.description) {
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
if (meta.id || meta.description) {
const text = meta.id ? `[BACKUP_MESSAGE_ID:${meta.id}] ${meta.description}` : meta.description;
tags.push({tagName: o.JSDocTagName.Desc, text: text !.trim()});
}
if (meta.meaning) {
@@ -205,8 +205,8 @@ export function compileComponentFromMetadata(
const template = meta.template;
const templateBuilder = new TemplateDefinitionBuilder(
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, templateName, meta.viewQueries,
directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, null, null, templateName,
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
meta.template.relativeContextFilePath);
const templateFunctionExpression = templateBuilder.buildTemplateFunction(
+140
View File
@@ -0,0 +1,140 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '../../output/output_ast';
/** I18n separators for metadata **/
const I18N_MEANING_SEPARATOR = '|';
const I18N_ID_SEPARATOR = '@@';
/** Name of the i18n attributes **/
export const I18N_ATTR = 'i18n';
export const I18N_ATTR_PREFIX = 'i18n-';
/** Placeholder wrapper for i18n expressions **/
export const I18N_PLACEHOLDER_SYMBOL = '�';
// Parse i18n metas like:
// - "@@id",
// - "description[@@id]",
// - "meaning|description[@@id]"
export function parseI18nMeta(meta?: string): I18nMeta {
let id: string|undefined;
let meaning: string|undefined;
let description: string|undefined;
if (meta) {
const idIndex = meta.indexOf(I18N_ID_SEPARATOR);
const descIndex = meta.indexOf(I18N_MEANING_SEPARATOR);
let meaningAndDesc: string;
[meaningAndDesc, id] =
(idIndex > -1) ? [meta.slice(0, idIndex), meta.slice(idIndex + 2)] : [meta, ''];
[meaning, description] = (descIndex > -1) ?
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
['', meaningAndDesc];
}
return {id, meaning, description};
}
export function isI18NAttribute(name: string): boolean {
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
}
export function wrapI18nPlaceholder(content: string | number, contextId: number = 0): string {
const blockId = contextId > 0 ? `:${contextId}` : '';
return `${I18N_PLACEHOLDER_SYMBOL}${content}${blockId}${I18N_PLACEHOLDER_SYMBOL}`;
}
export function assembleI18nBoundString(
strings: Array<string>, bindingStartIndex: number = 0, contextId: number = 0): string {
if (!strings.length) return '';
let acc = '';
const lastIdx = strings.length - 1;
for (let i = 0; i < lastIdx; i++) {
acc += `${strings[i]}${wrapI18nPlaceholder(bindingStartIndex + i, contextId)}`;
}
acc += strings[lastIdx];
return acc;
}
function getSeqNumberGenerator(startsAt: number = 0): () => number {
let current = startsAt;
return () => current++;
}
export type I18nMeta = {
id?: string,
description?: string,
meaning?: string
};
/**
* I18nContext is a helper class which keeps track of all i18n-related aspects
* (accumulates content, bindings, etc) between i18nStart and i18nEnd instructions.
*
* When we enter a nested template, the top-level context is being passed down
* to the nested component, which uses this context to generate a child instance
* of I18nContext class (to handle nested template) and at the end, reconciles it back
* with the parent context.
*/
export class I18nContext {
private id: number;
private content: string = '';
private bindings = new Set<o.Expression>();
constructor(
private index: number, private templateIndex: number|null, private ref: any,
private level: number = 0, private uniqueIdGen?: () => number) {
this.uniqueIdGen = uniqueIdGen || getSeqNumberGenerator();
this.id = this.uniqueIdGen();
}
private wrap(symbol: string, elementIndex: number, contextId: number, closed?: boolean) {
const state = closed ? '/' : '';
return wrapI18nPlaceholder(`${state}${symbol}${elementIndex}`, contextId);
}
private append(content: string) { this.content += content; }
private genTemplatePattern(contextId: number|string, templateId: number|string): string {
return wrapI18nPlaceholder(`tmpl:${contextId}:${templateId}`);
}
getId() { return this.id; }
getRef() { return this.ref; }
getIndex() { return this.index; }
getContent() { return this.content; }
getTemplateIndex() { return this.templateIndex; }
getBindings() { return this.bindings; }
appendBinding(binding: o.Expression) { this.bindings.add(binding); }
isRoot() { return this.level === 0; }
isResolved() {
const regex = new RegExp(this.genTemplatePattern('\\d+', '\\d+'));
return !regex.test(this.content);
}
appendText(content: string) { this.append(content.trim()); }
appendTemplate(index: number) { this.append(this.genTemplatePattern(this.id, index)); }
appendElement(elementIndex: number, closed?: boolean) {
this.append(this.wrap('#', elementIndex, this.id, closed));
}
forkChildContext(index: number, templateIndex: number) {
return new I18nContext(index, templateIndex, this.ref, this.level + 1, this.uniqueIdGen);
}
reconcileChildContext(context: I18nContext) {
const id = context.getId();
const content = context.getContent();
const templateIndex = context.getTemplateIndex() !;
const pattern = new RegExp(this.genTemplatePattern(this.id, templateIndex));
const replacement =
`${this.wrap('*', templateIndex, id)}${content}${this.wrap('*', templateIndex, id, true)}`;
this.content = this.content.replace(pattern, replacement);
}
}
File diff suppressed because it is too large Load Diff
+1 -30
View File
@@ -11,6 +11,7 @@ import * as o from '../../output/output_ast';
import * as t from '../r3_ast';
import {R3QueryMetadata} from './api';
import {isI18NAttribute} from './i18n';
/** Name of the temporary to use during data binding */
export const TEMPORARY_NAME = '_t';
@@ -27,17 +28,6 @@ export const REFERENCE_PREFIX = '_r';
/** The name of the implicit context reference */
export const IMPLICIT_REFERENCE = '$implicit';
/** Name of the i18n attributes **/
export const I18N_ATTR = 'i18n';
export const I18N_ATTR_PREFIX = 'i18n-';
/** I18n separators for metadata **/
export const MEANING_SEPARATOR = '|';
export const ID_SEPARATOR = '@@';
/** Placeholder wrapper for i18n expressions **/
export const I18N_PLACEHOLDER_SYMBOL = '�';
/** Non bindable attribute name **/
export const NON_BINDABLE_ATTR = 'ngNonBindable';
@@ -70,25 +60,6 @@ export function invalid<T>(arg: o.Expression | o.Statement | t.Node): never {
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`);
}
export function isI18NAttribute(name: string): boolean {
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
}
export function wrapI18nPlaceholder(content: string | number): string {
return `${I18N_PLACEHOLDER_SYMBOL}${content}${I18N_PLACEHOLDER_SYMBOL}`;
}
export function assembleI18nTemplate(strings: Array<string>): string {
if (!strings.length) return '';
let acc = '';
const lastIdx = strings.length - 1;
for (let i = 0; i < lastIdx; i++) {
acc += `${strings[i]}${wrapI18nPlaceholder(i)}`;
}
acc += strings[lastIdx];
return acc;
}
export function asLiteral(value: any): o.Expression {
if (Array.isArray(value)) {
return o.literalArr(value.map(asLiteral));
@@ -0,0 +1,70 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '../../../src/output/output_ast';
import {I18nContext} from '../../../src/render3/view/i18n';
describe('I18nContext', () => {
it('should support i18n content collection', () => {
const ctx = new I18nContext(5, null, 'myRef');
// basic checks
expect(ctx.isRoot()).toBe(true);
expect(ctx.isResolved()).toBe(true);
expect(ctx.getId()).toBe(0);
expect(ctx.getIndex()).toBe(5);
expect(ctx.getTemplateIndex()).toBeNull();
expect(ctx.getRef()).toBe('myRef');
// data collection checks
expect(ctx.getContent()).toBe('');
ctx.appendText('Foo');
ctx.appendElement(1);
ctx.appendText('Bar');
ctx.appendElement(1, true);
expect(ctx.getContent()).toBe('Foo�#1�Bar�/#1�');
// binding collection checks
expect(ctx.getBindings().size).toBe(0);
ctx.appendBinding(o.literal(1));
ctx.appendBinding(o.literal(2));
expect(ctx.getBindings().size).toBe(2);
});
it('should support nested contexts', () => {
const ctx = new I18nContext(5, null, 'myRef');
const templateIndex = 1;
// set some data for root ctx
ctx.appendText('Foo');
ctx.appendBinding(o.literal(1));
ctx.appendTemplate(templateIndex);
expect(ctx.isResolved()).toBe(false);
// create child context
const childCtx = ctx.forkChildContext(6, templateIndex);
expect(childCtx.getContent()).toBe('');
expect(childCtx.getBindings().size).toBe(0);
expect(childCtx.getRef()).toBe(ctx.getRef()); // ref should be passed into child ctx
expect(childCtx.isRoot()).toBe(false);
childCtx.appendText('Bar');
childCtx.appendElement(2);
childCtx.appendText('Baz');
childCtx.appendElement(2, true);
childCtx.appendBinding(o.literal(2));
childCtx.appendBinding(o.literal(3));
expect(childCtx.getContent()).toBe('Bar�#2:1�Baz�/#2:1�');
expect(childCtx.getBindings().size).toBe(2);
// reconcile
ctx.reconcileChildContext(childCtx);
expect(ctx.getContent()).toBe('Foo�*1:1�Bar�#2:1�Baz�/#2:1��/*1:1�');
});
});
@@ -737,6 +737,12 @@
{
"name": "I18NHtmlParser"
},
{
"name": "I18N_ID_SEPARATOR"
},
{
"name": "I18N_MEANING_SEPARATOR"
},
{
"name": "I18nError"
},
@@ -3449,6 +3455,9 @@
{
"name": "parseCookieValue"
},
{
"name": "parseI18nMeta"
},
{
"name": "parseIntAutoRadix"
},