feat(ivy): i18n compiler support for i18nStart and i18nEnd instructions (#26442)
PR Close #26442
This commit is contained in:
committed by
Misko Hevery
parent
8024857d4c
commit
8a3fd58cad
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user