refactor(ivy): update the compiler to emit $localize
tags (#31609)
This commit changes the Angular compiler (ivy-only) to generate `$localize` tagged strings for component templates that use `i18n` attributes. BREAKING CHANGE Since `$localize` is a global function, it must be included in any applications that use i18n. This is achieved by importing the `@angular/localize` package into an appropriate bundle, where it will be executed before the renderer needs to call `$localize`. For CLI based projects, this is best done in the `polyfills.ts` file. ```ts import '@angular/localize'; ``` For non-CLI applications this could be added as a script to the index.html file or another suitable script file. PR Close #31609
This commit is contained in:

committed by
Misko Hevery

parent
b21397bde9
commit
fa79f51645
@ -271,6 +271,7 @@ class KeyVisitor implements o.ExpressionVisitor {
|
||||
visitReadPropExpr = invalid;
|
||||
visitReadKeyExpr = invalid;
|
||||
visitCommaExpr = invalid;
|
||||
visitLocalizedString = invalid;
|
||||
}
|
||||
|
||||
function invalid<T>(this: o.ExpressionVisitor, arg: o.Expression | o.Statement): never {
|
||||
|
@ -361,6 +361,19 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
|
||||
return null;
|
||||
}
|
||||
|
||||
visitLocalizedString(ast: o.LocalizedString, ctx: EmitterVisitorContext): any {
|
||||
ctx.print(ast, '$localize `' + ast.messageParts[0]);
|
||||
for (let i = 1; i < ast.messageParts.length; i++) {
|
||||
ctx.print(ast, '${');
|
||||
ast.expressions[i - 1].visitExpression(this, ctx);
|
||||
// Add the placeholder name annotation to support runtime inlining
|
||||
ctx.print(ast, `}:${ast.placeHolderNames[i - 1]}:`);
|
||||
ctx.print(ast, ast.messageParts[i]);
|
||||
}
|
||||
ctx.print(ast, '`');
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract visitExternalExpr(ast: o.ExternalExpr, ctx: EmitterVisitorContext): any;
|
||||
|
||||
visitConditionalExpr(ast: o.ConditionalExpr, ctx: EmitterVisitorContext): any {
|
||||
|
@ -480,6 +480,26 @@ export class LiteralExpr extends Expression {
|
||||
}
|
||||
|
||||
|
||||
export class LocalizedString extends Expression {
|
||||
constructor(
|
||||
public messageParts: string[], public placeHolderNames: string[],
|
||||
public expressions: Expression[], sourceSpan?: ParseSourceSpan|null) {
|
||||
super(STRING_TYPE, sourceSpan);
|
||||
}
|
||||
|
||||
isEquivalent(e: Expression): boolean {
|
||||
// return e instanceof LocalizedString && this.message === e.message;
|
||||
return false;
|
||||
}
|
||||
|
||||
isConstant() { return false; }
|
||||
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitLocalizedString(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ExternalExpr extends Expression {
|
||||
constructor(
|
||||
public value: ExternalReference, type?: Type|null, public typeParams: Type[]|null = null,
|
||||
@ -749,6 +769,7 @@ export interface ExpressionVisitor {
|
||||
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any;
|
||||
visitInstantiateExpr(ast: InstantiateExpr, context: any): any;
|
||||
visitLiteralExpr(ast: LiteralExpr, context: any): any;
|
||||
visitLocalizedString(ast: LocalizedString, context: any): any;
|
||||
visitExternalExpr(ast: ExternalExpr, context: any): any;
|
||||
visitConditionalExpr(ast: ConditionalExpr, context: any): any;
|
||||
visitNotExpr(ast: NotExpr, context: any): any;
|
||||
@ -1074,6 +1095,14 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
|
||||
|
||||
visitLiteralExpr(ast: LiteralExpr, context: any): any { return this.transformExpr(ast, context); }
|
||||
|
||||
visitLocalizedString(ast: LocalizedString, context: any): any {
|
||||
return this.transformExpr(
|
||||
new LocalizedString(
|
||||
ast.messageParts, ast.placeHolderNames,
|
||||
this.visitAllExpressions(ast.expressions, context), ast.sourceSpan),
|
||||
context);
|
||||
}
|
||||
|
||||
visitExternalExpr(ast: ExternalExpr, context: any): any {
|
||||
return this.transformExpr(ast, context);
|
||||
}
|
||||
@ -1291,6 +1320,9 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
|
||||
visitLiteralExpr(ast: LiteralExpr, context: any): any {
|
||||
return this.visitExpression(ast, context);
|
||||
}
|
||||
visitLocalizedString(ast: LocalizedString, context: any): any {
|
||||
return this.visitExpression(ast, context);
|
||||
}
|
||||
visitExternalExpr(ast: ExternalExpr, context: any): any {
|
||||
if (ast.typeParams) {
|
||||
ast.typeParams.forEach(type => type.visitType(this, context));
|
||||
@ -1551,6 +1583,12 @@ export function literal(
|
||||
return new LiteralExpr(value, type, sourceSpan);
|
||||
}
|
||||
|
||||
export function localizedString(
|
||||
messageParts: string[], placeholderNames: string[], expressions: Expression[],
|
||||
sourceSpan?: ParseSourceSpan | null): LocalizedString {
|
||||
return new LocalizedString(messageParts, placeholderNames, expressions, sourceSpan);
|
||||
}
|
||||
|
||||
export function isNull(exp: Expression): boolean {
|
||||
return exp instanceof LiteralExpr && exp.value === null;
|
||||
}
|
||||
|
@ -5,11 +5,7 @@
|
||||
* 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 {CompileReflector} from '../compile_reflector';
|
||||
|
||||
import * as o from './output_ast';
|
||||
import {debugOutputAstAsTypeScript} from './ts_emitter';
|
||||
|
||||
@ -239,6 +235,7 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
|
||||
return new clazz(...args);
|
||||
}
|
||||
visitLiteralExpr(ast: o.LiteralExpr, ctx: _ExecutionContext): any { return ast.value; }
|
||||
visitLocalizedString(ast: o.LocalizedString, context: any): any { return null; }
|
||||
visitExternalExpr(ast: o.ExternalExpr, ctx: _ExecutionContext): any {
|
||||
return this.reflector.resolveExternalReference(ast.value);
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import {AST} from '../../../expression_parser/ast';
|
||||
import * as i18n from '../../../i18n/i18n_ast';
|
||||
import * as o from '../../../output/output_ast';
|
||||
|
||||
import {assembleBoundTextPlaceholders, findIndex, getSeqNumberGenerator, updatePlaceholderMap, wrapI18nPlaceholder} from './util';
|
||||
import {assembleBoundTextPlaceholders, getSeqNumberGenerator, updatePlaceholderMap, wrapI18nPlaceholder} from './util';
|
||||
|
||||
enum TagType {
|
||||
ELEMENT,
|
||||
@ -142,7 +142,7 @@ export class I18nContext {
|
||||
return;
|
||||
}
|
||||
// try to find matching template...
|
||||
const tmplIdx = findIndex(phs, findTemplateFn(context.id, context.templateIndex));
|
||||
const tmplIdx = phs.findIndex(findTemplateFn(context.id, context.templateIndex));
|
||||
if (tmplIdx >= 0) {
|
||||
// ... if found - replace it with nested template content
|
||||
const isCloseTag = key.startsWith('CLOSE');
|
||||
|
74
packages/compiler/src/render3/view/i18n/get_msg_utils.ts
Normal file
74
packages/compiler/src/render3/view/i18n/get_msg_utils.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @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 i18n from '../../../i18n/i18n_ast';
|
||||
import {mapLiteral} from '../../../output/map_util';
|
||||
import * as o from '../../../output/output_ast';
|
||||
|
||||
import {serializeIcuNode} from './icu_serializer';
|
||||
import {i18nMetaToDocStmt, metaFromI18nMessage} from './meta';
|
||||
import {formatI18nPlaceholderName} from './util';
|
||||
|
||||
/** Closure uses `goog.getMsg(message)` to lookup translations */
|
||||
const GOOG_GET_MSG = 'goog.getMsg';
|
||||
|
||||
export function createGoogleGetMsgStatements(
|
||||
variable: o.ReadVarExpr, message: i18n.Message, closureVar: o.ReadVarExpr,
|
||||
params: {[name: string]: o.Expression}): o.Statement[] {
|
||||
const messageString = serializeI18nMessageForGetMsg(message);
|
||||
const args = [o.literal(messageString) as o.Expression];
|
||||
if (Object.keys(params).length) {
|
||||
args.push(mapLiteral(params, true));
|
||||
}
|
||||
|
||||
// /** Description and meaning of message */
|
||||
// const MSG_... = goog.getMsg(..);
|
||||
// I18N_X = MSG_...;
|
||||
const statements = [];
|
||||
const jsdocComment = i18nMetaToDocStmt(metaFromI18nMessage(message));
|
||||
if (jsdocComment !== null) {
|
||||
statements.push(jsdocComment);
|
||||
}
|
||||
statements.push(closureVar.set(o.variable(GOOG_GET_MSG).callFn(args)).toConstDecl());
|
||||
statements.push(new o.ExpressionStatement(variable.set(closureVar)));
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
/**
|
||||
* This visitor walks over i18n tree and generates its string representation, including ICUs and
|
||||
* placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format.
|
||||
*/
|
||||
class GetMsgSerializerVisitor implements i18n.Visitor {
|
||||
private formatPh(value: string): string { return `{$${formatI18nPlaceholderName(value)}}`; }
|
||||
|
||||
visitText(text: i18n.Text): any { return text.value; }
|
||||
|
||||
visitContainer(container: i18n.Container): any {
|
||||
return container.children.map(child => child.visit(this)).join('');
|
||||
}
|
||||
|
||||
visitIcu(icu: i18n.Icu): any { return serializeIcuNode(icu); }
|
||||
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder): any {
|
||||
return ph.isVoid ?
|
||||
this.formatPh(ph.startName) :
|
||||
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder): any { return this.formatPh(ph.name); }
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||
return this.formatPh(ph.name);
|
||||
}
|
||||
}
|
||||
|
||||
const serializerVisitor = new GetMsgSerializerVisitor();
|
||||
|
||||
export function serializeI18nMessageForGetMsg(message: i18n.Message): string {
|
||||
return message.nodes.map(node => node.visit(serializerVisitor, null)).join('');
|
||||
}
|
47
packages/compiler/src/render3/view/i18n/icu_serializer.ts
Normal file
47
packages/compiler/src/render3/view/i18n/icu_serializer.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @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 i18n from '../../../i18n/i18n_ast';
|
||||
|
||||
import {formatI18nPlaceholderName} from './util';
|
||||
|
||||
class IcuSerializerVisitor implements i18n.Visitor {
|
||||
visitText(text: i18n.Text): any { return text.value; }
|
||||
|
||||
visitContainer(container: i18n.Container): any {
|
||||
return container.children.map(child => child.visit(this)).join('');
|
||||
}
|
||||
|
||||
visitIcu(icu: i18n.Icu): any {
|
||||
const strCases =
|
||||
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
||||
const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
||||
return result;
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder): any {
|
||||
return ph.isVoid ?
|
||||
this.formatPh(ph.startName) :
|
||||
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder): any { return this.formatPh(ph.name); }
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||
return this.formatPh(ph.name);
|
||||
}
|
||||
|
||||
private formatPh(value: string): string {
|
||||
return `{${formatI18nPlaceholderName(value, /* useCamelCase */ false)}}`;
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = new IcuSerializerVisitor();
|
||||
export function serializeIcuNode(icu: i18n.Icu): string {
|
||||
return icu.visit(serializer);
|
||||
}
|
128
packages/compiler/src/render3/view/i18n/localize_utils.ts
Normal file
128
packages/compiler/src/render3/view/i18n/localize_utils.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @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 i18n from '../../../i18n/i18n_ast';
|
||||
import * as o from '../../../output/output_ast';
|
||||
|
||||
import {serializeIcuNode} from './icu_serializer';
|
||||
import {i18nMetaToDocStmt, metaFromI18nMessage} from './meta';
|
||||
import {formatI18nPlaceholderName} from './util';
|
||||
|
||||
export function createLocalizeStatements(
|
||||
variable: o.ReadVarExpr, message: i18n.Message,
|
||||
params: {[name: string]: o.Expression}): o.Statement[] {
|
||||
const statements = [];
|
||||
|
||||
const jsdocComment = i18nMetaToDocStmt(metaFromI18nMessage(message));
|
||||
if (jsdocComment !== null) {
|
||||
statements.push(jsdocComment);
|
||||
}
|
||||
|
||||
const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message);
|
||||
statements.push(new o.ExpressionStatement(variable.set(
|
||||
o.localizedString(messageParts, placeHolders, placeHolders.map(ph => params[ph])))));
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
class MessagePiece {
|
||||
constructor(public text: string) {}
|
||||
}
|
||||
class LiteralPiece extends MessagePiece {}
|
||||
class PlaceholderPiece extends MessagePiece {
|
||||
constructor(name: string) { super(formatI18nPlaceholderName(name)); }
|
||||
}
|
||||
|
||||
/**
|
||||
* This visitor walks over an i18n tree, capturing literal strings and placeholders.
|
||||
*
|
||||
* The result can be used for generating the `$localize` tagged template literals.
|
||||
*/
|
||||
class LocalizeSerializerVisitor implements i18n.Visitor {
|
||||
visitText(text: i18n.Text, context: MessagePiece[]): any {
|
||||
context.push(new LiteralPiece(text.value));
|
||||
}
|
||||
|
||||
visitContainer(container: i18n.Container, context: MessagePiece[]): any {
|
||||
container.children.forEach(child => child.visit(this, context));
|
||||
}
|
||||
|
||||
visitIcu(icu: i18n.Icu, context: MessagePiece[]): any {
|
||||
context.push(new LiteralPiece(serializeIcuNode(icu)));
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: MessagePiece[]): any {
|
||||
context.push(new PlaceholderPiece(ph.startName));
|
||||
if (!ph.isVoid) {
|
||||
ph.children.forEach(child => child.visit(this, context));
|
||||
context.push(new PlaceholderPiece(ph.closeName));
|
||||
}
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder, context: MessagePiece[]): any {
|
||||
context.push(new PlaceholderPiece(ph.name));
|
||||
}
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||
context.push(new PlaceholderPiece(ph.name));
|
||||
}
|
||||
}
|
||||
|
||||
const serializerVisitor = new LocalizeSerializerVisitor();
|
||||
|
||||
/**
|
||||
* Serialize an i18n message into two arrays: messageParts and placeholders.
|
||||
*
|
||||
* These arrays will be used to generate `$localize` tagged template literals.
|
||||
*
|
||||
* @param message The message to be serialized.
|
||||
* @returns an object containing the messageParts and placeholders.
|
||||
*/
|
||||
export function serializeI18nMessageForLocalize(message: i18n.Message):
|
||||
{messageParts: string[], placeHolders: string[]} {
|
||||
const pieces: MessagePiece[] = [];
|
||||
message.nodes.forEach(node => node.visit(serializerVisitor, pieces));
|
||||
return processMessagePieces(pieces);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the list of serialized MessagePieces into two arrays.
|
||||
*
|
||||
* One contains the literal string pieces and the other the placeholders that will be replaced by
|
||||
* expressions when rendering `$localize` tagged template literals.
|
||||
*
|
||||
* @param pieces The pieces to process.
|
||||
* @returns an object containing the messageParts and placeholders.
|
||||
*/
|
||||
function processMessagePieces(pieces: MessagePiece[]):
|
||||
{messageParts: string[], placeHolders: string[]} {
|
||||
const messageParts: string[] = [];
|
||||
const placeHolders: string[] = [];
|
||||
|
||||
if (pieces[0] instanceof PlaceholderPiece) {
|
||||
// The first piece was a placeholder so we need to add an initial empty message part.
|
||||
messageParts.push('');
|
||||
}
|
||||
|
||||
for (let i = 0; i < pieces.length; i++) {
|
||||
const part = pieces[i];
|
||||
if (part instanceof LiteralPiece) {
|
||||
messageParts.push(part.text);
|
||||
} else {
|
||||
placeHolders.push(part.text);
|
||||
if (pieces[i - 1] instanceof PlaceholderPiece) {
|
||||
// There were two placeholders in a row, so we need to add an empty message part.
|
||||
messageParts.push('');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pieces[pieces.length - 1] instanceof PlaceholderPiece) {
|
||||
// The last piece was a placeholder so we need to add a final empty message part.
|
||||
messageParts.push('');
|
||||
}
|
||||
return {messageParts, placeHolders};
|
||||
}
|
@ -12,8 +12,15 @@ import {createI18nMessageFactory} from '../../../i18n/i18n_parser';
|
||||
import * as html from '../../../ml_parser/ast';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../ml_parser/interpolation_config';
|
||||
import {ParseTreeResult} from '../../../ml_parser/parser';
|
||||
import * as o from '../../../output/output_ast';
|
||||
|
||||
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nMeta, hasI18nAttrs, icuFromI18nMessage, metaFromI18nMessage, parseI18nMeta} from './util';
|
||||
import {I18N_ATTR, I18N_ATTR_PREFIX, hasI18nAttrs, icuFromI18nMessage} from './util';
|
||||
|
||||
export type I18nMeta = {
|
||||
id?: string,
|
||||
description?: string,
|
||||
meaning?: string
|
||||
};
|
||||
|
||||
function setI18nRefs(html: html.Node & {i18n?: i18n.AST}, i18n: i18n.Node) {
|
||||
html.i18n = i18n;
|
||||
@ -129,3 +136,57 @@ export function processI18nMeta(
|
||||
htmlAstWithErrors.rootNodes),
|
||||
htmlAstWithErrors.errors);
|
||||
}
|
||||
|
||||
export function metaFromI18nMessage(message: i18n.Message, id: string | null = null): I18nMeta {
|
||||
return {
|
||||
id: typeof id === 'string' ? id : message.id || '',
|
||||
meaning: message.meaning || '',
|
||||
description: message.description || ''
|
||||
};
|
||||
}
|
||||
|
||||
/** I18n separators for metadata **/
|
||||
const I18N_MEANING_SEPARATOR = '|';
|
||||
const I18N_ID_SEPARATOR = '@@';
|
||||
|
||||
/**
|
||||
* Parses i18n metas like:
|
||||
* - "@@id",
|
||||
* - "description[@@id]",
|
||||
* - "meaning|description[@@id]"
|
||||
* and returns an object with parsed output.
|
||||
*
|
||||
* @param meta String that represents i18n meta
|
||||
* @returns Object with id, meaning and description fields
|
||||
*/
|
||||
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};
|
||||
}
|
||||
|
||||
// Converts i18n meta information for a message (id, description, meaning)
|
||||
// to a JsDoc statement formatted as expected by the Closure compiler.
|
||||
export 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.meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||
}
|
||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
/**
|
||||
* @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 i18n from '../../../i18n/i18n_ast';
|
||||
|
||||
import {formatI18nPlaceholderName} from './util';
|
||||
|
||||
/**
|
||||
* This visitor walks over i18n tree and generates its string representation, including ICUs and
|
||||
* placeholders in `{$placeholder}` (for plain messages) or `{PLACEHOLDER}` (inside ICUs) format.
|
||||
*/
|
||||
class SerializerVisitor implements i18n.Visitor {
|
||||
/**
|
||||
* Keeps track of ICU nesting level, allowing to detect that we are processing elements of an ICU.
|
||||
*
|
||||
* This is needed due to the fact that placeholders in ICUs and in other messages are represented
|
||||
* differently in Closure:
|
||||
* - {$placeholder} in non-ICU case
|
||||
* - {PLACEHOLDER} inside ICU
|
||||
*/
|
||||
private icuNestingLevel = 0;
|
||||
|
||||
private formatPh(value: string): string {
|
||||
const isInsideIcu = this.icuNestingLevel > 0;
|
||||
const formatted = formatI18nPlaceholderName(value, /* useCamelCase */ !isInsideIcu);
|
||||
return isInsideIcu ? `{${formatted}}` : `{$${formatted}}`;
|
||||
}
|
||||
|
||||
visitText(text: i18n.Text, context: any): any { return text.value; }
|
||||
|
||||
visitContainer(container: i18n.Container, context: any): any {
|
||||
return container.children.map(child => child.visit(this)).join('');
|
||||
}
|
||||
|
||||
visitIcu(icu: i18n.Icu, context: any): any {
|
||||
this.icuNestingLevel++;
|
||||
const strCases =
|
||||
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
||||
const result = `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
||||
this.icuNestingLevel--;
|
||||
return result;
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
|
||||
return ph.isVoid ?
|
||||
this.formatPh(ph.startName) :
|
||||
`${this.formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${this.formatPh(ph.closeName)}`;
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any { return this.formatPh(ph.name); }
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
|
||||
return this.formatPh(ph.name);
|
||||
}
|
||||
}
|
||||
|
||||
const serializerVisitor = new SerializerVisitor();
|
||||
|
||||
export function getSerializedI18nContent(message: i18n.Message): string {
|
||||
return message.nodes.map(node => node.visit(serializerVisitor, null)).join('');
|
||||
}
|
@ -5,14 +5,10 @@
|
||||
* 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 i18n from '../../../i18n/i18n_ast';
|
||||
import {toPublicName} from '../../../i18n/serializers/xmb';
|
||||
import * as html from '../../../ml_parser/ast';
|
||||
import {mapLiteral} from '../../../output/map_util';
|
||||
import * as o from '../../../output/output_ast';
|
||||
import {Identifiers as R3} from '../../r3_identifiers';
|
||||
|
||||
|
||||
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
|
||||
const CLOSURE_TRANSLATION_PREFIX = 'MSG_';
|
||||
@ -20,16 +16,6 @@ const CLOSURE_TRANSLATION_PREFIX = 'MSG_';
|
||||
/* Prefix for non-`goog.getMsg` i18n-related vars */
|
||||
export const TRANSLATION_PREFIX = 'I18N_';
|
||||
|
||||
/** Closure uses `goog.getMsg(message)` to lookup translations */
|
||||
const GOOG_GET_MSG = 'goog.getMsg';
|
||||
|
||||
/** Name of the global variable that is used to determine if we use Closure translations or not */
|
||||
const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode';
|
||||
|
||||
/** 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-';
|
||||
@ -43,55 +29,6 @@ export const I18N_ICU_MAPPING_PREFIX = 'I18N_EXP_';
|
||||
/** Placeholder wrapper for i18n expressions **/
|
||||
export const I18N_PLACEHOLDER_SYMBOL = '<27>';
|
||||
|
||||
export type I18nMeta = {
|
||||
id?: string,
|
||||
description?: string,
|
||||
meaning?: string
|
||||
};
|
||||
|
||||
function i18nTranslationToDeclStmt(
|
||||
variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
|
||||
params?: {[name: string]: o.Expression}): o.Statement[] {
|
||||
const statements: o.Statement[] = [];
|
||||
// var I18N_X;
|
||||
statements.push(
|
||||
new o.DeclareVarStmt(variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan));
|
||||
|
||||
const args = [o.literal(message) as o.Expression];
|
||||
if (params && Object.keys(params).length) {
|
||||
args.push(mapLiteral(params, true));
|
||||
}
|
||||
|
||||
// Closure JSDoc comments
|
||||
const docStatements = i18nMetaToDocStmt(meta);
|
||||
const thenStatements: o.Statement[] = docStatements ? [docStatements] : [];
|
||||
const googFnCall = o.variable(GOOG_GET_MSG).callFn(args);
|
||||
// const MSG_... = goog.getMsg(..);
|
||||
thenStatements.push(closureVar.set(googFnCall).toConstDecl());
|
||||
// I18N_X = MSG_...;
|
||||
thenStatements.push(new o.ExpressionStatement(variable.set(closureVar)));
|
||||
const localizeFnCall = o.importExpr(R3.i18nLocalize).callFn(args);
|
||||
// I18N_X = i18nLocalize(...);
|
||||
const elseStatements = [new o.ExpressionStatement(variable.set(localizeFnCall))];
|
||||
// if(ngI18nClosureMode) { ... } else { ... }
|
||||
statements.push(o.ifStmt(o.variable(NG_I18N_CLOSURE_MODE), thenStatements, elseStatements));
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
// Converts i18n meta information 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.meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||
}
|
||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||
}
|
||||
|
||||
export function isI18nAttribute(name: string): boolean {
|
||||
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
|
||||
}
|
||||
@ -108,14 +45,6 @@ export function hasI18nAttrs(element: html.Element): boolean {
|
||||
return element.attrs.some((attr: html.Attribute) => isI18nAttribute(attr.name));
|
||||
}
|
||||
|
||||
export function metaFromI18nMessage(message: i18n.Message, id: string | null = null): I18nMeta {
|
||||
return {
|
||||
id: typeof id === 'string' ? id : message.id || '',
|
||||
meaning: message.meaning || '',
|
||||
description: message.description || ''
|
||||
};
|
||||
}
|
||||
|
||||
export function icuFromI18nMessage(message: i18n.Message) {
|
||||
return message.nodes[0] as i18n.IcuPlaceholder;
|
||||
}
|
||||
@ -143,8 +72,8 @@ export function getSeqNumberGenerator(startsAt: number = 0): () => number {
|
||||
}
|
||||
|
||||
export function placeholdersToParams(placeholders: Map<string, string[]>):
|
||||
{[name: string]: o.Expression} {
|
||||
const params: {[name: string]: o.Expression} = {};
|
||||
{[name: string]: o.LiteralExpr} {
|
||||
const params: {[name: string]: o.LiteralExpr} = {};
|
||||
placeholders.forEach((values: string[], key: string) => {
|
||||
params[key] = o.literal(values.length > 1 ? `[${values.join('|')}]` : values[0]);
|
||||
});
|
||||
@ -175,42 +104,24 @@ export function assembleBoundTextPlaceholders(
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
export function findIndex(items: any[], callback: (item: any) => boolean): number {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (callback(items[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses i18n metas like:
|
||||
* - "@@id",
|
||||
* - "description[@@id]",
|
||||
* - "meaning|description[@@id]"
|
||||
* and returns an object with parsed output.
|
||||
* Format the placeholder names in a map of placeholders to expressions.
|
||||
*
|
||||
* @param meta String that represents i18n meta
|
||||
* @returns Object with id, meaning and description fields
|
||||
* The placeholder names are converted from "internal" format (e.g. `START_TAG_DIV_1`) to "external"
|
||||
* format (e.g. `startTagDiv_1`).
|
||||
*
|
||||
* @param params A map of placeholder names to expressions.
|
||||
* @param useCamelCase whether to camelCase the placeholder name when formatting.
|
||||
* @returns A new map of formatted placeholder names to expressions.
|
||||
*/
|
||||
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];
|
||||
export function i18nFormatPlaceholderNames(
|
||||
params: {[name: string]: o.Expression} = {}, useCamelCase: boolean) {
|
||||
const _params: {[key: string]: o.Expression} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(
|
||||
key => _params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]);
|
||||
}
|
||||
|
||||
return {id, meaning, description};
|
||||
return _params;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -254,27 +165,10 @@ export function getTranslationConstPrefix(extra: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates translation declaration statements.
|
||||
*
|
||||
* @param variable Translation value reference
|
||||
* @param closureVar Variable for Closure `goog.getMsg` calls
|
||||
* @param message Text message to be translated
|
||||
* @param meta Object that contains meta information (id, meaning and description)
|
||||
* @param params Object with placeholders key-value pairs
|
||||
* @param transformFn Optional transformation (post processing) function reference
|
||||
* @returns Array of Statements that represent a given translation
|
||||
* Generate AST to declare a variable. E.g. `var I18N_1;`.
|
||||
* @param variable the name of the variable to declare.
|
||||
*/
|
||||
export function getTranslationDeclStmts(
|
||||
variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
|
||||
params: {[name: string]: o.Expression} = {},
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] {
|
||||
const statements: o.Statement[] = [];
|
||||
|
||||
statements.push(...i18nTranslationToDeclStmt(variable, closureVar, message, meta, params));
|
||||
|
||||
if (transformFn) {
|
||||
statements.push(new o.ExpressionStatement(variable.set(transformFn(variable))));
|
||||
}
|
||||
|
||||
return statements;
|
||||
export function declareI18nVariable(variable: o.ReadVarExpr): o.Statement {
|
||||
return new o.DeclareVarStmt(
|
||||
variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan);
|
||||
}
|
||||
|
@ -33,9 +33,10 @@ import {htmlAstToRender3Ast} from '../r3_template_transform';
|
||||
import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util';
|
||||
|
||||
import {I18nContext} from './i18n/context';
|
||||
import {createGoogleGetMsgStatements} from './i18n/get_msg_utils';
|
||||
import {createLocalizeStatements} from './i18n/localize_utils';
|
||||
import {I18nMetaVisitor} from './i18n/meta';
|
||||
import {getSerializedI18nContent} from './i18n/serializer';
|
||||
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, declareI18nVariable, getTranslationConstPrefix, i18nFormatPlaceholderNames, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {StylingBuilder, StylingInstruction} from './styling_builder';
|
||||
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, chainedInstruction, getAttrsForDirectiveMatching, getInterpolationArgsLength, invalid, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
@ -187,27 +188,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
});
|
||||
}
|
||||
|
||||
registerContextVariables(variable: t.Variable) {
|
||||
const scopedName = this._bindingScope.freshReferenceName();
|
||||
const retrievalLevel = this.level;
|
||||
const lhs = o.variable(variable.name + scopedName);
|
||||
this._bindingScope.set(
|
||||
retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT,
|
||||
(scope: BindingScope, relativeLevel: number) => {
|
||||
let rhs: o.Expression;
|
||||
if (scope.bindingLevel === retrievalLevel) {
|
||||
// e.g. ctx
|
||||
rhs = o.variable(CONTEXT_NAME);
|
||||
} else {
|
||||
const sharedCtxVar = scope.getSharedContextName(retrievalLevel);
|
||||
// e.g. ctx_r0 OR x(2);
|
||||
rhs = sharedCtxVar ? sharedCtxVar : generateNextContextExpr(relativeLevel);
|
||||
}
|
||||
// e.g. const $item$ = x(2).$implicit;
|
||||
return [lhs.set(rhs.prop(variable.value || IMPLICIT_REFERENCE)).toConstDecl()];
|
||||
});
|
||||
}
|
||||
|
||||
buildTemplateFunction(
|
||||
nodes: t.Node[], variables: t.Variable[], ngContentSelectorsOffset: number = 0,
|
||||
i18n?: i18n.AST): o.FunctionExpr {
|
||||
@ -317,38 +297,47 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// LocalResolver
|
||||
notifyImplicitReceiverUse(): void { this._bindingScope.notifyImplicitReceiverUse(); }
|
||||
|
||||
i18nTranslate(
|
||||
private i18nTranslate(
|
||||
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
|
||||
const _ref = ref || o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
|
||||
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
|
||||
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
|
||||
const closureVar = this.i18nGenerateClosureVar(message.id);
|
||||
const formattedParams = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ true);
|
||||
const meta = metaFromI18nMessage(message);
|
||||
const content = getSerializedI18nContent(message);
|
||||
const statements =
|
||||
getTranslationDeclStmts(_ref, closureVar, content, meta, formattedParams, transformFn);
|
||||
const statements = getTranslationDeclStmts(message, _ref, closureVar, params, transformFn);
|
||||
this.constantPool.statements.push(...statements);
|
||||
return _ref;
|
||||
}
|
||||
|
||||
i18nFormatPlaceholderNames(params: {[name: string]: o.Expression} = {}, useCamelCase: boolean) {
|
||||
const _params: {[key: string]: o.Expression} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(
|
||||
key => _params[formatI18nPlaceholderName(key, useCamelCase)] = params[key]);
|
||||
}
|
||||
return _params;
|
||||
private registerContextVariables(variable: t.Variable) {
|
||||
const scopedName = this._bindingScope.freshReferenceName();
|
||||
const retrievalLevel = this.level;
|
||||
const lhs = o.variable(variable.name + scopedName);
|
||||
this._bindingScope.set(
|
||||
retrievalLevel, variable.name, lhs, DeclarationPriority.CONTEXT,
|
||||
(scope: BindingScope, relativeLevel: number) => {
|
||||
let rhs: o.Expression;
|
||||
if (scope.bindingLevel === retrievalLevel) {
|
||||
// e.g. ctx
|
||||
rhs = o.variable(CONTEXT_NAME);
|
||||
} else {
|
||||
const sharedCtxVar = scope.getSharedContextName(retrievalLevel);
|
||||
// e.g. ctx_r0 OR x(2);
|
||||
rhs = sharedCtxVar ? sharedCtxVar : generateNextContextExpr(relativeLevel);
|
||||
}
|
||||
// e.g. const $item$ = x(2).$implicit;
|
||||
return [lhs.set(rhs.prop(variable.value || IMPLICIT_REFERENCE)).toConstDecl()];
|
||||
});
|
||||
}
|
||||
|
||||
i18nAppendBindings(expressions: AST[]) {
|
||||
private i18nAppendBindings(expressions: AST[]) {
|
||||
if (expressions.length > 0) {
|
||||
expressions.forEach(expression => this.i18n !.appendBinding(expression));
|
||||
}
|
||||
}
|
||||
|
||||
i18nBindProps(props: {[key: string]: t.Text | t.BoundText}): {[key: string]: o.Expression} {
|
||||
private i18nBindProps(props: {[key: string]: t.Text | t.BoundText}):
|
||||
{[key: string]: o.Expression} {
|
||||
const bound: {[key: string]: o.Expression} = {};
|
||||
Object.keys(props).forEach(key => {
|
||||
const prop = props[key];
|
||||
@ -369,7 +358,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
return bound;
|
||||
}
|
||||
|
||||
i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
|
||||
private i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
|
||||
let name: string;
|
||||
const suffix = this.fileBasedI18nSuffix.toUpperCase();
|
||||
if (this.i18nUseExternalIds) {
|
||||
@ -383,7 +372,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
return o.variable(name);
|
||||
}
|
||||
|
||||
i18nUpdateRef(context: I18nContext): void {
|
||||
private i18nUpdateRef(context: I18nContext): void {
|
||||
const {icus, meta, isRoot, isResolved, isEmitted} = context;
|
||||
if (isRoot && isResolved && !isEmitted && !isSingleI18nIcu(meta)) {
|
||||
context.isEmitted = true;
|
||||
@ -428,7 +417,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
}
|
||||
}
|
||||
|
||||
i18nStart(span: ParseSourceSpan|null = null, meta: i18n.AST, selfClosing?: boolean): void {
|
||||
private i18nStart(span: ParseSourceSpan|null = null, meta: i18n.AST, selfClosing?: boolean):
|
||||
void {
|
||||
const index = this.allocateDataSlot();
|
||||
if (this.i18nContext) {
|
||||
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta);
|
||||
@ -448,7 +438,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this.creationInstruction(span, selfClosing ? R3.i18n : R3.i18nStart, params);
|
||||
}
|
||||
|
||||
i18nEnd(span: ParseSourceSpan|null = null, selfClosing?: boolean): void {
|
||||
private i18nEnd(span: ParseSourceSpan|null = null, selfClosing?: boolean): void {
|
||||
if (!this.i18n) {
|
||||
throw new Error('i18nEnd is executed with no i18n context present');
|
||||
}
|
||||
@ -476,6 +466,34 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
this.i18n = null; // reset local i18n context
|
||||
}
|
||||
|
||||
private getNamespaceInstruction(namespaceKey: string|null) {
|
||||
switch (namespaceKey) {
|
||||
case 'math':
|
||||
return R3.namespaceMathML;
|
||||
case 'svg':
|
||||
return R3.namespaceSVG;
|
||||
default:
|
||||
return R3.namespaceHTML;
|
||||
}
|
||||
}
|
||||
|
||||
private addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) {
|
||||
this._namespace = nsInstruction;
|
||||
this.creationInstruction(element.sourceSpan, nsInstruction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an update instruction for an interpolated property or attribute, such as
|
||||
* `prop="{{value}}"` or `attr.title="{{value}}"`
|
||||
*/
|
||||
private interpolatedUpdateInstruction(
|
||||
instruction: o.ExternalReference, elementIndex: number, attrName: string,
|
||||
input: t.BoundAttribute, value: any, params: any[]) {
|
||||
this.updateInstruction(
|
||||
elementIndex, input.sourceSpan, instruction,
|
||||
() => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]);
|
||||
}
|
||||
|
||||
visitContent(ngContent: t.Content) {
|
||||
const slot = this.allocateDataSlot();
|
||||
const projectionSlotIdx = this._ngContentSelectorsOffset + this._ngContentReservedSlots.length;
|
||||
@ -505,23 +523,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getNamespaceInstruction(namespaceKey: string|null) {
|
||||
switch (namespaceKey) {
|
||||
case 'math':
|
||||
return R3.namespaceMathML;
|
||||
case 'svg':
|
||||
return R3.namespaceSVG;
|
||||
default:
|
||||
return R3.namespaceHTML;
|
||||
}
|
||||
}
|
||||
|
||||
addNamespaceInstruction(nsInstruction: o.ExternalReference, element: t.Element) {
|
||||
this._namespace = nsInstruction;
|
||||
this.creationInstruction(element.sourceSpan, nsInstruction);
|
||||
}
|
||||
|
||||
visitElement(element: t.Element) {
|
||||
const elementIndex = this.allocateDataSlot();
|
||||
const stylingBuilder = new StylingBuilder(o.literal(elementIndex), null);
|
||||
@ -844,17 +845,6 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an update instruction for an interpolated property or attribute, such as
|
||||
* `prop="{{value}}"` or `attr.title="{{value}}"`
|
||||
*/
|
||||
interpolatedUpdateInstruction(
|
||||
instruction: o.ExternalReference, elementIndex: number, attrName: string,
|
||||
input: t.BoundAttribute, value: any, params: any[]) {
|
||||
this.updateInstruction(
|
||||
elementIndex, input.sourceSpan, instruction,
|
||||
() => [o.literal(attrName), ...this.getUpdateInstructionArguments(value), ...params]);
|
||||
}
|
||||
|
||||
visitTemplate(template: t.Template) {
|
||||
const NG_TEMPLATE_TAG_NAME = 'ng-template';
|
||||
@ -1007,7 +997,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// - all ICU vars (such as `VAR_SELECT` or `VAR_PLURAL`) are replaced with correct values
|
||||
const transformFn = (raw: o.ReadVarExpr) => {
|
||||
const params = {...vars, ...placeholders};
|
||||
const formatted = this.i18nFormatPlaceholderNames(params, /* useCamelCase */ false);
|
||||
const formatted = i18nFormatPlaceholderNames(params, /* useCamelCase */ false);
|
||||
return instruction(null, R3.i18nPostprocess, [raw, mapLiteral(formatted, true)]);
|
||||
};
|
||||
|
||||
@ -2004,3 +1994,52 @@ interface ChainableBindingInstruction {
|
||||
value: () => o.Expression;
|
||||
params?: any[];
|
||||
}
|
||||
|
||||
/** Name of the global variable that is used to determine if we use Closure translations or not */
|
||||
const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode';
|
||||
|
||||
/**
|
||||
* Generate statements that define a given translation message.
|
||||
*
|
||||
* ```
|
||||
* var I18N_1;
|
||||
* if (ngI18nClosureMode) {
|
||||
* var MSG_EXTERNAL_XXX = goog.getMsg(
|
||||
* "Some message with {$interpolation}!",
|
||||
* { "interpolation": "\uFFFD0\uFFFD" }
|
||||
* );
|
||||
* I18N_1 = MSG_EXTERNAL_XXX;
|
||||
* }
|
||||
* else {
|
||||
* I18N_1 = $localize`Some message with ${'\uFFFD0\uFFFD'}!`;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param message The original i18n AST message node
|
||||
* @param variable The variable that will be assigned the translation, e.g. `I18N_1`.
|
||||
* @param closureVar The variable for Closure `goog.getMsg` calls, e.g. `MSG_EXTERNAL_XXX`.
|
||||
* @param params Object mapping placeholder names to their values (e.g.
|
||||
* `{ "interpolation": "\uFFFD0\uFFFD" }`).
|
||||
* @param transformFn Optional transformation function that will be applied to the translation (e.g.
|
||||
* post-processing).
|
||||
* @returns An array of statements that defined a given translation.
|
||||
*/
|
||||
export function getTranslationDeclStmts(
|
||||
message: i18n.Message, variable: o.ReadVarExpr, closureVar: o.ReadVarExpr,
|
||||
params: {[name: string]: o.Expression} = {},
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] {
|
||||
const formattedParams = i18nFormatPlaceholderNames(params, /* useCamelCase */ true);
|
||||
const statements: o.Statement[] = [
|
||||
declareI18nVariable(variable),
|
||||
o.ifStmt(
|
||||
o.variable(NG_I18N_CLOSURE_MODE),
|
||||
createGoogleGetMsgStatements(variable, message, closureVar, formattedParams),
|
||||
createLocalizeStatements(variable, message, formattedParams)),
|
||||
];
|
||||
|
||||
if (transformFn) {
|
||||
statements.push(new o.ExpressionStatement(variable.set(transformFn(variable))));
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
* 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 {I18nMeta, parseI18nMeta} from '@angular/compiler/src/render3/view/i18n/meta';
|
||||
|
||||
import {AST} from '../../../src/expression_parser/ast';
|
||||
import {Lexer} from '../../../src/expression_parser/lexer';
|
||||
@ -13,8 +14,10 @@ import * as i18n from '../../../src/i18n/i18n_ast';
|
||||
import * as o from '../../../src/output/output_ast';
|
||||
import * as t from '../../../src/render3/r3_ast';
|
||||
import {I18nContext} from '../../../src/render3/view/i18n/context';
|
||||
import {getSerializedI18nContent} from '../../../src/render3/view/i18n/serializer';
|
||||
import {I18nMeta, formatI18nPlaceholderName, parseI18nMeta} from '../../../src/render3/view/i18n/util';
|
||||
import {serializeI18nMessageForGetMsg} from '../../../src/render3/view/i18n/get_msg_utils';
|
||||
import {serializeIcuNode} from '../../../src/render3/view/i18n/icu_serializer';
|
||||
import {serializeI18nMessageForLocalize} from '../../../src/render3/view/i18n/localize_utils';
|
||||
import {formatI18nPlaceholderName} from '../../../src/render3/view/i18n/util';
|
||||
|
||||
import {parseR3 as parse} from './util';
|
||||
|
||||
@ -214,45 +217,162 @@ describe('Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Serializer', () => {
|
||||
describe('serializeI18nMessageForGetMsg', () => {
|
||||
const serialize = (input: string): string => {
|
||||
const tree = parse(`<div i18n>${input}</div>`);
|
||||
const root = tree.nodes[0] as t.Element;
|
||||
return getSerializedI18nContent(root.i18n as i18n.Message);
|
||||
return serializeI18nMessageForGetMsg(root.i18n as i18n.Message);
|
||||
};
|
||||
it('should produce output for i18n content', () => {
|
||||
const cases = [
|
||||
// plain text
|
||||
['Some text', 'Some text'],
|
||||
|
||||
// text with interpolation
|
||||
[
|
||||
'Some text {{ valueA }} and {{ valueB + valueC }}',
|
||||
'Some text {$interpolation} and {$interpolation_1}'
|
||||
],
|
||||
it('should serialize plain text for `GetMsg()`',
|
||||
() => { expect(serialize('Some text')).toEqual('Some text'); });
|
||||
|
||||
// content with HTML tags
|
||||
[
|
||||
'A <span>B<div>C</div></span> D',
|
||||
'A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D'
|
||||
],
|
||||
it('should serialize text with interpolation for `GetMsg()`', () => {
|
||||
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}'))
|
||||
.toEqual('Some text {$interpolation} and {$interpolation_1}');
|
||||
});
|
||||
|
||||
// simple ICU
|
||||
['{age, plural, 10 {ten} other {other}}', '{VAR_PLURAL, plural, 10 {ten} other {other}}'],
|
||||
it('should serialize content with HTML tags for `GetMsg()`', () => {
|
||||
expect(serialize('A <span>B<div>C</div></span> D'))
|
||||
.toEqual('A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D');
|
||||
});
|
||||
|
||||
// nested ICUs
|
||||
[
|
||||
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}',
|
||||
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'
|
||||
],
|
||||
it('should serialize simple ICU for `GetMsg()`', () => {
|
||||
expect(serialize('{age, plural, 10 {ten} other {other}}'))
|
||||
.toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}');
|
||||
});
|
||||
|
||||
// ICU with nested HTML
|
||||
[
|
||||
'{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}',
|
||||
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'
|
||||
]
|
||||
];
|
||||
it('should serialize nested ICUs for `GetMsg()`', () => {
|
||||
expect(serialize(
|
||||
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}'))
|
||||
.toEqual(
|
||||
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}');
|
||||
});
|
||||
|
||||
cases.forEach(([input, output]) => { expect(serialize(input)).toEqual(output); });
|
||||
it('should serialize ICU with nested HTML for `GetMsg()`', () => {
|
||||
expect(serialize('{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}'))
|
||||
.toEqual(
|
||||
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}');
|
||||
});
|
||||
|
||||
it('should serialize ICU with nested HTML containing further ICUs for `GetMsg()`', () => {
|
||||
expect(
|
||||
serialize(
|
||||
'{gender, select, male {male} female {female} other {other}}<div>{gender, select, male {male} female {female} other {other}}</div>'))
|
||||
.toEqual('{$icu}{$startTagDiv}{$icu}{$closeTagDiv}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeI18nMessageForLocalize', () => {
|
||||
const serialize = (input: string) => {
|
||||
const tree = parse(`<div i18n>${input}</div>`);
|
||||
const root = tree.nodes[0] as t.Element;
|
||||
return serializeI18nMessageForLocalize(root.i18n as i18n.Message);
|
||||
};
|
||||
|
||||
it('should serialize plain text for `$localize()`', () => {
|
||||
expect(serialize('Some text')).toEqual({messageParts: ['Some text'], placeHolders: []});
|
||||
});
|
||||
|
||||
it('should serialize text with interpolation for `$localize()`', () => {
|
||||
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }} done')).toEqual({
|
||||
messageParts: ['Some text ', ' and ', ' done'],
|
||||
placeHolders: ['interpolation', 'interpolation_1']
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize text with interpolation at start for `$localize()`', () => {
|
||||
expect(serialize('{{ valueA }} and {{ valueB + valueC }} done')).toEqual({
|
||||
messageParts: ['', ' and ', ' done'],
|
||||
placeHolders: ['interpolation', 'interpolation_1']
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should serialize text with interpolation at end for `$localize()`', () => {
|
||||
expect(serialize('Some text {{ valueA }} and {{ valueB + valueC }}')).toEqual({
|
||||
messageParts: ['Some text ', ' and ', ''],
|
||||
placeHolders: ['interpolation', 'interpolation_1']
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should serialize only interpolation for `$localize()`', () => {
|
||||
expect(serialize('{{ valueB + valueC }}'))
|
||||
.toEqual({messageParts: ['', ''], placeHolders: ['interpolation']});
|
||||
});
|
||||
|
||||
|
||||
it('should serialize content with HTML tags for `$localize()`', () => {
|
||||
expect(serialize('A <span>B<div>C</div></span> D')).toEqual({
|
||||
messageParts: ['A ', 'B', 'C', '', ' D'],
|
||||
placeHolders: ['startTagSpan', 'startTagDiv', 'closeTagDiv', 'closeTagSpan']
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should serialize simple ICU for `$localize()`', () => {
|
||||
expect(serialize('{age, plural, 10 {ten} other {other}}')).toEqual({
|
||||
messageParts: ['{VAR_PLURAL, plural, 10 {ten} other {other}}'],
|
||||
placeHolders: []
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should serialize nested ICUs for `$localize()`', () => {
|
||||
expect(serialize(
|
||||
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}'))
|
||||
.toEqual({
|
||||
messageParts: [
|
||||
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}'
|
||||
],
|
||||
placeHolders: []
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should serialize ICU with nested HTML for `$localize()`', () => {
|
||||
expect(serialize('{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}')).toEqual({
|
||||
messageParts: [
|
||||
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}'
|
||||
],
|
||||
placeHolders: []
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize ICU with nested HTML containing further ICUs for `$localize()`', () => {
|
||||
expect(
|
||||
serialize(
|
||||
'{gender, select, male {male} female {female} other {other}}<div>{gender, select, male {male} female {female} other {other}}</div>'))
|
||||
.toEqual({
|
||||
messageParts: ['', '', '', '', ''],
|
||||
placeHolders: ['icu', 'startTagDiv', 'icu', 'closeTagDiv']
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeIcuNode', () => {
|
||||
const serialize = (input: string) => {
|
||||
const tree = parse(`<div i18n>${input}</div>`);
|
||||
const rooti18n = (tree.nodes[0] as t.Element).i18n as i18n.Message;
|
||||
return serializeIcuNode(rooti18n.nodes[0] as i18n.Icu);
|
||||
};
|
||||
|
||||
it('should serialize a simple ICU', () => {
|
||||
expect(serialize('{age, plural, 10 {ten} other {other}}'))
|
||||
.toEqual('{VAR_PLURAL, plural, 10 {ten} other {other}}');
|
||||
});
|
||||
|
||||
it('should serialize a next ICU', () => {
|
||||
expect(serialize(
|
||||
'{age, plural, 10 {ten {size, select, 1 {one} 2 {two} other {2+}}} other {other}}'))
|
||||
.toEqual(
|
||||
'{VAR_PLURAL, plural, 10 {ten {VAR_SELECT, select, 1 {one} 2 {two} other {2+}}} other {other}}');
|
||||
});
|
||||
|
||||
it('should serialize ICU with nested HTML', () => {
|
||||
expect(serialize('{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}'))
|
||||
.toEqual(
|
||||
'{VAR_PLURAL, plural, 10 {{START_BOLD_TEXT}ten{CLOSE_BOLD_TEXT}} other {{START_TAG_DIV}other{CLOSE_TAG_DIV}}}');
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user