parent
a4934a74b6
commit
92e80af875
File diff suppressed because it is too large
Load Diff
|
@ -7,21 +7,12 @@
|
|||
*/
|
||||
|
||||
import * as o from './output/output_ast';
|
||||
import {I18nMeta, parseI18nMeta} from './render3/view/i18n';
|
||||
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.
|
||||
*
|
||||
|
@ -78,8 +69,6 @@ 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>();
|
||||
|
@ -115,60 +104,6 @@ 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.
|
||||
//
|
||||
// ```
|
||||
// /**
|
||||
// * @desc description?
|
||||
// * @meaning meaning?
|
||||
// */
|
||||
// const MSG_XYZ = goog.getMsg('message');
|
||||
// ```
|
||||
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 = parsedMeta.meaning ? `${message}\u0000\u0000${parsedMeta.meaning}` : message;
|
||||
|
||||
const exp = this.translations.get(key);
|
||||
|
||||
if (exp) {
|
||||
return exp;
|
||||
}
|
||||
|
||||
const variable = o.variable(this.freshTranslationName(suffix));
|
||||
this.appendTranslationMeta(parsedMeta);
|
||||
this.statements.push(this.getTranslationDeclStmt(variable, message));
|
||||
|
||||
this.translations.set(key, variable);
|
||||
return variable;
|
||||
}
|
||||
|
||||
getDefinition(type: any, kind: DefinitionKind, ctx: OutputContext, forceShared: boolean = false):
|
||||
o.Expression {
|
||||
const definitions = this.definitionsOf(kind);
|
||||
|
@ -279,10 +214,6 @@ export class ConstantPool {
|
|||
|
||||
private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }
|
||||
|
||||
private freshTranslationName(suffix: string): string {
|
||||
return this.uniqueName(TRANSLATION_PREFIX + suffix).toUpperCase();
|
||||
}
|
||||
|
||||
private keyOf(expression: o.Expression) {
|
||||
return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT);
|
||||
}
|
||||
|
@ -350,20 +281,3 @@ 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 (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.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) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||
}
|
||||
|
||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||
}
|
||||
|
|
|
@ -95,6 +95,8 @@ export class IcuPlaceholder implements Node {
|
|||
visit(visitor: Visitor, context?: any): any { return visitor.visitIcuPlaceholder(this, context); }
|
||||
}
|
||||
|
||||
export type AST = Message | Node;
|
||||
|
||||
export interface Visitor {
|
||||
visitText(text: Text, context?: any): any;
|
||||
visitContainer(container: Container, context?: any): any;
|
||||
|
|
|
@ -18,15 +18,19 @@ import {PlaceholderRegistry} from './serializers/placeholder';
|
|||
|
||||
const _expParser = new ExpressionParser(new ExpressionLexer());
|
||||
|
||||
type VisitNodeFn = (html: html.Node, i18n: i18n.Node) => void;
|
||||
|
||||
/**
|
||||
* Returns a function converting html nodes to an i18n Message given an interpolationConfig
|
||||
*/
|
||||
export function createI18nMessageFactory(interpolationConfig: InterpolationConfig): (
|
||||
nodes: html.Node[], meaning: string, description: string, id: string) => i18n.Message {
|
||||
nodes: html.Node[], meaning: string, description: string, id: string,
|
||||
visitNodeFn?: VisitNodeFn) => i18n.Message {
|
||||
const visitor = new _I18nVisitor(_expParser, interpolationConfig);
|
||||
|
||||
return (nodes: html.Node[], meaning: string, description: string, id: string) =>
|
||||
visitor.toI18nMessage(nodes, meaning, description, id);
|
||||
return (nodes: html.Node[], meaning: string, description: string, id: string,
|
||||
visitNodeFn?: VisitNodeFn) =>
|
||||
visitor.toI18nMessage(nodes, meaning, description, id, visitNodeFn);
|
||||
}
|
||||
|
||||
class _I18nVisitor implements html.Visitor {
|
||||
|
@ -40,18 +44,21 @@ class _I18nVisitor implements html.Visitor {
|
|||
private _placeholderToContent !: {[phName: string]: string};
|
||||
// TODO(issue/24571): remove '!'.
|
||||
private _placeholderToMessage !: {[phName: string]: i18n.Message};
|
||||
private _visitNodeFn: VisitNodeFn|undefined;
|
||||
|
||||
constructor(
|
||||
private _expressionParser: ExpressionParser,
|
||||
private _interpolationConfig: InterpolationConfig) {}
|
||||
|
||||
public toI18nMessage(nodes: html.Node[], meaning: string, description: string, id: string):
|
||||
i18n.Message {
|
||||
public toI18nMessage(
|
||||
nodes: html.Node[], meaning: string, description: string, id: string,
|
||||
visitNodeFn?: VisitNodeFn): i18n.Message {
|
||||
this._isIcu = nodes.length == 1 && nodes[0] instanceof html.Expansion;
|
||||
this._icuDepth = 0;
|
||||
this._placeholderRegistry = new PlaceholderRegistry();
|
||||
this._placeholderToContent = {};
|
||||
this._placeholderToMessage = {};
|
||||
this._visitNodeFn = visitNodeFn;
|
||||
|
||||
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
|
||||
|
||||
|
@ -59,6 +66,13 @@ class _I18nVisitor implements html.Visitor {
|
|||
i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description, id);
|
||||
}
|
||||
|
||||
private _visitNode(html: html.Node, i18n: i18n.Node): i18n.Node {
|
||||
if (this._visitNodeFn) {
|
||||
this._visitNodeFn(html, i18n);
|
||||
}
|
||||
return i18n;
|
||||
}
|
||||
|
||||
visitElement(el: html.Element, context: any): i18n.Node {
|
||||
const children = html.visitAll(this, el.children);
|
||||
const attrs: {[k: string]: string} = {};
|
||||
|
@ -79,16 +93,19 @@ class _I18nVisitor implements html.Visitor {
|
|||
this._placeholderToContent[closePhName] = `</${el.name}>`;
|
||||
}
|
||||
|
||||
return new i18n.TagPlaceholder(
|
||||
const node = new i18n.TagPlaceholder(
|
||||
el.name, attrs, startPhName, closePhName, children, isVoid, el.sourceSpan !);
|
||||
return this._visitNode(el, node);
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): i18n.Node {
|
||||
return this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan);
|
||||
const node = this._visitTextWithInterpolation(attribute.value, attribute.sourceSpan);
|
||||
return this._visitNode(attribute, node);
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: any): i18n.Node {
|
||||
return this._visitTextWithInterpolation(text.value, text.sourceSpan !);
|
||||
const node = this._visitTextWithInterpolation(text.value, text.sourceSpan !);
|
||||
return this._visitNode(text, node);
|
||||
}
|
||||
|
||||
visitComment(comment: html.Comment, context: any): i18n.Node|null { return null; }
|
||||
|
@ -110,8 +127,7 @@ class _I18nVisitor implements html.Visitor {
|
|||
const expPh = this._placeholderRegistry.getUniquePlaceholder(`VAR_${icu.type}`);
|
||||
i18nIcu.expressionPlaceholder = expPh;
|
||||
this._placeholderToContent[expPh] = icu.switchValue;
|
||||
|
||||
return i18nIcu;
|
||||
return this._visitNode(icu, i18nIcu);
|
||||
}
|
||||
|
||||
// Else returns a placeholder
|
||||
|
@ -122,7 +138,8 @@ class _I18nVisitor implements html.Visitor {
|
|||
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
|
||||
const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
|
||||
this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', '', '');
|
||||
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
|
||||
const node = new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
|
||||
return this._visitNode(icu, node);
|
||||
}
|
||||
|
||||
visitExpansionCase(icuCase: html.ExpansionCase, context: any): i18n.Node {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import {AstPath} from '../ast_path';
|
||||
import {AST as I18nAST} from '../i18n/i18n_ast';
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
|
||||
export interface Node {
|
||||
|
@ -15,14 +16,15 @@ export interface Node {
|
|||
}
|
||||
|
||||
export class Text implements Node {
|
||||
constructor(public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
constructor(public value: string, public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitText(this, context); }
|
||||
}
|
||||
|
||||
export class Expansion implements Node {
|
||||
constructor(
|
||||
public switchValue: string, public type: string, public cases: ExpansionCase[],
|
||||
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
|
||||
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan,
|
||||
public i18n?: I18nAST) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitExpansion(this, context); }
|
||||
}
|
||||
|
||||
|
@ -37,7 +39,7 @@ export class ExpansionCase implements Node {
|
|||
export class Attribute implements Node {
|
||||
constructor(
|
||||
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
|
||||
public valueSpan?: ParseSourceSpan) {}
|
||||
public valueSpan?: ParseSourceSpan, public i18n?: I18nAST) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitAttribute(this, context); }
|
||||
}
|
||||
|
||||
|
@ -45,7 +47,7 @@ export class Element implements Node {
|
|||
constructor(
|
||||
public name: string, public attrs: Attribute[], public children: Node[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null = null,
|
||||
public endSourceSpan: ParseSourceSpan|null = null) {}
|
||||
public endSourceSpan: ParseSourceSpan|null = null, public i18n?: I18nAST) {}
|
||||
visit(visitor: Visitor, context: any): any { return visitor.visitElement(this, context); }
|
||||
}
|
||||
|
||||
|
|
|
@ -56,12 +56,12 @@ export class WhitespaceVisitor implements html.Visitor {
|
|||
// but still visit all attributes to eliminate one used as a market to preserve WS
|
||||
return new html.Element(
|
||||
element.name, html.visitAll(this, element.attrs), element.children, element.sourceSpan,
|
||||
element.startSourceSpan, element.endSourceSpan);
|
||||
element.startSourceSpan, element.endSourceSpan, element.i18n);
|
||||
}
|
||||
|
||||
return new html.Element(
|
||||
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
|
||||
element.startSourceSpan, element.endSourceSpan);
|
||||
element.startSourceSpan, element.endSourceSpan, element.i18n);
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute, context: any): any {
|
||||
|
@ -73,7 +73,7 @@ export class WhitespaceVisitor implements html.Visitor {
|
|||
|
||||
if (isNotBlank) {
|
||||
return new html.Text(
|
||||
replaceNgsp(text.value).replace(WS_REPLACE_REGEXP, ' '), text.sourceSpan);
|
||||
replaceNgsp(text.value).replace(WS_REPLACE_REGEXP, ' '), text.sourceSpan, text.i18n);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -20,10 +20,11 @@ export function mapEntry(key: string, value: o.Expression): MapEntry {
|
|||
return {key, value, quoted: false};
|
||||
}
|
||||
|
||||
export function mapLiteral(obj: {[key: string]: o.Expression}): o.Expression {
|
||||
export function mapLiteral(
|
||||
obj: {[key: string]: o.Expression}, quoted: boolean = false): o.Expression {
|
||||
return o.literalMap(Object.keys(obj).map(key => ({
|
||||
key,
|
||||
quoted: false,
|
||||
quoted,
|
||||
value: obj[key],
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import {SecurityContext} from '../core';
|
||||
import {AST, BindingType, BoundElementProperty, ParsedEvent, ParsedEventType} from '../expression_parser/ast';
|
||||
import {AST as I18nAST} from '../i18n/i18n_ast';
|
||||
import {ParseSourceSpan} from '../parse_util';
|
||||
|
||||
export interface Node {
|
||||
|
@ -21,25 +22,26 @@ export class Text implements Node {
|
|||
}
|
||||
|
||||
export class BoundText implements Node {
|
||||
constructor(public value: AST, public sourceSpan: ParseSourceSpan) {}
|
||||
constructor(public value: AST, public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundText(this); }
|
||||
}
|
||||
|
||||
export class TextAttribute implements Node {
|
||||
constructor(
|
||||
public name: string, public value: string, public sourceSpan: ParseSourceSpan,
|
||||
public valueSpan?: ParseSourceSpan) {}
|
||||
public valueSpan?: ParseSourceSpan, public i18n?: I18nAST) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitTextAttribute(this); }
|
||||
}
|
||||
|
||||
export class BoundAttribute implements Node {
|
||||
constructor(
|
||||
public name: string, public type: BindingType, public securityContext: SecurityContext,
|
||||
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan) {}
|
||||
public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan,
|
||||
public i18n?: I18nAST) {}
|
||||
|
||||
static fromBoundElementProperty(prop: BoundElementProperty) {
|
||||
static fromBoundElementProperty(prop: BoundElementProperty, i18n?: I18nAST) {
|
||||
return new BoundAttribute(
|
||||
prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan);
|
||||
prop.name, prop.type, prop.securityContext, prop.value, prop.unit, prop.sourceSpan, i18n);
|
||||
}
|
||||
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundAttribute(this); }
|
||||
|
@ -65,7 +67,7 @@ export class Element implements Node {
|
|||
public name: string, public attributes: TextAttribute[], public inputs: BoundAttribute[],
|
||||
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan|null,
|
||||
public endSourceSpan: ParseSourceSpan|null) {}
|
||||
public endSourceSpan: ParseSourceSpan|null, public i18n?: I18nAST) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitElement(this); }
|
||||
}
|
||||
|
||||
|
@ -74,14 +76,15 @@ export class Template implements Node {
|
|||
public attributes: TextAttribute[], public inputs: BoundAttribute[],
|
||||
public outputs: BoundEvent[], public children: Node[], public references: Reference[],
|
||||
public variables: Variable[], public sourceSpan: ParseSourceSpan,
|
||||
public startSourceSpan: ParseSourceSpan|null, public endSourceSpan: ParseSourceSpan|null) {}
|
||||
public startSourceSpan: ParseSourceSpan|null, public endSourceSpan: ParseSourceSpan|null,
|
||||
public i18n?: I18nAST) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitTemplate(this); }
|
||||
}
|
||||
|
||||
export class Content implements Node {
|
||||
constructor(
|
||||
public selectorIndex: number, public attributes: TextAttribute[],
|
||||
public sourceSpan: ParseSourceSpan) {}
|
||||
public sourceSpan: ParseSourceSpan, public i18n?: I18nAST) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitContent(this); }
|
||||
}
|
||||
|
||||
|
@ -95,6 +98,14 @@ export class Reference implements Node {
|
|||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitReference(this); }
|
||||
}
|
||||
|
||||
export class Icu implements Node {
|
||||
constructor(
|
||||
public vars: {[name: string]: BoundText},
|
||||
public placeholders: {[name: string]: Text | BoundText}, public sourceSpan: ParseSourceSpan,
|
||||
public i18n?: I18nAST) {}
|
||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitIcu(this); }
|
||||
}
|
||||
|
||||
export interface Visitor<Result = any> {
|
||||
// Returning a truthy value from `visit()` will prevent `visitAll()` from the call to the typed
|
||||
// method and result returned will become the result included in `visitAll()`s result array.
|
||||
|
@ -110,6 +121,7 @@ export interface Visitor<Result = any> {
|
|||
visitBoundEvent(attribute: BoundEvent): Result;
|
||||
visitText(text: Text): Result;
|
||||
visitBoundText(text: BoundText): Result;
|
||||
visitIcu(icu: Icu): Result;
|
||||
}
|
||||
|
||||
export class NullVisitor implements Visitor<void> {
|
||||
|
@ -123,6 +135,7 @@ export class NullVisitor implements Visitor<void> {
|
|||
visitBoundEvent(attribute: BoundEvent): void {}
|
||||
visitText(text: Text): void {}
|
||||
visitBoundText(text: BoundText): void {}
|
||||
visitIcu(icu: Icu): void {}
|
||||
}
|
||||
|
||||
export class RecursiveVisitor implements Visitor<void> {
|
||||
|
@ -145,6 +158,7 @@ export class RecursiveVisitor implements Visitor<void> {
|
|||
visitBoundEvent(attribute: BoundEvent): void {}
|
||||
visitText(text: Text): void {}
|
||||
visitBoundText(text: BoundText): void {}
|
||||
visitIcu(icu: Icu): void {}
|
||||
}
|
||||
|
||||
export class TransformVisitor implements Visitor<Node> {
|
||||
|
@ -190,6 +204,7 @@ export class TransformVisitor implements Visitor<Node> {
|
|||
visitBoundEvent(attribute: BoundEvent): Node { return attribute; }
|
||||
visitText(text: Text): Node { return text; }
|
||||
visitBoundText(text: BoundText): Node { return text; }
|
||||
visitIcu(icu: Icu): Node { return icu; }
|
||||
}
|
||||
|
||||
export function visitAll<Result>(visitor: Visitor<Result>, nodes: Node[]): Result[] {
|
||||
|
|
|
@ -97,11 +97,13 @@ export class Identifiers {
|
|||
static pipeBind4: o.ExternalReference = {name: 'ɵpipeBind4', moduleName: CORE};
|
||||
static pipeBindV: o.ExternalReference = {name: 'ɵpipeBindV', moduleName: CORE};
|
||||
|
||||
static i18n: o.ExternalReference = {name: 'ɵi18n', moduleName: CORE};
|
||||
static i18nAttributes: o.ExternalReference = {name: 'ɵi18nAttributes', moduleName: CORE};
|
||||
static i18nExp: o.ExternalReference = {name: 'ɵi18nExp', moduleName: CORE};
|
||||
static i18nStart: o.ExternalReference = {name: 'ɵi18nStart', moduleName: CORE};
|
||||
static i18nEnd: o.ExternalReference = {name: 'ɵi18nEnd', moduleName: CORE};
|
||||
static i18nApply: o.ExternalReference = {name: 'ɵi18nApply', moduleName: CORE};
|
||||
static i18nPostprocess: o.ExternalReference = {name: 'ɵi18nPostprocess', moduleName: CORE};
|
||||
|
||||
static load: o.ExternalReference = {name: 'ɵload', moduleName: CORE};
|
||||
static loadQueryList: o.ExternalReference = {name: 'ɵloadQueryList', moduleName: CORE};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import {ParsedEvent, ParsedProperty, ParsedVariable} from '../expression_parser/ast';
|
||||
import * as i18n from '../i18n/i18n_ast';
|
||||
import * as html from '../ml_parser/ast';
|
||||
import {replaceNgsp} from '../ml_parser/html_whitespaces';
|
||||
import {isNgTemplate} from '../ml_parser/tags';
|
||||
|
@ -17,7 +18,7 @@ import {PreparsedElementType, preparseElement} from '../template_parser/template
|
|||
import {syntaxError} from '../util';
|
||||
|
||||
import * as t from './r3_ast';
|
||||
|
||||
import {I18N_ICU_VAR_PREFIX} from './view/i18n/util';
|
||||
|
||||
const BIND_NAME_REGEXP =
|
||||
/^(?:(?:(?:(bind-)|(let-)|(ref-|#)|(on-)|(bindon-)|(@))(.+))|\[\(([^\)]+)\)\]|\[([^\]]+)\]|\(([^\)]+)\))$/;
|
||||
|
@ -112,6 +113,7 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||
const variables: t.Variable[] = [];
|
||||
const references: t.Reference[] = [];
|
||||
const attributes: t.TextAttribute[] = [];
|
||||
const i18nAttrsMeta: {[key: string]: i18n.AST} = {};
|
||||
|
||||
const templateParsedProperties: ParsedProperty[] = [];
|
||||
const templateVariables: t.Variable[] = [];
|
||||
|
@ -126,6 +128,10 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||
// `*attr` defines template bindings
|
||||
let isTemplateBinding = false;
|
||||
|
||||
if (attribute.i18n) {
|
||||
i18nAttrsMeta[attribute.name] = attribute.i18n;
|
||||
}
|
||||
|
||||
if (normalizedName.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
||||
// *-attributes
|
||||
if (elementHasInlineTemplate) {
|
||||
|
@ -175,61 +181,83 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||
|
||||
const selectorIndex =
|
||||
selector === DEFAULT_CONTENT_SELECTOR ? 0 : this.ngContentSelectors.push(selector);
|
||||
parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan);
|
||||
parsedElement = new t.Content(selectorIndex, attributes, element.sourceSpan, element.i18n);
|
||||
} else if (isTemplateElement) {
|
||||
// `<ng-template>`
|
||||
const attrs = this.extractAttributes(element.name, parsedProperties);
|
||||
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
||||
|
||||
parsedElement = new t.Template(
|
||||
attributes, attrs.bound, boundEvents, children, references, variables, element.sourceSpan,
|
||||
element.startSourceSpan, element.endSourceSpan);
|
||||
element.startSourceSpan, element.endSourceSpan, element.i18n);
|
||||
} else {
|
||||
const attrs = this.extractAttributes(element.name, parsedProperties);
|
||||
|
||||
const attrs = this.extractAttributes(element.name, parsedProperties, i18nAttrsMeta);
|
||||
parsedElement = new t.Element(
|
||||
element.name, attributes, attrs.bound, boundEvents, children, references,
|
||||
element.sourceSpan, element.startSourceSpan, element.endSourceSpan);
|
||||
element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.i18n);
|
||||
}
|
||||
|
||||
if (elementHasInlineTemplate) {
|
||||
const attrs = this.extractAttributes('ng-template', templateParsedProperties);
|
||||
const attrs = this.extractAttributes('ng-template', templateParsedProperties, i18nAttrsMeta);
|
||||
// TODO(pk): test for this case
|
||||
parsedElement = new t.Template(
|
||||
attrs.literal, attrs.bound, [], [parsedElement], [], templateVariables,
|
||||
element.sourceSpan, element.startSourceSpan, element.endSourceSpan);
|
||||
element.sourceSpan, element.startSourceSpan, element.endSourceSpan, element.i18n);
|
||||
}
|
||||
return parsedElement;
|
||||
}
|
||||
|
||||
visitAttribute(attribute: html.Attribute): t.TextAttribute {
|
||||
return new t.TextAttribute(
|
||||
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan);
|
||||
attribute.name, attribute.value, attribute.sourceSpan, attribute.valueSpan, attribute.i18n);
|
||||
}
|
||||
|
||||
visitText(text: html.Text): t.Node {
|
||||
const valueNoNgsp = replaceNgsp(text.value);
|
||||
const expr = this.bindingParser.parseInterpolation(valueNoNgsp, text.sourceSpan);
|
||||
return expr ? new t.BoundText(expr, text.sourceSpan) : new t.Text(valueNoNgsp, text.sourceSpan);
|
||||
return this._visitTextWithInterpolation(text.value, text.sourceSpan, text.i18n);
|
||||
}
|
||||
|
||||
visitComment(comment: html.Comment): null { return null; }
|
||||
|
||||
visitExpansion(expansion: html.Expansion): null { return null; }
|
||||
visitExpansion(expansion: html.Expansion): t.Icu|null {
|
||||
const meta = expansion.i18n as i18n.Message;
|
||||
// do not generate Icu in case it was created
|
||||
// outside of i18n block in a template
|
||||
if (!meta) {
|
||||
return null;
|
||||
}
|
||||
const vars: {[name: string]: t.BoundText} = {};
|
||||
const placeholders: {[name: string]: t.Text | t.BoundText} = {};
|
||||
// extract VARs from ICUs - we process them separately while
|
||||
// assembling resulting message via goog.getMsg function, since
|
||||
// we need to pass them to top-level goog.getMsg call
|
||||
Object.keys(meta.placeholders).forEach(key => {
|
||||
const value = meta.placeholders[key];
|
||||
if (key.startsWith(I18N_ICU_VAR_PREFIX)) {
|
||||
vars[key] =
|
||||
this._visitTextWithInterpolation(`{{${value}}}`, expansion.sourceSpan) as t.BoundText;
|
||||
} else {
|
||||
placeholders[key] = this._visitTextWithInterpolation(value, expansion.sourceSpan);
|
||||
}
|
||||
});
|
||||
return new t.Icu(vars, placeholders, expansion.sourceSpan, meta);
|
||||
}
|
||||
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase): null { return null; }
|
||||
|
||||
visitComment(comment: html.Comment): null { return null; }
|
||||
|
||||
// convert view engine `ParsedProperty` to a format suitable for IVY
|
||||
private extractAttributes(elementName: string, properties: ParsedProperty[]):
|
||||
private extractAttributes(
|
||||
elementName: string, properties: ParsedProperty[], i18nPropsMeta: {[key: string]: i18n.AST}):
|
||||
{bound: t.BoundAttribute[], literal: t.TextAttribute[]} {
|
||||
const bound: t.BoundAttribute[] = [];
|
||||
const literal: t.TextAttribute[] = [];
|
||||
|
||||
properties.forEach(prop => {
|
||||
const i18n = i18nPropsMeta[prop.name];
|
||||
if (prop.isLiteral) {
|
||||
literal.push(new t.TextAttribute(prop.name, prop.expression.source || '', prop.sourceSpan));
|
||||
literal.push(new t.TextAttribute(
|
||||
prop.name, prop.expression.source || '', prop.sourceSpan, undefined, i18n));
|
||||
} else {
|
||||
const bep = this.bindingParser.createBoundElementProperty(elementName, prop);
|
||||
bound.push(t.BoundAttribute.fromBoundElementProperty(bep));
|
||||
bound.push(t.BoundAttribute.fromBoundElementProperty(bep, i18n));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -305,6 +333,13 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||
return hasBinding;
|
||||
}
|
||||
|
||||
private _visitTextWithInterpolation(value: string, sourceSpan: ParseSourceSpan, i18n?: i18n.AST):
|
||||
t.Text|t.BoundText {
|
||||
const valueNoNgsp = replaceNgsp(value);
|
||||
const expr = this.bindingParser.parseInterpolation(valueNoNgsp, sourceSpan);
|
||||
return expr ? new t.BoundText(expr, sourceSpan, i18n) : new t.Text(valueNoNgsp, sourceSpan);
|
||||
}
|
||||
|
||||
private parseVariable(
|
||||
identifier: string, value: string, sourceSpan: ParseSourceSpan, variables: t.Variable[]) {
|
||||
if (identifier.indexOf('-') > -1) {
|
||||
|
@ -360,7 +395,8 @@ class NonBindableVisitor implements html.Visitor {
|
|||
visitComment(comment: html.Comment): any { return null; }
|
||||
|
||||
visitAttribute(attribute: html.Attribute): t.TextAttribute {
|
||||
return new t.TextAttribute(attribute.name, attribute.value, attribute.sourceSpan);
|
||||
return new t.TextAttribute(
|
||||
attribute.name, attribute.value, attribute.sourceSpan, undefined, attribute.i18n);
|
||||
}
|
||||
|
||||
visitText(text: html.Text): t.Text { return new t.Text(text.value, text.sourceSpan); }
|
||||
|
|
|
@ -1,140 +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 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 = '<27>';
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* @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 {assembleBoundTextPlaceholders, findIndex, getSeqNumberGenerator, updatePlaceholderMap, wrapI18nPlaceholder} from './util';
|
||||
|
||||
enum TagType {
|
||||
ELEMENT,
|
||||
TEMPLATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an object that is used as a shared state between parent and all child contexts.
|
||||
*/
|
||||
function setupRegistry() {
|
||||
return {getUniqueId: getSeqNumberGenerator(), icus: new Map<string, any[]>()};
|
||||
}
|
||||
|
||||
/**
|
||||
* I18nContext is a helper class which keeps track of all i18n-related aspects
|
||||
* (accumulates placeholders, 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.
|
||||
*
|
||||
* @param index Instruction index of i18nStart, which initiates this context
|
||||
* @param ref Reference to a translation const that represents the content if thus context
|
||||
* @param level Nestng level defined for child contexts
|
||||
* @param templateIndex Instruction index of a template which this context belongs to
|
||||
* @param meta Meta information (id, meaning, description, etc) associated with this context
|
||||
*/
|
||||
export class I18nContext {
|
||||
public readonly id: number;
|
||||
public bindings = new Set<o.Expression>();
|
||||
public placeholders = new Map<string, any[]>();
|
||||
|
||||
private _registry !: any;
|
||||
private _unresolvedCtxCount: number = 0;
|
||||
|
||||
constructor(
|
||||
readonly index: number, readonly ref: o.ReadVarExpr, readonly level: number = 0,
|
||||
readonly templateIndex: number|null = null, readonly meta: i18n.AST, private registry?: any) {
|
||||
this._registry = registry || setupRegistry();
|
||||
this.id = this._registry.getUniqueId();
|
||||
}
|
||||
|
||||
private appendTag(type: TagType, node: i18n.TagPlaceholder, index: number, closed?: boolean) {
|
||||
if (node.isVoid && closed) {
|
||||
return; // ignore "close" for void tags
|
||||
}
|
||||
const ph = node.isVoid || !closed ? node.startName : node.closeName;
|
||||
const content = {type, index, ctx: this.id, isVoid: node.isVoid, closed};
|
||||
updatePlaceholderMap(this.placeholders, ph, content);
|
||||
}
|
||||
|
||||
get icus() { return this._registry.icus; }
|
||||
get isRoot() { return this.level === 0; }
|
||||
get isResolved() { return this._unresolvedCtxCount === 0; }
|
||||
|
||||
getSerializedPlaceholders() {
|
||||
const result = new Map<string, any[]>();
|
||||
this.placeholders.forEach(
|
||||
(values, key) => result.set(key, values.map(serializePlaceholderValue)));
|
||||
return result;
|
||||
}
|
||||
|
||||
// public API to accumulate i18n-related content
|
||||
appendBinding(binding: o.Expression) { this.bindings.add(binding); }
|
||||
appendIcu(name: string, ref: o.Expression) {
|
||||
updatePlaceholderMap(this._registry.icus, name, ref);
|
||||
}
|
||||
appendBoundText(node: i18n.AST) {
|
||||
const phs = assembleBoundTextPlaceholders(node, this.bindings.size, this.id);
|
||||
phs.forEach((values, key) => updatePlaceholderMap(this.placeholders, key, ...values));
|
||||
}
|
||||
appendTemplate(node: i18n.AST, index: number) {
|
||||
// add open and close tags at the same time,
|
||||
// since we process nested templates separately
|
||||
this.appendTag(TagType.TEMPLATE, node as i18n.TagPlaceholder, index, false);
|
||||
this.appendTag(TagType.TEMPLATE, node as i18n.TagPlaceholder, index, true);
|
||||
this._unresolvedCtxCount++;
|
||||
}
|
||||
appendElement(node: i18n.AST, index: number, closed?: boolean) {
|
||||
this.appendTag(TagType.ELEMENT, node as i18n.TagPlaceholder, index, closed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an instance of a child context based on the root one,
|
||||
* when we enter a nested template within I18n section.
|
||||
*
|
||||
* @param index Instruction index of corresponding i18nStart, which initiates this context
|
||||
* @param templateIndex Instruction index of a template which this context belongs to
|
||||
* @param meta Meta information (id, meaning, description, etc) associated with this context
|
||||
*
|
||||
* @returns I18nContext instance
|
||||
*/
|
||||
forkChildContext(index: number, templateIndex: number, meta: i18n.AST) {
|
||||
return new I18nContext(index, this.ref, this.level + 1, templateIndex, meta, this._registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconciles child context into parent one once the end of the i18n block is reached (i18nEnd).
|
||||
*
|
||||
* @param context Child I18nContext instance to be reconciled with parent context.
|
||||
*/
|
||||
reconcileChildContext(context: I18nContext) {
|
||||
// set the right context id for open and close
|
||||
// template tags, so we can use it as sub-block ids
|
||||
['start', 'close'].forEach((op: string) => {
|
||||
const key = (context.meta as any)[`${op}Name`];
|
||||
const phs = this.placeholders.get(key) || [];
|
||||
const tag = phs.find(findTemplateFn(this.id, context.templateIndex));
|
||||
if (tag) {
|
||||
tag.ctx = context.id;
|
||||
}
|
||||
});
|
||||
|
||||
// reconcile placeholders
|
||||
const childPhs = context.placeholders;
|
||||
childPhs.forEach((values: any[], key: string) => {
|
||||
const phs = this.placeholders.get(key);
|
||||
if (!phs) {
|
||||
this.placeholders.set(key, values);
|
||||
return;
|
||||
}
|
||||
// try to find matching template...
|
||||
const tmplIdx = findIndex(phs, findTemplateFn(context.id, context.templateIndex));
|
||||
if (tmplIdx >= 0) {
|
||||
// ... if found - replace it with nested template content
|
||||
const isCloseTag = key.startsWith('CLOSE');
|
||||
const isTemplateTag = key.endsWith('NG-TEMPLATE');
|
||||
if (isTemplateTag) {
|
||||
// current template's content is placed before or after
|
||||
// parent template tag, depending on the open/close atrribute
|
||||
phs.splice(tmplIdx + (isCloseTag ? 0 : 1), 0, ...values);
|
||||
} else {
|
||||
const idx = isCloseTag ? values.length - 1 : 0;
|
||||
values[idx].tmpl = phs[tmplIdx];
|
||||
phs.splice(tmplIdx, 1, ...values);
|
||||
}
|
||||
} else {
|
||||
// ... otherwise just append content to placeholder value
|
||||
phs.push(...values);
|
||||
}
|
||||
this.placeholders.set(key, phs);
|
||||
});
|
||||
this._unresolvedCtxCount--;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helper methods
|
||||
//
|
||||
|
||||
function wrap(symbol: string, index: number, contextId: number, closed?: boolean): string {
|
||||
const state = closed ? '/' : '';
|
||||
return wrapI18nPlaceholder(`${state}${symbol}${index}`, contextId);
|
||||
}
|
||||
|
||||
function wrapTag(symbol: string, {index, ctx, isVoid}: any, closed?: boolean): string {
|
||||
return isVoid ? wrap(symbol, index, ctx) + wrap(symbol, index, ctx, true) :
|
||||
wrap(symbol, index, ctx, closed);
|
||||
}
|
||||
|
||||
function findTemplateFn(ctx: number, templateIndex: number | null) {
|
||||
return (token: any) => typeof token === 'object' && token.type === TagType.TEMPLATE &&
|
||||
token.index === templateIndex && token.ctx === ctx;
|
||||
}
|
||||
|
||||
function serializePlaceholderValue(value: any): string {
|
||||
const element = (data: any, closed?: boolean) => wrapTag('#', data, closed);
|
||||
const template = (data: any, closed?: boolean) => wrapTag('*', data, closed);
|
||||
|
||||
switch (value.type) {
|
||||
case TagType.ELEMENT:
|
||||
// close element tag
|
||||
if (value.closed) {
|
||||
return element(value, true) + (value.tmpl ? template(value.tmpl, true) : '');
|
||||
}
|
||||
// open element tag that also initiates a template
|
||||
if (value.tmpl) {
|
||||
return template(value.tmpl) + element(value) +
|
||||
(value.isVoid ? template(value.tmpl, true) : '');
|
||||
}
|
||||
return element(value);
|
||||
|
||||
case TagType.TEMPLATE:
|
||||
return template(value, value.closed);
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* @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 {decimalDigest} from '../../../i18n/digest';
|
||||
import * as i18n from '../../../i18n/i18n_ast';
|
||||
import {createI18nMessageFactory} from '../../../i18n/i18n_parser';
|
||||
import * as html from '../../../ml_parser/ast';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../ml_parser/interpolation_config';
|
||||
import {ParseTreeResult} from '../../../ml_parser/parser';
|
||||
|
||||
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nMeta, hasI18nAttrs, icuFromI18nMessage, metaFromI18nMessage, parseI18nMeta} from './util';
|
||||
|
||||
function setI18nRefs(html: html.Node & {i18n: i18n.AST}, i18n: i18n.Node) {
|
||||
html.i18n = i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* This visitor walks over HTML parse tree and converts information stored in
|
||||
* i18n-related attributes ("i18n" and "i18n-*") into i18n meta object that is
|
||||
* stored with other element's and attribute's information.
|
||||
*/
|
||||
export class I18nMetaVisitor implements html.Visitor {
|
||||
// i18n message generation factory
|
||||
private _createI18nMessage = createI18nMessageFactory(DEFAULT_INTERPOLATION_CONFIG);
|
||||
|
||||
constructor(private config: {keepI18nAttrs: boolean}) {}
|
||||
|
||||
private _generateI18nMessage(
|
||||
nodes: html.Node[], meta: string|i18n.AST = '',
|
||||
visitNodeFn?: (html: html.Node, i18n: i18n.Node) => void): i18n.Message {
|
||||
const parsed: I18nMeta =
|
||||
typeof meta === 'string' ? parseI18nMeta(meta) : metaFromI18nMessage(meta as i18n.Message);
|
||||
const message = this._createI18nMessage(
|
||||
nodes, parsed.meaning || '', parsed.description || '', parsed.id || '', visitNodeFn);
|
||||
if (!message.id) {
|
||||
// generate (or restore) message id if not specified in template
|
||||
message.id = typeof meta !== 'string' && (meta as i18n.Message).id || decimalDigest(message);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
visitElement(element: html.Element, context: any): any {
|
||||
if (hasI18nAttrs(element)) {
|
||||
const attrs: html.Attribute[] = [];
|
||||
const attrsMeta: {[key: string]: string} = {};
|
||||
|
||||
for (const attr of element.attrs) {
|
||||
if (attr.name === I18N_ATTR) {
|
||||
// root 'i18n' node attribute
|
||||
const i18n = element.i18n || attr.value;
|
||||
const message = this._generateI18nMessage(element.children, i18n, setI18nRefs);
|
||||
// do not assign empty i18n meta
|
||||
if (message.nodes.length) {
|
||||
element.i18n = message;
|
||||
}
|
||||
|
||||
} else if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
|
||||
// 'i18n-*' attributes
|
||||
const key = attr.name.slice(I18N_ATTR_PREFIX.length);
|
||||
attrsMeta[key] = attr.value;
|
||||
|
||||
} else {
|
||||
// non-i18n attributes
|
||||
attrs.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
// set i18n meta for attributes
|
||||
if (Object.keys(attrsMeta).length) {
|
||||
for (const attr of attrs) {
|
||||
const meta = attrsMeta[attr.name];
|
||||
// do not create translation for empty attributes
|
||||
if (meta !== undefined && attr.value) {
|
||||
attr.i18n = this._generateI18nMessage([attr], attr.i18n || meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.config.keepI18nAttrs) {
|
||||
// update element's attributes,
|
||||
// keeping only non-i18n related ones
|
||||
element.attrs = attrs;
|
||||
}
|
||||
}
|
||||
html.visitAll(this, element.children);
|
||||
return element;
|
||||
}
|
||||
|
||||
visitExpansion(expansion: html.Expansion, context: any): any {
|
||||
let message;
|
||||
const meta = expansion.i18n;
|
||||
if (meta instanceof i18n.IcuPlaceholder) {
|
||||
// set ICU placeholder name (e.g. "ICU_1"),
|
||||
// generated while processing root element contents,
|
||||
// so we can reference it when we output translation
|
||||
const name = meta.name;
|
||||
message = this._generateI18nMessage([expansion], meta);
|
||||
const icu = icuFromI18nMessage(message);
|
||||
icu.name = name;
|
||||
} else {
|
||||
// when ICU is a root level translation
|
||||
message = this._generateI18nMessage([expansion], meta);
|
||||
}
|
||||
expansion.i18n = message;
|
||||
return expansion;
|
||||
}
|
||||
|
||||
visitText(text: html.Text, context: any): any { return text; }
|
||||
visitAttribute(attribute: html.Attribute, context: any): any { return attribute; }
|
||||
visitComment(comment: html.Comment, context: any): any { return comment; }
|
||||
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
|
||||
}
|
||||
|
||||
export function processI18nMeta(htmlAstWithErrors: ParseTreeResult): ParseTreeResult {
|
||||
return new ParseTreeResult(
|
||||
html.visitAll(new I18nMetaVisitor({keepI18nAttrs: false}), htmlAstWithErrors.rootNodes),
|
||||
htmlAstWithErrors.errors);
|
||||
}
|
|
@ -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';
|
||||
|
||||
const formatPh = (value: string): string => `{$${formatI18nPlaceholderName(value)}}`;
|
||||
|
||||
/**
|
||||
* This visitor walks over i18n tree and generates its string representation,
|
||||
* including ICUs and placeholders in {$PLACEHOLDER} format.
|
||||
*/
|
||||
class SerializerVisitor implements i18n.Visitor {
|
||||
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 {
|
||||
const strCases =
|
||||
Object.keys(icu.cases).map((k: string) => `${k} {${icu.cases[k].visit(this)}}`);
|
||||
return `{${icu.expressionPlaceholder}, ${icu.type}, ${strCases.join(' ')}}`;
|
||||
}
|
||||
|
||||
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: any): any {
|
||||
return ph.isVoid ?
|
||||
formatPh(ph.startName) :
|
||||
`${formatPh(ph.startName)}${ph.children.map(child => child.visit(this)).join('')}${formatPh(ph.closeName)}`;
|
||||
}
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder, context: any): any { return formatPh(ph.name); }
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { return formatPh(ph.name); }
|
||||
}
|
||||
|
||||
const serializerVisitor = new SerializerVisitor();
|
||||
|
||||
export function getSerializedI18nContent(message: i18n.Message): string {
|
||||
return message.nodes.map(node => node.visit(serializerVisitor, null)).join('');
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* @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 {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';
|
||||
|
||||
|
||||
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
|
||||
const TRANSLATION_PREFIX = 'MSG_';
|
||||
|
||||
/** Closure uses `goog.getMsg(message)` to lookup translations */
|
||||
const GOOG_GET_MSG = 'goog.getMsg';
|
||||
|
||||
/** String key that is used to provide backup id of translatable message in Closure */
|
||||
const BACKUP_MESSAGE_ID = 'BACKUP_MESSAGE_ID';
|
||||
|
||||
/** Regexp to identify whether backup id already provided in description */
|
||||
const BACKUP_MESSAGE_ID_REGEXP = new RegExp(BACKUP_MESSAGE_ID);
|
||||
|
||||
/** 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-';
|
||||
|
||||
/** Prefix of var expressions used in ICUs */
|
||||
export const I18N_ICU_VAR_PREFIX = 'VAR_';
|
||||
|
||||
/** Prefix of ICU expressions for post processing */
|
||||
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, message: string,
|
||||
params?: {[name: string]: o.Expression}): o.DeclareVarStmt {
|
||||
const args = [o.literal(message) as o.Expression];
|
||||
if (params && Object.keys(params).length) {
|
||||
args.push(mapLiteral(params, true));
|
||||
}
|
||||
const fnCall = o.variable(GOOG_GET_MSG).callFn(args);
|
||||
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||
}
|
||||
|
||||
// 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[] = [];
|
||||
const {id, description, meaning} = meta;
|
||||
if (id || description) {
|
||||
const hasBackupId = !!description && BACKUP_MESSAGE_ID_REGEXP.test(description);
|
||||
const text =
|
||||
id && !hasBackupId ? `[${BACKUP_MESSAGE_ID}:${id}] ${description || ''}` : description;
|
||||
tags.push({tagName: o.JSDocTagName.Desc, text: text !.trim()});
|
||||
}
|
||||
if (meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: 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);
|
||||
}
|
||||
|
||||
export function isI18nRootNode(meta?: i18n.AST): meta is i18n.Message {
|
||||
return meta instanceof i18n.Message;
|
||||
}
|
||||
|
||||
export function isSingleI18nIcu(meta?: i18n.AST): boolean {
|
||||
return isI18nRootNode(meta) && meta.nodes.length === 1 && meta.nodes[0] instanceof i18n.Icu;
|
||||
}
|
||||
|
||||
export function hasI18nAttrs(element: html.Element): boolean {
|
||||
return element.attrs.some((attr: html.Attribute) => isI18nAttribute(attr.name));
|
||||
}
|
||||
|
||||
export function metaFromI18nMessage(message: i18n.Message): I18nMeta {
|
||||
return {
|
||||
id: message.id || '',
|
||||
meaning: message.meaning || '',
|
||||
description: message.description || ''
|
||||
};
|
||||
}
|
||||
|
||||
export function icuFromI18nMessage(message: i18n.Message) {
|
||||
return message.nodes[0] as i18n.IcuPlaceholder;
|
||||
}
|
||||
|
||||
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: 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;
|
||||
}
|
||||
|
||||
export function getSeqNumberGenerator(startsAt: number = 0): () => number {
|
||||
let current = startsAt;
|
||||
return () => current++;
|
||||
}
|
||||
|
||||
export function placeholdersToParams(placeholders: Map<string, string[]>):
|
||||
{[name: string]: o.Expression} {
|
||||
const params: {[name: string]: o.Expression} = {};
|
||||
placeholders.forEach((values: string[], key: string) => {
|
||||
params[key] = o.literal(values.length > 1 ? `[${values.join('|')}]` : values[0]);
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
export function updatePlaceholderMap(map: Map<string, any[]>, name: string, ...values: any[]) {
|
||||
const current = map.get(name) || [];
|
||||
current.push(...values);
|
||||
map.set(name, current);
|
||||
}
|
||||
|
||||
export function assembleBoundTextPlaceholders(
|
||||
meta: i18n.AST, bindingStartIndex: number = 0, contextId: number = 0): Map<string, any[]> {
|
||||
const startIdx = bindingStartIndex;
|
||||
const placeholders = new Map<string, any>();
|
||||
const node =
|
||||
meta instanceof i18n.Message ? meta.nodes.find(node => node instanceof i18n.Container) : meta;
|
||||
if (node) {
|
||||
(node as i18n.Container)
|
||||
.children.filter((child: i18n.Node) => child instanceof i18n.Placeholder)
|
||||
.forEach((child: i18n.Placeholder, idx: number) => {
|
||||
const content = wrapI18nPlaceholder(startIdx + idx, contextId);
|
||||
updatePlaceholderMap(placeholders, child.name, content);
|
||||
});
|
||||
}
|
||||
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.
|
||||
*
|
||||
* @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 internal placeholder names to public-facing format
|
||||
* (for example to use in goog.getMsg call).
|
||||
* Example: `START_TAG_DIV_1` is converted to `startTagDiv_1`.
|
||||
*
|
||||
* @param name The placeholder name that should be formatted
|
||||
* @returns Formatted placeholder name
|
||||
*/
|
||||
export function formatI18nPlaceholderName(name: string): string {
|
||||
const chunks = toPublicName(name).split('_');
|
||||
if (chunks.length === 1) {
|
||||
// if no "_" found - just lowercase the value
|
||||
return name.toLowerCase();
|
||||
}
|
||||
let postfix;
|
||||
// eject last element if it's a number
|
||||
if (/^\d+$/.test(chunks[chunks.length - 1])) {
|
||||
postfix = chunks.pop();
|
||||
}
|
||||
let raw = chunks.shift() !.toLowerCase();
|
||||
if (chunks.length) {
|
||||
raw += chunks.map(c => c.charAt(0).toUpperCase() + c.slice(1).toLowerCase()).join('');
|
||||
}
|
||||
return postfix ? `${raw}_${postfix}` : raw;
|
||||
}
|
||||
|
||||
export function getTranslationConstPrefix(fileBasedSuffix: string): string {
|
||||
return `${TRANSLATION_PREFIX}${fileBasedSuffix}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates translation declaration statements.
|
||||
*
|
||||
* @param variable Translation value reference
|
||||
* @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
|
||||
*/
|
||||
export function getTranslationDeclStmts(
|
||||
variable: o.ReadVarExpr, message: string, meta: I18nMeta,
|
||||
params: {[name: string]: o.Expression} = {},
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] {
|
||||
const statements: o.Statement[] = [];
|
||||
const docStatements = i18nMetaToDocStmt(meta);
|
||||
if (docStatements) {
|
||||
statements.push(docStatements);
|
||||
}
|
||||
if (transformFn) {
|
||||
const raw = o.variable(`${variable.name}_RAW`);
|
||||
statements.push(i18nTranslationToDeclStmt(raw, message, params));
|
||||
statements.push(
|
||||
variable.set(transformFn(raw)).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
||||
} else {
|
||||
statements.push(i18nTranslationToDeclStmt(variable, message, params));
|
||||
}
|
||||
return statements;
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {AST, ImplicitReceiver, MethodCall, PropertyRead, PropertyWrite, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../expression_parser/ast';
|
||||
import {CssSelector, SelectorMatcher} from '../../selector';
|
||||
import {BoundAttribute, BoundEvent, BoundText, Content, Element, Node, Reference, Template, Text, TextAttribute, Variable, Visitor} from '../r3_ast';
|
||||
import {BoundAttribute, BoundEvent, BoundText, Content, Element, Icu, Node, Reference, Template, Text, TextAttribute, Variable, Visitor} from '../r3_ast';
|
||||
|
||||
import {BoundTarget, DirectiveMeta, Target, TargetBinder} from './t2_api';
|
||||
import {getAttrsForDirectiveMatching} from './util';
|
||||
|
@ -132,6 +132,7 @@ class Scope implements Visitor {
|
|||
visitBoundText(text: BoundText) {}
|
||||
visitText(text: Text) {}
|
||||
visitTextAttribute(attr: TextAttribute) {}
|
||||
visitIcu(icu: Icu) {}
|
||||
|
||||
private maybeDeclare(thing: Reference|Variable) {
|
||||
// Declare something with a name, as long as that name isn't taken.
|
||||
|
@ -312,6 +313,7 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
|
|||
visitBoundAttributeOrEvent(node: BoundAttribute|BoundEvent) {}
|
||||
visitText(text: Text): void {}
|
||||
visitBoundText(text: BoundText): void {}
|
||||
visitIcu(icu: Icu): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -423,6 +425,7 @@ class TemplateBinder extends RecursiveAstVisitor implements Visitor {
|
|||
visitText(text: Text) {}
|
||||
visitContent(content: Content) {}
|
||||
visitTextAttribute(attribute: TextAttribute) {}
|
||||
visitIcu(icu: Icu): void {}
|
||||
|
||||
// The remaining visitors are concerned with processing AST expressions within template bindings
|
||||
|
||||
|
|
|
@ -13,11 +13,13 @@ import * as core from '../../core';
|
|||
import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast';
|
||||
import {Lexer} from '../../expression_parser/lexer';
|
||||
import {Parser} from '../../expression_parser/parser';
|
||||
import * as i18n from '../../i18n/i18n_ast';
|
||||
import * as html from '../../ml_parser/ast';
|
||||
import {HtmlParser} from '../../ml_parser/html_parser';
|
||||
import {WhitespaceVisitor} from '../../ml_parser/html_whitespaces';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
|
||||
import {isNgContainer as checkIsNgContainer, splitNsName} from '../../ml_parser/tags';
|
||||
import {mapLiteral} from '../../output/map_util';
|
||||
import * as o from '../../output/output_ast';
|
||||
import {ParseError, ParseSourceSpan} from '../../parse_util';
|
||||
import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry';
|
||||
|
@ -29,7 +31,10 @@ import {Identifiers as R3} from '../r3_identifiers';
|
|||
import {htmlAstToRender3Ast} from '../r3_template_transform';
|
||||
|
||||
import {R3QueryMetadata} from './api';
|
||||
import {I18N_ATTR, I18N_ATTR_PREFIX, I18nContext, assembleI18nBoundString} from './i18n';
|
||||
import {I18nContext} from './i18n/context';
|
||||
import {I18nMetaVisitor} from './i18n/meta';
|
||||
import {getSerializedI18nContent} from './i18n/serializer';
|
||||
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {StylingBuilder, StylingInstruction} from './styling';
|
||||
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
|
@ -151,7 +156,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
|
||||
buildTemplateFunction(
|
||||
nodes: t.Node[], variables: t.Variable[], hasNgContent: boolean = false,
|
||||
ngContentSelectors: string[] = []): o.FunctionExpr {
|
||||
ngContentSelectors: string[] = [], i18n?: i18n.AST): o.FunctionExpr {
|
||||
if (this._namespace !== R3.namespaceHTML) {
|
||||
this.creationInstruction(null, this._namespace);
|
||||
}
|
||||
|
@ -175,8 +180,15 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
this.creationInstruction(null, R3.projectionDef, parameters);
|
||||
}
|
||||
|
||||
if (this.i18nContext) {
|
||||
this.i18nStart();
|
||||
// Initiate i18n context in case:
|
||||
// - this template has parent i18n context
|
||||
// - or the template has i18n meta associated with it,
|
||||
// but it's not initiated by the Element (e.g. <ng-template i18n>)
|
||||
const initI18nContext = this.i18nContext ||
|
||||
(isI18nRootNode(i18n) && !(isSingleElementTemplate(nodes) && nodes[0].i18n === i18n));
|
||||
const selfClosingI18nInstruction = hasTextChildrenOnly(nodes);
|
||||
if (initI18nContext) {
|
||||
this.i18nStart(null, i18n !, selfClosingI18nInstruction);
|
||||
}
|
||||
|
||||
// This is the initial pass through the nodes of this template. In this pass, we
|
||||
|
@ -198,8 +210,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
// instructions can be generated with the correct internal const count.
|
||||
this._nestedTemplateFns.forEach(buildTemplateFn => buildTemplateFn());
|
||||
|
||||
if (this.i18nContext) {
|
||||
this.i18nEnd();
|
||||
if (initI18nContext) {
|
||||
this.i18nEnd(null, selfClosingI18nInstruction);
|
||||
}
|
||||
|
||||
// Generate all the creation mode instructions (e.g. resolve bindings in listeners)
|
||||
|
@ -240,59 +252,140 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
// LocalResolver
|
||||
getLocal(name: string): o.Expression|null { return this._bindingScope.get(name); }
|
||||
|
||||
i18nTranslate(label: string, meta: string = ''): o.Expression {
|
||||
return this.constantPool.getTranslation(label, meta, this.fileBasedI18nSuffix);
|
||||
i18nTranslate(
|
||||
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Expression {
|
||||
const _ref = ref || this.i18nAllocateRef();
|
||||
const _params: {[key: string]: any} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
|
||||
}
|
||||
const meta = metaFromI18nMessage(message);
|
||||
const content = getSerializedI18nContent(message);
|
||||
const statements = getTranslationDeclStmts(_ref, content, meta, _params, transformFn);
|
||||
this.constantPool.statements.push(...statements);
|
||||
return _ref;
|
||||
}
|
||||
|
||||
i18nAppendTranslationMeta(meta: string = '') { this.constantPool.appendTranslationMeta(meta); }
|
||||
i18nAppendBindings(expressions: AST[]) {
|
||||
if (!this.i18n || !expressions.length) return;
|
||||
const implicit = o.variable(CONTEXT_NAME);
|
||||
expressions.forEach(expression => {
|
||||
const binding = this.convertExpressionBinding(implicit, expression);
|
||||
this.i18n !.appendBinding(binding);
|
||||
});
|
||||
}
|
||||
|
||||
i18nAllocateRef(): o.ReadVarExpr {
|
||||
return this.constantPool.getDeferredTranslationConst(this.fileBasedI18nSuffix);
|
||||
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];
|
||||
if (prop instanceof t.Text) {
|
||||
bound[key] = o.literal(prop.value);
|
||||
} else {
|
||||
const value = prop.value.visit(this._valueConverter);
|
||||
if (value instanceof Interpolation) {
|
||||
const {strings, expressions} = value;
|
||||
const {id, bindings} = this.i18n !;
|
||||
const label = assembleI18nBoundString(strings, bindings.size, id);
|
||||
this.i18nAppendBindings(expressions);
|
||||
bound[key] = o.literal(label);
|
||||
}
|
||||
}
|
||||
});
|
||||
return bound;
|
||||
}
|
||||
|
||||
i18nAllocateRef() {
|
||||
const prefix = getTranslationConstPrefix(this.fileBasedI18nSuffix);
|
||||
return o.variable(this.constantPool.uniqueName(prefix));
|
||||
}
|
||||
|
||||
i18nUpdateRef(context: I18nContext): void {
|
||||
if (context.isRoot() && context.isResolved()) {
|
||||
this.constantPool.setDeferredTranslationConst(context.getRef(), context.getContent());
|
||||
const {icus, meta, isRoot, isResolved} = context;
|
||||
if (isRoot && isResolved && !isSingleI18nIcu(meta)) {
|
||||
const placeholders = context.getSerializedPlaceholders();
|
||||
let icuMapping: {[name: string]: o.Expression} = {};
|
||||
let params: {[name: string]: o.Expression} =
|
||||
placeholders.size ? placeholdersToParams(placeholders) : {};
|
||||
if (icus.size) {
|
||||
icus.forEach((refs: o.Expression[], key: string) => {
|
||||
if (refs.length === 1) {
|
||||
// if we have one ICU defined for a given
|
||||
// placeholder - just output its reference
|
||||
params[key] = refs[0];
|
||||
} else {
|
||||
// ... otherwise we need to activate post-processing
|
||||
// to replace ICU placeholders with proper values
|
||||
const placeholder: string = wrapI18nPlaceholder(`${I18N_ICU_MAPPING_PREFIX}${key}`);
|
||||
params[key] = o.literal(placeholder);
|
||||
icuMapping[key] = o.literalArr(refs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// translation requires post processing in 2 cases:
|
||||
// - if we have placeholders with multiple values (ex. `START_DIV`: [<5B>#1<>, <20>#2<>, ...])
|
||||
// - if we have multiple ICUs that refer to the same placeholder name
|
||||
const needsPostprocessing =
|
||||
Array.from(placeholders.values()).some((value: string[]) => value.length > 1) ||
|
||||
Object.keys(icuMapping).length;
|
||||
|
||||
let transformFn;
|
||||
if (needsPostprocessing) {
|
||||
transformFn = (raw: o.ReadVarExpr) => {
|
||||
const args: o.Expression[] = [raw];
|
||||
if (Object.keys(icuMapping).length) {
|
||||
args.push(mapLiteral(icuMapping, true));
|
||||
}
|
||||
return instruction(null, R3.i18nPostprocess, args);
|
||||
};
|
||||
}
|
||||
this.i18nTranslate(meta as i18n.Message, params, context.ref, transformFn);
|
||||
}
|
||||
}
|
||||
|
||||
i18nStart(span: ParseSourceSpan|null = null, meta?: string): void {
|
||||
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 !);
|
||||
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta);
|
||||
} else {
|
||||
this.i18nAppendTranslationMeta(meta);
|
||||
const ref = this.i18nAllocateRef();
|
||||
this.i18n = new I18nContext(index, this.templateIndex, ref);
|
||||
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
|
||||
}
|
||||
|
||||
// generate i18nStart instruction
|
||||
const params: o.Expression[] = [o.literal(index), this.i18n.getRef()];
|
||||
if (this.i18n.getId() > 0) {
|
||||
const {id, ref} = this.i18n;
|
||||
const params: o.Expression[] = [o.literal(index), ref];
|
||||
if (id > 0) {
|
||||
// do not push 3rd argument (sub-block id)
|
||||
// into i18nStart call for top level i18n context
|
||||
params.push(o.literal(this.i18n.getId()));
|
||||
params.push(o.literal(id));
|
||||
}
|
||||
this.creationInstruction(span, R3.i18nStart, params);
|
||||
this.creationInstruction(span, selfClosing ? R3.i18n : R3.i18nStart, params);
|
||||
}
|
||||
|
||||
i18nEnd(span: ParseSourceSpan|null = null, selfClosing?: boolean): void {
|
||||
if (!this.i18n) {
|
||||
throw new Error('i18nEnd is executed with no i18n context present');
|
||||
}
|
||||
|
||||
i18nEnd(span: ParseSourceSpan|null = null): void {
|
||||
if (this.i18nContext) {
|
||||
this.i18nContext.reconcileChildContext(this.i18n !);
|
||||
this.i18nContext.reconcileChildContext(this.i18n);
|
||||
this.i18nUpdateRef(this.i18nContext);
|
||||
} else {
|
||||
this.i18nUpdateRef(this.i18n !);
|
||||
this.i18nUpdateRef(this.i18n);
|
||||
}
|
||||
|
||||
// setup accumulated bindings
|
||||
const bindings = this.i18n !.getBindings();
|
||||
const {index, bindings} = this.i18n;
|
||||
if (bindings.size) {
|
||||
bindings.forEach(binding => { this.updateInstruction(span, R3.i18nExp, [binding]); });
|
||||
const index: o.Expression = o.literal(this.i18n !.getIndex());
|
||||
this.updateInstruction(span, R3.i18nApply, [index]);
|
||||
bindings.forEach(binding => this.updateInstruction(span, R3.i18nExp, [binding]));
|
||||
this.updateInstruction(span, R3.i18nApply, [o.literal(index)]);
|
||||
}
|
||||
|
||||
if (!selfClosing) {
|
||||
this.creationInstruction(span, R3.i18nEnd);
|
||||
}
|
||||
this.i18n = null; // reset local i18n context
|
||||
}
|
||||
|
||||
|
@ -341,36 +434,31 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const stylingBuilder = new StylingBuilder(elementIndex);
|
||||
|
||||
let isNonBindableMode: boolean = false;
|
||||
let isI18nRootElement: boolean = false;
|
||||
const isI18nRootElement: boolean = isI18nRootNode(element.i18n);
|
||||
|
||||
const outputAttrs: {[name: string]: string} = {};
|
||||
const attrI18nMetas: {[name: string]: string} = {};
|
||||
let i18nMeta: string = '';
|
||||
if (isI18nRootElement && this.i18n) {
|
||||
throw new Error(`Could not mark an element as translatable inside of a translatable section`);
|
||||
}
|
||||
|
||||
const i18nAttrs: (t.TextAttribute | t.BoundAttribute)[] = [];
|
||||
const outputAttrs: t.TextAttribute[] = [];
|
||||
|
||||
const [namespaceKey, elementName] = splitNsName(element.name);
|
||||
const isNgContainer = checkIsNgContainer(element.name);
|
||||
|
||||
// Handle i18n and ngNonBindable attributes
|
||||
// Handle styling, i18n, ngNonBindable attributes
|
||||
for (const attr of element.attributes) {
|
||||
const name = attr.name;
|
||||
const value = attr.value;
|
||||
const {name, value} = attr;
|
||||
if (name === NON_BINDABLE_ATTR) {
|
||||
isNonBindableMode = true;
|
||||
} else if (name === I18N_ATTR) {
|
||||
if (this.i18n) {
|
||||
throw new Error(
|
||||
`Could not mark an element as translatable inside of a translatable section`);
|
||||
}
|
||||
isI18nRootElement = true;
|
||||
i18nMeta = value;
|
||||
} else if (name.startsWith(I18N_ATTR_PREFIX)) {
|
||||
attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value;
|
||||
} else if (name == 'style') {
|
||||
stylingBuilder.registerStyleAttr(value);
|
||||
} else if (name == 'class') {
|
||||
stylingBuilder.registerClassAttr(value);
|
||||
} else if (attr.i18n) {
|
||||
i18nAttrs.push(attr);
|
||||
} else {
|
||||
outputAttrs[name] = value;
|
||||
outputAttrs.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -387,12 +475,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const attributes: o.Expression[] = [];
|
||||
const allOtherInputs: t.BoundAttribute[] = [];
|
||||
|
||||
const i18nAttrs: Array<{name: string, value: string | AST}> = [];
|
||||
element.inputs.forEach((input: t.BoundAttribute) => {
|
||||
if (!stylingBuilder.registerInput(input)) {
|
||||
if (input.type == BindingType.Property) {
|
||||
if (attrI18nMetas.hasOwnProperty(input.name)) {
|
||||
i18nAttrs.push({name: input.name, value: input.value});
|
||||
if (input.i18n) {
|
||||
i18nAttrs.push(input);
|
||||
} else {
|
||||
allOtherInputs.push(input);
|
||||
}
|
||||
|
@ -402,14 +489,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
}
|
||||
});
|
||||
|
||||
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
|
||||
const value = outputAttrs[name];
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
i18nAttrs.push({name, value});
|
||||
} else {
|
||||
attributes.push(o.literal(name), o.literal(value));
|
||||
}
|
||||
});
|
||||
outputAttrs.forEach(attr => attributes.push(o.literal(attr.name), o.literal(attr.value)));
|
||||
|
||||
// this will build the instructions so that they fall into the following syntax
|
||||
// add attributes for directive matching purposes
|
||||
|
@ -431,15 +511,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const implicit = o.variable(CONTEXT_NAME);
|
||||
|
||||
if (this.i18n) {
|
||||
this.i18n.appendElement(elementIndex);
|
||||
this.i18n.appendElement(element.i18n !, elementIndex);
|
||||
}
|
||||
|
||||
const hasChildren = () => {
|
||||
if (!isI18nRootElement && this.i18n) {
|
||||
// we do not append text node instructions inside i18n section, so we
|
||||
// exclude them while calculating whether current element has children
|
||||
return element.children.find(
|
||||
child => !(child instanceof t.Text || child instanceof t.BoundText));
|
||||
// we do not append text node instructions and ICUs inside i18n section,
|
||||
// so we exclude them while calculating whether current element has children
|
||||
return !hasTextChildrenOnly(element.children);
|
||||
}
|
||||
return element.children.length > 0;
|
||||
};
|
||||
|
@ -447,6 +526,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const createSelfClosingInstruction = !stylingBuilder.hasBindingsOrInitialValues &&
|
||||
!isNgContainer && element.outputs.length === 0 && i18nAttrs.length === 0 && !hasChildren();
|
||||
|
||||
const createSelfClosingI18nInstruction =
|
||||
!createSelfClosingInstruction && hasTextChildrenOnly(element.children);
|
||||
|
||||
if (createSelfClosingInstruction) {
|
||||
this.creationInstruction(element.sourceSpan, R3.element, trimTrailingNulls(parameters));
|
||||
} else {
|
||||
|
@ -459,27 +541,24 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
}
|
||||
|
||||
if (isI18nRootElement) {
|
||||
this.i18nStart(element.sourceSpan, i18nMeta);
|
||||
this.i18nStart(element.sourceSpan, element.i18n !, createSelfClosingI18nInstruction);
|
||||
}
|
||||
|
||||
// process i18n element attributes
|
||||
if (i18nAttrs.length) {
|
||||
let hasBindings: boolean = false;
|
||||
const i18nAttrArgs: o.Expression[] = [];
|
||||
i18nAttrs.forEach(({name, value}) => {
|
||||
const meta = attrI18nMetas[name];
|
||||
if (typeof value === 'string') {
|
||||
// in case of static string value, 3rd argument is 0 declares
|
||||
// that there are no expressions defined in this translation
|
||||
i18nAttrArgs.push(o.literal(name), this.i18nTranslate(value, meta), o.literal(0));
|
||||
i18nAttrs.forEach(attr => {
|
||||
const message = attr.i18n !as i18n.Message;
|
||||
if (attr instanceof t.TextAttribute) {
|
||||
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message));
|
||||
} else {
|
||||
const converted = value.visit(this._valueConverter);
|
||||
const converted = attr.value.visit(this._valueConverter);
|
||||
if (converted instanceof Interpolation) {
|
||||
const {strings, expressions} = converted;
|
||||
const label = assembleI18nBoundString(strings);
|
||||
i18nAttrArgs.push(
|
||||
o.literal(name), this.i18nTranslate(label, meta), o.literal(expressions.length));
|
||||
expressions.forEach(expression => {
|
||||
const placeholders = assembleBoundTextPlaceholders(message);
|
||||
const params = placeholdersToParams(placeholders);
|
||||
i18nAttrArgs.push(o.literal(attr.name), this.i18nTranslate(message, params));
|
||||
converted.expressions.forEach(expression => {
|
||||
hasBindings = true;
|
||||
const binding = this.convertExpressionBinding(implicit, expression);
|
||||
this.updateInstruction(element.sourceSpan, R3.i18nExp, [binding]);
|
||||
|
@ -552,14 +631,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
t.visitAll(this, element.children);
|
||||
|
||||
if (!isI18nRootElement && this.i18n) {
|
||||
this.i18n.appendElement(elementIndex, true);
|
||||
this.i18n.appendElement(element.i18n !, elementIndex, true);
|
||||
}
|
||||
|
||||
if (!createSelfClosingInstruction) {
|
||||
// Finish element construction mode.
|
||||
const span = element.endSourceSpan || element.sourceSpan;
|
||||
if (isI18nRootElement) {
|
||||
this.i18nEnd(span);
|
||||
this.i18nEnd(span, createSelfClosingI18nInstruction);
|
||||
}
|
||||
if (isNonBindableMode) {
|
||||
this.creationInstruction(span, R3.enableBindings);
|
||||
|
@ -572,13 +651,13 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const templateIndex = this.allocateDataSlot();
|
||||
|
||||
if (this.i18n) {
|
||||
this.i18n.appendTemplate(templateIndex);
|
||||
this.i18n.appendTemplate(template.i18n !, templateIndex);
|
||||
}
|
||||
|
||||
let elName = '';
|
||||
if (template.children.length === 1 && template.children[0] instanceof t.Element) {
|
||||
if (isSingleElementTemplate(template.children)) {
|
||||
// When the template as a single child, derive the context name from the tag
|
||||
elName = sanitizeIdentifier((template.children[0] as t.Element).name);
|
||||
elName = sanitizeIdentifier(template.children[0].name);
|
||||
}
|
||||
|
||||
const contextName = elName ? `${this.contextName}_${elName}` : '';
|
||||
|
@ -632,8 +711,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
// be able to support bindings in nested templates to local refs that occur after the
|
||||
// template definition. e.g. <div *ngIf="showing"> {{ foo }} </div> <div #foo></div>
|
||||
this._nestedTemplateFns.push(() => {
|
||||
const templateFunctionExpr =
|
||||
templateVisitor.buildTemplateFunction(template.children, template.variables);
|
||||
const templateFunctionExpr = templateVisitor.buildTemplateFunction(
|
||||
template.children, template.variables, false, [], template.i18n);
|
||||
this.constantPool.statements.push(templateFunctionExpr.toDeclStmt(templateName, null));
|
||||
});
|
||||
|
||||
|
@ -664,15 +743,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
if (this.i18n) {
|
||||
const value = text.value.visit(this._valueConverter);
|
||||
if (value instanceof Interpolation) {
|
||||
const {strings, expressions} = value;
|
||||
const label =
|
||||
assembleI18nBoundString(strings, this.i18n.getBindings().size, this.i18n.getId());
|
||||
const implicit = o.variable(CONTEXT_NAME);
|
||||
expressions.forEach(expression => {
|
||||
const binding = this.convertExpressionBinding(implicit, expression);
|
||||
this.i18n !.appendBinding(binding);
|
||||
});
|
||||
this.i18n.appendText(label);
|
||||
this.i18n.appendBoundText(text.i18n !);
|
||||
this.i18nAppendBindings(value.expressions);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -689,13 +761,51 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
}
|
||||
|
||||
visitText(text: t.Text) {
|
||||
if (this.i18n) {
|
||||
this.i18n.appendText(text.value);
|
||||
return;
|
||||
}
|
||||
// when a text element is located within a translatable
|
||||
// block, we exclude this text element from instructions set,
|
||||
// since it will be captured in i18n content and processed at runtime
|
||||
if (!this.i18n) {
|
||||
this.creationInstruction(
|
||||
text.sourceSpan, R3.text, [o.literal(this.allocateDataSlot()), o.literal(text.value)]);
|
||||
}
|
||||
}
|
||||
|
||||
visitIcu(icu: t.Icu) {
|
||||
let initWasInvoked = false;
|
||||
|
||||
// if an ICU was created outside of i18n block, we still treat
|
||||
// it as a translatable entity and invoke i18nStart and i18nEnd
|
||||
// to generate i18n context and the necessary instructions
|
||||
if (!this.i18n) {
|
||||
initWasInvoked = true;
|
||||
this.i18nStart(null, icu.i18n !, true);
|
||||
}
|
||||
|
||||
const i18n = this.i18n !;
|
||||
const vars = this.i18nBindProps(icu.vars);
|
||||
const placeholders = this.i18nBindProps(icu.placeholders);
|
||||
|
||||
// output ICU directly and keep ICU reference in context
|
||||
const message = icu.i18n !as i18n.Message;
|
||||
const transformFn = (raw: o.ReadVarExpr) =>
|
||||
instruction(null, R3.i18nPostprocess, [raw, mapLiteral(vars, true)]);
|
||||
|
||||
// in case the whole i18n message is a single ICU - we do not need to
|
||||
// create a separate top-level translation, we can use the root ref instead
|
||||
// and make this ICU a top-level translation
|
||||
if (isSingleI18nIcu(i18n.meta)) {
|
||||
this.i18nTranslate(message, placeholders, i18n.ref, transformFn);
|
||||
} else {
|
||||
// output ICU directly and keep ICU reference in context
|
||||
const ref = this.i18nTranslate(message, placeholders, undefined, transformFn);
|
||||
i18n.appendIcu(icuFromI18nMessage(message).name, ref);
|
||||
}
|
||||
|
||||
if (initWasInvoked) {
|
||||
this.i18nEnd(null, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private allocateDataSlot() { return this._dataIndex++; }
|
||||
|
||||
|
@ -1283,7 +1393,7 @@ export function parseTemplate(
|
|||
} {
|
||||
const bindingParser = makeBindingParser();
|
||||
const htmlParser = new HtmlParser();
|
||||
const parseResult = htmlParser.parse(template, templateUrl);
|
||||
const parseResult = htmlParser.parse(template, templateUrl, true);
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
return {
|
||||
|
@ -1295,8 +1405,22 @@ export function parseTemplate(
|
|||
}
|
||||
|
||||
let rootNodes: html.Node[] = parseResult.rootNodes;
|
||||
|
||||
// process i18n meta information (scan attributes, generate ids)
|
||||
// before we run whitespace removal process, because existing i18n
|
||||
// extraction process (ng xi18n) relies on a raw content to generate
|
||||
// message ids
|
||||
const i18nConfig = {keepI18nAttrs: !options.preserveWhitespaces};
|
||||
rootNodes = html.visitAll(new I18nMetaVisitor(i18nConfig), rootNodes);
|
||||
|
||||
if (!options.preserveWhitespaces) {
|
||||
rootNodes = html.visitAll(new WhitespaceVisitor(), rootNodes);
|
||||
|
||||
// run i18n meta visitor again in case we remove whitespaces, because
|
||||
// that might affect generated i18n message content. During this pass
|
||||
// i18n IDs generated at the first pass will be preserved, so we can mimic
|
||||
// existing extraction process (ng xi18n)
|
||||
rootNodes = html.visitAll(new I18nMetaVisitor({keepI18nAttrs: false}), rootNodes);
|
||||
}
|
||||
|
||||
const {nodes, hasNgContent, ngContentSelectors, errors} =
|
||||
|
@ -1345,3 +1469,13 @@ function resolveSanitizationFn(input: t.BoundAttribute, context: core.SecurityCo
|
|||
function prepareSyntheticAttributeName(name: string) {
|
||||
return '@' + name;
|
||||
}
|
||||
|
||||
function isSingleElementTemplate(children: t.Node[]): children is[t.Element] {
|
||||
return children.length === 1 && children[0] instanceof t.Element;
|
||||
}
|
||||
|
||||
function hasTextChildrenOnly(children: t.Node[]): boolean {
|
||||
return !children.find(
|
||||
child =>
|
||||
!(child instanceof t.Text || child instanceof t.BoundText || child instanceof t.Icu));
|
||||
}
|
|
@ -11,7 +11,7 @@ import * as o from '../../output/output_ast';
|
|||
import * as t from '../r3_ast';
|
||||
|
||||
import {R3QueryMetadata} from './api';
|
||||
import {isI18NAttribute} from './i18n';
|
||||
import {isI18nAttribute} from './i18n/util';
|
||||
|
||||
/** Name of the temporary to use during data binding */
|
||||
export const TEMPORARY_NAME = '_t';
|
||||
|
@ -135,7 +135,7 @@ export function getAttrsForDirectiveMatching(elOrTpl: t.Element | t.Template):
|
|||
const attributesMap: {[name: string]: string} = {};
|
||||
|
||||
elOrTpl.attributes.forEach(a => {
|
||||
if (!isI18NAttribute(a.name)) {
|
||||
if (!isI18nAttribute(a.name)) {
|
||||
attributesMap[a.name] = a.value;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,38 +7,11 @@
|
|||
*/
|
||||
|
||||
import {BindingType} from '../../src/expression_parser/ast';
|
||||
import {Lexer} from '../../src/expression_parser/lexer';
|
||||
import {Parser} from '../../src/expression_parser/parser';
|
||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
|
||||
import * as t from '../../src/render3/r3_ast';
|
||||
import {Render3ParseResult, htmlAstToRender3Ast} from '../../src/render3/r3_template_transform';
|
||||
import {BindingParser} from '../../src/template_parser/binding_parser';
|
||||
import {MockSchemaRegistry} from '../../testing';
|
||||
import {unparse} from '../expression_parser/utils/unparser';
|
||||
import {parseR3 as parse} from './view/util';
|
||||
|
||||
|
||||
// Parse an html string to IVY specific info
|
||||
function parse(html: string): Render3ParseResult {
|
||||
const htmlParser = new HtmlParser();
|
||||
|
||||
const parseResult = htmlParser.parse(html, 'path:://to/template', true);
|
||||
|
||||
if (parseResult.errors.length > 0) {
|
||||
const msg = parseResult.errors.map(e => e.toString()).join('\n');
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const htmlNodes = parseResult.rootNodes;
|
||||
const expressionParser = new Parser(new Lexer());
|
||||
const schemaRegistry = new MockSchemaRegistry(
|
||||
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false},
|
||||
['onEvent'], ['onEvent']);
|
||||
const bindingParser =
|
||||
new BindingParser(expressionParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, null, []);
|
||||
return htmlAstToRender3Ast(htmlNodes, bindingParser);
|
||||
}
|
||||
|
||||
// Transform an IVY AST to a flat list of nodes to ease testing
|
||||
class R3AstHumanizer implements t.Visitor<void> {
|
||||
result: any[] = [];
|
||||
|
@ -104,6 +77,8 @@ class R3AstHumanizer implements t.Visitor<void> {
|
|||
|
||||
visitBoundText(text: t.BoundText) { this.result.push(['BoundText', unparse(text.value)]); }
|
||||
|
||||
visitIcu(icu: t.Icu) { return null; }
|
||||
|
||||
private visitAll(nodes: t.Node[][]) { nodes.forEach(node => t.visitAll(this, node)); }
|
||||
}
|
||||
|
||||
|
|
|
@ -6,65 +6,249 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as i18n from '../../../src/i18n/i18n_ast';
|
||||
import * as o from '../../../src/output/output_ast';
|
||||
import {I18nContext} from '../../../src/render3/view/i18n';
|
||||
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 {parseR3 as parse} from './util';
|
||||
|
||||
const i18nOf = (element: t.Node & {i18n?: i18n.AST}) => element.i18n !;
|
||||
|
||||
describe('I18nContext', () => {
|
||||
it('should support i18n content collection', () => {
|
||||
const ctx = new I18nContext(5, null, 'myRef');
|
||||
const ref = o.variable('ref');
|
||||
const ast = new i18n.Message([], {}, {}, '', '', '');
|
||||
const ctx = new I18nContext(5, ref, 0, null, ast);
|
||||
|
||||
// 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');
|
||||
expect(ctx.isRoot).toBe(true);
|
||||
expect(ctx.isResolved).toBe(true);
|
||||
expect(ctx.id).toBe(0);
|
||||
expect(ctx.ref).toBe(ref);
|
||||
expect(ctx.index).toBe(5);
|
||||
expect(ctx.templateIndex).toBe(null);
|
||||
|
||||
const tree = parse('<div i18n>A {{ valueA }} <div> B </div><p *ngIf="visible"> C </p></div>');
|
||||
const [boundText, element, template] = (tree.nodes[0] as t.Element).children;
|
||||
|
||||
// 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<6F>#1<>Bar<61>/#1<>');
|
||||
expect(ctx.placeholders.size).toBe(0);
|
||||
ctx.appendBoundText(i18nOf(boundText)); // interpolation
|
||||
ctx.appendElement(i18nOf(element), 1); // open tag
|
||||
ctx.appendElement(i18nOf(element), 1, true); // close tag
|
||||
ctx.appendTemplate(i18nOf(template), 2); // open + close tags
|
||||
expect(ctx.placeholders.size).toBe(5);
|
||||
|
||||
// binding collection checks
|
||||
expect(ctx.getBindings().size).toBe(0);
|
||||
expect(ctx.bindings.size).toBe(0);
|
||||
ctx.appendBinding(o.literal(1));
|
||||
ctx.appendBinding(o.literal(2));
|
||||
expect(ctx.getBindings().size).toBe(2);
|
||||
expect(ctx.bindings.size).toBe(2);
|
||||
});
|
||||
|
||||
it('should support nested contexts', () => {
|
||||
const ctx = new I18nContext(5, null, 'myRef');
|
||||
const templateIndex = 1;
|
||||
const template = `
|
||||
<div i18n>
|
||||
A {{ valueA }}
|
||||
<div>A</div>
|
||||
<b *ngIf="visible">
|
||||
B {{ valueB }}
|
||||
<div>B</div>
|
||||
C {{ valueC }}
|
||||
</b>
|
||||
</div>
|
||||
`;
|
||||
const tree = parse(template);
|
||||
const root = tree.nodes[0] as t.Element;
|
||||
const [boundTextA, elementA, templateA] = root.children;
|
||||
const elementB = (templateA as t.Template).children[0] as t.Element;
|
||||
const [boundTextB, elementC, boundTextC] = (elementB as t.Element).children;
|
||||
|
||||
// set some data for root ctx
|
||||
ctx.appendText('Foo');
|
||||
ctx.appendBinding(o.literal(1));
|
||||
ctx.appendTemplate(templateIndex);
|
||||
expect(ctx.isResolved()).toBe(false);
|
||||
// simulate I18nContext for a given template
|
||||
const ctx = new I18nContext(1, o.variable('ctx'), 0, null, root.i18n !);
|
||||
|
||||
// set data for root ctx
|
||||
ctx.appendBoundText(i18nOf(boundTextA));
|
||||
ctx.appendBinding(o.literal('valueA'));
|
||||
ctx.appendElement(i18nOf(elementA), 0);
|
||||
ctx.appendTemplate(i18nOf(templateA), 1);
|
||||
ctx.appendElement(i18nOf(elementA), 0, true);
|
||||
expect(ctx.bindings.size).toBe(1);
|
||||
expect(ctx.placeholders.size).toBe(5);
|
||||
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);
|
||||
const childCtx = ctx.forkChildContext(2, 1, (templateA as t.Template).i18n !);
|
||||
expect(childCtx.bindings.size).toBe(0);
|
||||
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));
|
||||
// set data for child context
|
||||
childCtx.appendElement(i18nOf(elementB), 0);
|
||||
childCtx.appendBoundText(i18nOf(boundTextB));
|
||||
childCtx.appendBinding(o.literal('valueB'));
|
||||
childCtx.appendElement(i18nOf(elementC), 1);
|
||||
childCtx.appendElement(i18nOf(elementC), 1, true);
|
||||
childCtx.appendBoundText(i18nOf(boundTextC));
|
||||
childCtx.appendBinding(o.literal('valueC'));
|
||||
childCtx.appendElement(i18nOf(elementB), 0, true);
|
||||
|
||||
expect(childCtx.getContent()).toBe('Bar<61>#2:1<>Baz<61>/#2:1<>');
|
||||
expect(childCtx.getBindings().size).toBe(2);
|
||||
expect(childCtx.bindings.size).toBe(2);
|
||||
expect(childCtx.placeholders.size).toBe(6);
|
||||
|
||||
// ctx bindings and placeholders are not shared,
|
||||
// so root bindings and placeholders do not change
|
||||
expect(ctx.bindings.size).toBe(1);
|
||||
expect(ctx.placeholders.size).toBe(5);
|
||||
|
||||
// reconcile
|
||||
ctx.reconcileChildContext(childCtx);
|
||||
expect(ctx.getContent()).toBe('Foo<6F>*1:1<>Bar<61>#2:1<>Baz<61>/#2:1<><31>/*1:1<>');
|
||||
|
||||
// verify placeholders
|
||||
const expected = new Map([
|
||||
['INTERPOLATION', '<27>0<EFBFBD>'], ['START_TAG_DIV', '<27>#0<>|<7C>#1:1<>'],
|
||||
['START_BOLD_TEXT', '<27>*1:1<><31>#0:1<>'], ['CLOSE_BOLD_TEXT', '<27>/#0:1<><31>/*1:1<>'],
|
||||
['CLOSE_TAG_DIV', '<27>/#0<>|<7C>/#1:1<>'], ['INTERPOLATION_1', '<27>0:1<>'],
|
||||
['INTERPOLATION_2', '<27>1:1<>']
|
||||
]);
|
||||
const phs = ctx.getSerializedPlaceholders();
|
||||
expected.forEach((value, key) => { expect(phs.get(key) !.join('|')).toEqual(value); });
|
||||
|
||||
// placeholders are added into the root ctx
|
||||
expect(phs.size).toBe(expected.size);
|
||||
|
||||
// root context is considered resolved now
|
||||
expect(ctx.isResolved).toBe(true);
|
||||
|
||||
// bindings are not merged into root ctx
|
||||
expect(ctx.bindings.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should support templates based on <ng-template>', () => {
|
||||
const template = `
|
||||
<ng-template i18n>
|
||||
Level A
|
||||
<ng-template>
|
||||
Level B
|
||||
<ng-template>
|
||||
Level C
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
`;
|
||||
const tree = parse(template);
|
||||
const root = tree.nodes[0] as t.Template;
|
||||
|
||||
const [textA, templateA] = root.children;
|
||||
const [textB, templateB] = (templateA as t.Template).children;
|
||||
const [textC] = (templateB as t.Template).children;
|
||||
|
||||
// simulate I18nContext for a given template
|
||||
const ctxLevelA = new I18nContext(0, o.variable('ctx'), 0, null, root.i18n !);
|
||||
|
||||
// create Level A context
|
||||
ctxLevelA.appendTemplate(i18nOf(templateA), 1);
|
||||
expect(ctxLevelA.placeholders.size).toBe(2);
|
||||
expect(ctxLevelA.isResolved).toBe(false);
|
||||
|
||||
// create Level B context
|
||||
const ctxLevelB = ctxLevelA.forkChildContext(0, 1, (templateA as t.Template).i18n !);
|
||||
ctxLevelB.appendTemplate(i18nOf(templateB), 1);
|
||||
expect(ctxLevelB.isRoot).toBe(false);
|
||||
|
||||
// create Level 2 context
|
||||
const ctxLevelC = ctxLevelB.forkChildContext(0, 1, (templateB as t.Template).i18n !);
|
||||
expect(ctxLevelC.isRoot).toBe(false);
|
||||
|
||||
// reconcile
|
||||
ctxLevelB.reconcileChildContext(ctxLevelC);
|
||||
ctxLevelA.reconcileChildContext(ctxLevelB);
|
||||
|
||||
// verify placeholders
|
||||
const expected = new Map(
|
||||
[['START_TAG_NG-TEMPLATE', '<27>*1:1<>|<7C>*1:2<>'], ['CLOSE_TAG_NG-TEMPLATE', '<27>/*1:2<>|<7C>/*1:1<>']]);
|
||||
const phs = ctxLevelA.getSerializedPlaceholders();
|
||||
expected.forEach((value, key) => { expect(phs.get(key) !.join('|')).toEqual(value); });
|
||||
|
||||
// placeholders are added into the root ctx
|
||||
expect(phs.size).toBe(expected.size);
|
||||
|
||||
// root context is considered resolved now
|
||||
expect(ctxLevelA.isResolved).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Utils', () => {
|
||||
it('formatI18nPlaceholderName', () => {
|
||||
const cases = [
|
||||
// input, output
|
||||
['', ''], ['ICU', 'icu'], ['ICU_1', 'icu_1'], ['ICU_1000', 'icu_1000'],
|
||||
['START_TAG_NG-CONTAINER', 'startTagNgContainer'],
|
||||
['START_TAG_NG-CONTAINER_1', 'startTagNgContainer_1'], ['CLOSE_TAG_ITALIC', 'closeTagItalic'],
|
||||
['CLOSE_TAG_BOLD_1', 'closeTagBold_1']
|
||||
];
|
||||
cases.forEach(
|
||||
([input, output]) => { expect(formatI18nPlaceholderName(input)).toEqual(output); });
|
||||
});
|
||||
|
||||
it('parseI18nMeta', () => {
|
||||
const meta = (id?: string, meaning?: string, description?: string) =>
|
||||
({id, meaning, description});
|
||||
const cases = [
|
||||
['', meta()],
|
||||
['desc', meta('', '', 'desc')],
|
||||
['desc@@id', meta('id', '', 'desc')],
|
||||
['meaning|desc', meta('', 'meaning', 'desc')],
|
||||
['meaning|desc@@id', meta('id', 'meaning', 'desc')],
|
||||
['@@id', meta('id', '', '')],
|
||||
];
|
||||
cases.forEach(([input, output]) => {
|
||||
expect(parseI18nMeta(input as string)).toEqual(output as I18nMeta, input);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Serializer', () => {
|
||||
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);
|
||||
};
|
||||
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}'
|
||||
],
|
||||
|
||||
// content with HTML tags
|
||||
[
|
||||
'A <span>B<div>C</div></span> D',
|
||||
'A {$startTagSpan}B{$startTagDiv}C{$closeTagDiv}{$closeTagSpan} D'
|
||||
],
|
||||
|
||||
// simple ICU
|
||||
['{age, plural, 10 {ten} other {other}}', '{VAR_PLURAL, plural, 10 {ten} other {other}}'],
|
||||
|
||||
// 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}}'
|
||||
],
|
||||
|
||||
// ICU with nested HTML
|
||||
[
|
||||
'{age, plural, 10 {<b>ten</b>} other {<div class="A">other</div>}}',
|
||||
'{VAR_PLURAL, plural, 10 {{$startBoldText}ten{$closeBoldText}} other {{$startTagDiv}other{$closeTagDiv}}}'
|
||||
]
|
||||
];
|
||||
|
||||
cases.forEach(([input, output]) => { expect(serialize(input)).toEqual(output); });
|
||||
});
|
||||
});
|
|
@ -7,7 +7,17 @@
|
|||
*/
|
||||
|
||||
import * as e from '../../../src/expression_parser/ast';
|
||||
import {Lexer} from '../../../src/expression_parser/lexer';
|
||||
import {Parser} from '../../../src/expression_parser/parser';
|
||||
import * as html from '../../../src/ml_parser/ast';
|
||||
import {HtmlParser} from '../../../src/ml_parser/html_parser';
|
||||
import {WhitespaceVisitor} from '../../../src/ml_parser/html_whitespaces';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
|
||||
import * as a from '../../../src/render3/r3_ast';
|
||||
import {Render3ParseResult, htmlAstToRender3Ast} from '../../../src/render3/r3_template_transform';
|
||||
import {processI18nMeta} from '../../../src/render3/view/i18n/meta';
|
||||
import {BindingParser} from '../../../src/template_parser/binding_parser';
|
||||
import {MockSchemaRegistry} from '../../../testing';
|
||||
|
||||
export function findExpression(tmpl: a.Node[], expr: string): e.AST|null {
|
||||
const res = tmpl.reduce((found, node) => {
|
||||
|
@ -65,3 +75,30 @@ export function toStringExpression(expr: e.AST): string {
|
|||
throw new Error(`Unsupported type: ${(expr as any).constructor.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse an html string to IVY specific info
|
||||
export function parseR3(
|
||||
input: string, options: {preserveWhitespaces?: boolean} = {}): Render3ParseResult {
|
||||
const htmlParser = new HtmlParser();
|
||||
|
||||
const parseResult = htmlParser.parse(input, 'path:://to/template', true);
|
||||
|
||||
if (parseResult.errors.length > 0) {
|
||||
const msg = parseResult.errors.map(e => e.toString()).join('\n');
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
let htmlNodes = processI18nMeta(parseResult).rootNodes;
|
||||
|
||||
if (!options.preserveWhitespaces) {
|
||||
htmlNodes = html.visitAll(new WhitespaceVisitor(), htmlNodes);
|
||||
}
|
||||
|
||||
const expressionParser = new Parser(new Lexer());
|
||||
const schemaRegistry = new MockSchemaRegistry(
|
||||
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false},
|
||||
['onEvent'], ['onEvent']);
|
||||
const bindingParser =
|
||||
new BindingParser(expressionParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, null, []);
|
||||
return htmlAstToRender3Ast(htmlNodes, bindingParser);
|
||||
}
|
|
@ -110,12 +110,13 @@ export {
|
|||
PipeDef as ɵPipeDef,
|
||||
PipeDefWithMeta as ɵPipeDefWithMeta,
|
||||
whenRendered as ɵwhenRendered,
|
||||
i18n as ɵi18n,
|
||||
i18nAttributes as ɵi18nAttributes,
|
||||
i18nExp as ɵi18nExp,
|
||||
i18nStart as ɵi18nStart,
|
||||
i18nEnd as ɵi18nEnd,
|
||||
i18nApply as ɵi18nApply,
|
||||
i18nIcuReplaceVars as ɵi18nIcuReplaceVars,
|
||||
i18nPostprocess as ɵi18nPostprocess,
|
||||
WRAP_RENDERER_FACTORY2 as ɵWRAP_RENDERER_FACTORY2,
|
||||
setClassMetadata as ɵsetClassMetadata,
|
||||
} from './render3/index';
|
||||
|
|
|
@ -210,8 +210,9 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
|
|||
| i18nAttributes | ✅ | ✅ | ✅ |
|
||||
| i18nExp | ✅ | ✅ | ✅ |
|
||||
| i18nApply | ✅ | ✅ | ✅ |
|
||||
| ICU expressions | ✅ | ✅ | ❌ |
|
||||
| closure support for g3 | ✅ | ✅ | ❌ |
|
||||
| ICU expressions | ✅ | ✅ | ✅ |
|
||||
| closure support for g3 | ✅ | ✅ | ✅ |
|
||||
| `<ng-container>` support | ✅ | ✅ | ✅ |
|
||||
| runtime service for external world | ❌ | ❌ | ❌ |
|
||||
| migration tool | ❌ | ❌ | ❌ |
|
||||
|
||||
|
|
|
@ -1175,12 +1175,13 @@ const MSG_div_icu = goog.getMsg(`{VAR_PLURAL, plural,
|
|||
/**
|
||||
* @desc [BACKUP_MESSAGE_ID:2919330615509803611] Some description.
|
||||
*/
|
||||
const MSG_div = goog.getMsg('{$COUNT_1} is rendered as: {$START_BOLD_TEXT_1}{$ICU}{$END_BOLD_TEXT_1}', {
|
||||
ICU: i18nIcuReplaceVar(MSG_div_icu, 'VAR_PLURAL', '<27>0:1<>'),
|
||||
const MSG_div_raw = goog.getMsg('{$COUNT_1} is rendered as: {$START_BOLD_TEXT_1}{$ICU}{$END_BOLD_TEXT_1}', {
|
||||
ICU: MSG_div_icu,
|
||||
COUNT: '<27>0:1<>',
|
||||
START_BOLD_TEXT_1: '<27>*3:1<><31>#1<>',
|
||||
END_BOLD_TEXT_1: '<27>/#1:1<><31>/*3:1<>',
|
||||
});
|
||||
const MSG_div = i18nPostprocess(MSG_div_raw, {VAR_PLURAL: '<27>0:1<>'});
|
||||
```
|
||||
NOTE:
|
||||
- The compiler generates `[BACKUP_MESSAGE_ID:2919330615509803611]` which forces the `goog.getMsg` to use a specific message ID.
|
||||
|
@ -1196,9 +1197,38 @@ Resulting in same string which Angular can process:
|
|||
}<7D>/#1:1<><31>/*3:1<>.
|
||||
```
|
||||
|
||||
### Notice `i18nIcuReplaceVar` function
|
||||
### Placeholders with multiple values
|
||||
|
||||
The `i18nIcuReplaceVar(MSG_div_icu, 'VAR_PLURAL', '<27>0:1<>')` function is needed to replace `VAR_PLURAL` for `<60>0:1<>`.
|
||||
This is required because the ICU format does not allow placeholders in the ICU header location, a variable such as `VAR_PLURAL` must be used.
|
||||
The point of `i18nIcuReplaceVar` is to format the ICU message to something that `i18nStart` can understand.
|
||||
While extracting messages via `ng xi18n`, the tool performs an optimization and reuses the same placeholders for elements/interpolations in case placeholder content is identical.
|
||||
For example the following template:
|
||||
```html
|
||||
<b>My text 1</b><b>My text 2</b>
|
||||
```
|
||||
is transformed into:
|
||||
```html
|
||||
{$START_TAG_BOLD}My text 1{$CLOSE_TAG_BOLD}{$START_TAG_BOLD}My text 2{$CLOSE_TAG_BOLD}
|
||||
```
|
||||
In IVY we need to have specific element instruction indices for open and close tags, so the result string (that can be consumed by `i18nStart`) produced, should look like this:
|
||||
```html
|
||||
<EFBFBD>#1<>My text 1<>/#1<><31>#2<>My text 1<>/#2<>
|
||||
```
|
||||
In order to resolve this, we need to supply all values that a given placeholder represents and invoke post processing function to transform intermediate string into its final version.
|
||||
In this case the `goog.getMsg` invocation will look like this:
|
||||
```typescript
|
||||
/**
|
||||
* @desc [BACKUP_MESSAGE_ID:2919330615509803611] Some description.
|
||||
*/
|
||||
const MSG_div_raw = goog.getMsg('{$START_TAG_BOLD}My text 1{$CLOSE_TAG_BOLD}{$START_TAG_BOLD}My text 2{$CLOSE_TAG_BOLD}', {
|
||||
START_TAG_BOLD: '[<5B>#1<>|<7C>#2<>]',
|
||||
CLOSE_TAG_BOLD: '[<5B>/#2<>|<7C>/#1<>]'
|
||||
});
|
||||
const MSG_div = i18nPostprocess(MSG_div_raw);
|
||||
```
|
||||
|
||||
### `i18nPostprocess` function
|
||||
|
||||
Due to backwards-compatibility requirements and some limitations of `goog.getMsg`, in some cases we need to run post process to convert intermediate string into its final version that can be consumed by Ivy runtime code (something that `i18nStart` can understand), specifically:
|
||||
- we replace all `VAR_PLURAL` and `VAR_SELECT` with respective values. This is required because the ICU format does not allow placeholders in the ICU header location, a variable such as `VAR_PLURAL` must be used.
|
||||
- in some cases, ICUs may share the same placeholder name (like `ICU_1`). For this scenario we inject a special markers (`<60>I18N_EXP_ICU<43>) into a string and resolve this within the post processing function
|
||||
- this function also resolves the case when one placeholder is used to represent multiple elements (see example above)
|
||||
|
||||
|
|
|
@ -30,6 +30,11 @@ const PH_REGEXP = /<2F>(\/?[#*]\d+):?\d*<2A>/gi;
|
|||
const BINDING_REGEXP = /<2F>(\d+):?\d*<2A>/gi;
|
||||
const ICU_REGEXP = /({\s*<2A>\d+<2B>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
|
||||
|
||||
// i18nPostproocess regexps
|
||||
const PP_PLACEHOLDERS = /\[(<28>.+?<3F>?)\]/g;
|
||||
const PP_ICU_VARS = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
|
||||
const PP_ICUS = /<2F>I18N_EXP_(ICU(_\d+)?)<29>/g;
|
||||
|
||||
interface IcuExpression {
|
||||
type: IcuType;
|
||||
mainBinding: number;
|
||||
|
@ -482,6 +487,77 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
|
|||
return tNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles message string post-processing for internationalization.
|
||||
*
|
||||
* Handles message string post-processing by transforming it from intermediate
|
||||
* format (that might contain some markers that we need to replace) to the final
|
||||
* form, consumable by i18nStart instruction. Post processing steps include:
|
||||
*
|
||||
* 1. Resolve all multi-value cases (like [<EFBFBD>*1:1<EFBFBD><EFBFBD>#2:1<EFBFBD>|<EFBFBD>#4:1<EFBFBD>|<EFBFBD>5<EFBFBD>])
|
||||
* 2. Replace all ICU vars (like "VAR_PLURAL")
|
||||
* 3. Replace all ICU references with corresponding values (like <EFBFBD>ICU_EXP_ICU_1<EFBFBD>)
|
||||
* in case multiple ICUs have the same placeholder name
|
||||
*
|
||||
* @param message Raw translation string for post processing
|
||||
* @param replacements Set of replacements that should be applied
|
||||
*
|
||||
* @returns Transformed string that can be consumed by i18nStart instruction
|
||||
*
|
||||
* @publicAPI
|
||||
*/
|
||||
export function i18nPostprocess(
|
||||
message: string, replacements: {[key: string]: (string | string[])}): string {
|
||||
//
|
||||
// Step 1: resolve all multi-value cases (like [<5B>*1:1<><31>#2:1<>|<7C>#4:1<>|<7C>5<EFBFBD>])
|
||||
//
|
||||
const matches: {[key: string]: string[]} = {};
|
||||
let result = message.replace(PP_PLACEHOLDERS, (_match, content: string): string => {
|
||||
if (!matches[content]) {
|
||||
matches[content] = content.split('|');
|
||||
}
|
||||
if (!matches[content].length) {
|
||||
throw new Error(`i18n postprocess: unmatched placeholder - ${content}`);
|
||||
}
|
||||
return matches[content].shift() !;
|
||||
});
|
||||
|
||||
// verify that we injected all values
|
||||
const hasUnmatchedValues = Object.keys(matches).some(key => !!matches[key].length);
|
||||
if (hasUnmatchedValues) {
|
||||
throw new Error(`i18n postprocess: unmatched values - ${JSON.stringify(matches)}`);
|
||||
}
|
||||
|
||||
// return current result if no replacements specified
|
||||
if (!Object.keys(replacements).length) {
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
// Step 2: replace all ICU vars (like "VAR_PLURAL")
|
||||
//
|
||||
result = result.replace(PP_ICU_VARS, (match, start, key, _type, _idx, end): string => {
|
||||
return replacements.hasOwnProperty(key) ? `${start}${replacements[key]}${end}` : match;
|
||||
});
|
||||
|
||||
//
|
||||
// Step 3: replace all ICU references with corresponding values (like <20>ICU_EXP_ICU_1<5F>)
|
||||
// in case multiple ICUs have the same placeholder name
|
||||
//
|
||||
result = result.replace(PP_ICUS, (match, key): string => {
|
||||
if (replacements.hasOwnProperty(key)) {
|
||||
const list = replacements[key] as string[];
|
||||
if (!list.length) {
|
||||
throw new Error(`i18n postprocess: unmatched ICU - ${match} with key: ${key}`);
|
||||
}
|
||||
return list.shift() !;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a translation block marked by `i18nStart` and `i18nEnd`. It inserts the text/ICU nodes
|
||||
* into the render tree, moves the placeholder nodes and removes the deleted nodes.
|
||||
|
@ -1434,26 +1510,3 @@ function parseNodes(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const RAW_ICU_REGEXP = /{\s*(\S*)\s*,\s*\S{6}\s*,[\s\S]*}/gi;
|
||||
|
||||
/**
|
||||
* Replaces the variable parameter (main binding) of an ICU by a given value.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* const MSG_APP_1_RAW = "{VAR_SELECT, select, male {male} female {female} other {other}}";
|
||||
* const MSG_APP_1 = i18nIcuReplaceVars(MSG_APP_1_RAW, { VAR_SELECT: "<22>0<EFBFBD>" });
|
||||
* // --> MSG_APP_1 = "{<7B>0<EFBFBD>, select, male {male} female {female} other {other}}"
|
||||
* ```
|
||||
*/
|
||||
export function i18nIcuReplaceVars(message: string, replacements: {[key: string]: string}): string {
|
||||
const keys = Object.keys(replacements);
|
||||
function replaceFn(replacement: string) {
|
||||
return (str: string, varMatch: string) => { return str.replace(varMatch, replacement); };
|
||||
}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
message = message.replace(RAW_ICU_REGEXP, replaceFn(replacements[keys[i]]));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
|
|
@ -87,12 +87,13 @@ export {
|
|||
} from './state';
|
||||
|
||||
export {
|
||||
i18n,
|
||||
i18nAttributes,
|
||||
i18nExp,
|
||||
i18nStart,
|
||||
i18nEnd,
|
||||
i18nApply,
|
||||
i18nIcuReplaceVars,
|
||||
i18nPostprocess
|
||||
} from './i18n';
|
||||
|
||||
export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref';
|
||||
|
|
|
@ -97,11 +97,13 @@ export const angularCoreEnv: {[name: string]: Function} = {
|
|||
'ɵtextBinding': r3.textBinding,
|
||||
'ɵembeddedViewStart': r3.embeddedViewStart,
|
||||
'ɵembeddedViewEnd': r3.embeddedViewEnd,
|
||||
'ɵi18n': r3.i18n,
|
||||
'ɵi18nAttributes': r3.i18nAttributes,
|
||||
'ɵi18nExp': r3.i18nExp,
|
||||
'ɵi18nStart': r3.i18nStart,
|
||||
'ɵi18nEnd': r3.i18nEnd,
|
||||
'ɵi18nApply': r3.i18nApply,
|
||||
'ɵi18nPostprocess': r3.i18nPostprocess,
|
||||
|
||||
'ɵsanitizeHtml': sanitization.sanitizeHtml,
|
||||
'ɵsanitizeStyle': sanitization.sanitizeStyle,
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
import {noop} from '../../../compiler/src/render3/view/util';
|
||||
import {Component as _Component} from '../../src/core';
|
||||
import {defineComponent} from '../../src/render3/definition';
|
||||
import {getTranslationForTemplate, i18n, i18nApply, i18nAttributes, i18nEnd, i18nExp, i18nIcuReplaceVars, i18nStart} from '../../src/render3/i18n';
|
||||
import {getTranslationForTemplate, i18n, i18nApply, i18nAttributes, i18nEnd, i18nExp, i18nPostprocess, i18nStart} from '../../src/render3/i18n';
|
||||
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||
import {getNativeByIndex} from '../../src/render3/util';
|
||||
|
||||
import {NgIf} from './common_with_def';
|
||||
|
||||
import {element, elementEnd, elementStart, template, text, bind, elementProperty, projectionDef, projection} from '../../src/render3/instructions';
|
||||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode, I18nUpdateOpCodes, TI18n} from '../../src/render3/interfaces/i18n';
|
||||
import {HEADER_OFFSET, LViewData, TVIEW} from '../../src/render3/interfaces/view';
|
||||
|
@ -58,14 +60,6 @@ describe('Runtime i18n', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('i18nIcuReplaceVars', () => {
|
||||
it('should replace var names', () => {
|
||||
const MSG_APP_1_RAW = '{VAR_SELECT, select, male {male} female {female} other {other}}';
|
||||
const MSG_APP_1 = i18nIcuReplaceVars(MSG_APP_1_RAW, {VAR_SELECT: '\uFFFD0\uFFFD'});
|
||||
expect(MSG_APP_1).toEqual('{<7B>0<EFBFBD>, select, male {male} female {female} other {other}}');
|
||||
});
|
||||
});
|
||||
|
||||
function prepareFixture(
|
||||
createTemplate: () => void, updateTemplate: (() => void)|null, nbConsts = 0,
|
||||
nbVars = 0): TemplateFixture {
|
||||
|
@ -1506,4 +1500,105 @@ describe('Runtime i18n', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18nPostprocess', () => {
|
||||
it('should handle valid cases', () => {
|
||||
const arr = ['<27>*1:1<><31>#2:1<>', '<27>#4:2<>', '<27>6:4<>', '<27>/#2:1<><31>/*1:1<>'];
|
||||
const str = `[${arr.join('|')}]`;
|
||||
|
||||
const cases = [
|
||||
// empty string
|
||||
['', {}, ''],
|
||||
|
||||
// string without any special cases
|
||||
['Foo [1,2,3] Bar - no ICU here', {}, 'Foo [1,2,3] Bar - no ICU here'],
|
||||
|
||||
// multi-value cases
|
||||
[
|
||||
`Start: ${str}, ${str} and ${str}, ${str} end.`, {},
|
||||
`Start: ${arr[0]}, ${arr[1]} and ${arr[2]}, ${arr[3]} end.`
|
||||
],
|
||||
|
||||
// replace VAR_SELECT
|
||||
[
|
||||
'My ICU: {VAR_SELECT, select, =1 {one} other {other}}', {VAR_SELECT: '<27>1:2<>'},
|
||||
'My ICU: {<7B>1:2<>, select, =1 {one} other {other}}'
|
||||
],
|
||||
|
||||
[
|
||||
'My ICU: {\n\n\tVAR_SELECT_1 \n\n, select, =1 {one} other {other}}',
|
||||
{VAR_SELECT_1: '<27>1:2<>'}, 'My ICU: {\n\n\t<>1:2<> \n\n, select, =1 {one} other {other}}'
|
||||
],
|
||||
|
||||
// replace VAR_PLURAL
|
||||
[
|
||||
'My ICU: {VAR_PLURAL, plural, one {1} other {other}}', {VAR_PLURAL: '<27>1:2<>'},
|
||||
'My ICU: {<7B>1:2<>, plural, one {1} other {other}}'
|
||||
],
|
||||
|
||||
[
|
||||
'My ICU: {\n\n\tVAR_PLURAL_1 \n\n, select, =1 {one} other {other}}',
|
||||
{VAR_PLURAL_1: '<27>1:2<>'}, 'My ICU: {\n\n\t<>1:2<> \n\n, select, =1 {one} other {other}}'
|
||||
],
|
||||
|
||||
// do not replace VAR_* anywhere else in a string (only in ICU)
|
||||
[
|
||||
'My ICU: {VAR_PLURAL, plural, one {1} other {other}} VAR_PLURAL and VAR_SELECT',
|
||||
{VAR_PLURAL: '<27>1:2<>'},
|
||||
'My ICU: {<7B>1:2<>, plural, one {1} other {other}} VAR_PLURAL and VAR_SELECT'
|
||||
],
|
||||
|
||||
// replace VAR_*'s in nested ICUs
|
||||
[
|
||||
'My ICU: {VAR_PLURAL, plural, one {1 - {VAR_SELECT, age, 50 {fifty} other {other}}} other {other}}',
|
||||
{VAR_PLURAL: '<27>1:2<>', VAR_SELECT: '<27>5<EFBFBD>'},
|
||||
'My ICU: {<7B>1:2<>, plural, one {1 - {<7B>5<EFBFBD>, age, 50 {fifty} other {other}}} other {other}}'
|
||||
],
|
||||
|
||||
[
|
||||
'My ICU: {VAR_PLURAL, plural, one {1 - {VAR_PLURAL_1, age, 50 {fifty} other {other}}} other {other}}',
|
||||
{VAR_PLURAL: '<27>1:2<>', VAR_PLURAL_1: '<27>5<EFBFBD>'},
|
||||
'My ICU: {<7B>1:2<>, plural, one {1 - {<7B>5<EFBFBD>, age, 50 {fifty} other {other}}} other {other}}'
|
||||
],
|
||||
|
||||
// ICU replacement
|
||||
[
|
||||
'My ICU #1: <20>I18N_EXP_ICU<43>, My ICU #2: <20>I18N_EXP_ICU<43>',
|
||||
{ICU: ['ICU_VALUE_1', 'ICU_VALUE_2']}, 'My ICU #1: ICU_VALUE_1, My ICU #2: ICU_VALUE_2'
|
||||
],
|
||||
|
||||
// mixed case
|
||||
[
|
||||
`Start: ${str}, ${str}. ICU: {VAR_SELECT, count, 10 {ten} other {other}}.
|
||||
Another ICU: <EFBFBD>I18N_EXP_ICU<EFBFBD> and ${str}, ${str} and one more ICU: <EFBFBD>I18N_EXP_ICU<EFBFBD> and end.`,
|
||||
{VAR_SELECT: '<27>1:2<>', ICU: ['ICU_VALUE_1', 'ICU_VALUE_2']},
|
||||
`Start: ${arr[0]}, ${arr[1]}. ICU: {<7B>1:2<>, count, 10 {ten} other {other}}.
|
||||
Another ICU: ICU_VALUE_1 and ${arr[2]}, ${arr[3]} and one more ICU: ICU_VALUE_2 and end.`,
|
||||
],
|
||||
];
|
||||
cases.forEach(([input, replacements, output]) => {
|
||||
expect(i18nPostprocess(input as string, replacements as any)).toEqual(output as string);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw in case we have invalid string', () => {
|
||||
const arr = ['<27>*1:1<><31>#2:1<>', '<27>#4:2<>', '<27>6:4<>', '<27>/#2:1<><31>/*1:1<>'];
|
||||
const str = `[${arr.join('|')}]`;
|
||||
|
||||
const cases = [
|
||||
// less placeholders than we have
|
||||
[`Start: ${str}, ${str} and ${str} end.`, {}],
|
||||
|
||||
// more placeholders than we have
|
||||
[`Start: ${str}, ${str} and ${str}, ${str} ${str} end.`, {}],
|
||||
|
||||
// not enough ICU replacements
|
||||
['My ICU #1: <20>I18N_EXP_ICU<43>, My ICU #2: <20>I18N_EXP_ICU<43>', {ICU: ['ICU_VALUE_1']}]
|
||||
];
|
||||
cases.forEach(([input, replacements, output]) => {
|
||||
expect(() => i18nPostprocess(input as string, replacements as any)).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue