feat(compiler): add basic support for in ivy/i18n code generation (#22654)
PR Close #22654
This commit is contained in:
parent
e5e1b0da33
commit
204ba9d413
|
@ -65,8 +65,14 @@ export function createAotCompiler(
|
|||
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
|
||||
const staticReflector =
|
||||
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
|
||||
const htmlParser = new I18NHtmlParser(
|
||||
new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
|
||||
let htmlParser: I18NHtmlParser;
|
||||
if (!!options.enableIvy) {
|
||||
// Ivy handles i18n at the compiler level so we must use a regular parser
|
||||
htmlParser = new HtmlParser() as I18NHtmlParser;
|
||||
} else {
|
||||
htmlParser = new I18NHtmlParser(
|
||||
new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
|
||||
}
|
||||
const config = new CompilerConfig({
|
||||
defaultEncapsulation: ViewEncapsulation.Emulated,
|
||||
useJit: false,
|
||||
|
|
|
@ -237,7 +237,13 @@ class KeyVisitor implements o.ExpressionVisitor {
|
|||
`EX:${ast.value.runtime.name}`;
|
||||
}
|
||||
|
||||
visitReadVarExpr = invalid;
|
||||
visitReadVarExpr(ast: o.ReadVarExpr): string {
|
||||
if (!ast.name) {
|
||||
invalid(ast);
|
||||
}
|
||||
return ast.name as string;
|
||||
}
|
||||
|
||||
visitWriteVarExpr = invalid;
|
||||
visitWriteKeyExpr = invalid;
|
||||
visitWritePropExpr = invalid;
|
||||
|
@ -257,9 +263,9 @@ class KeyVisitor implements o.ExpressionVisitor {
|
|||
|
||||
function invalid<T>(arg: o.Expression | o.Statement): never {
|
||||
throw new Error(
|
||||
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`);
|
||||
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${arg.constructor.name}`);
|
||||
}
|
||||
|
||||
function isVariable(e: o.Expression): e is o.ReadVarExpr {
|
||||
return e instanceof o.ReadVarExpr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -252,7 +252,7 @@ export class ReadVarExpr extends Expression {
|
|||
this.builtin = null;
|
||||
} else {
|
||||
this.name = null;
|
||||
this.builtin = <BuiltinVar>name;
|
||||
this.builtin = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1486,7 +1486,11 @@ export function literal(
|
|||
}
|
||||
|
||||
// The list of JSDoc tags that we currently support. Extend it if needed.
|
||||
export const enum JSDocTagName {Desc = 'desc', Id = 'id', Meaning = 'meaning'}
|
||||
export const enum JSDocTagName {
|
||||
Desc = 'desc',
|
||||
Id = 'id',
|
||||
Meaning = 'meaning',
|
||||
}
|
||||
|
||||
/*
|
||||
* TypeScript has an API for JSDoc already, but it's not exposed.
|
||||
|
@ -1496,42 +1500,43 @@ export const enum JSDocTagName {Desc = 'desc', Id = 'id', Meaning = 'meaning'}
|
|||
*/
|
||||
export type JSDocTag = {
|
||||
// `tagName` is e.g. "param" in an `@param` declaration
|
||||
tagName: JSDocTagName | string;
|
||||
tagName: JSDocTagName | string,
|
||||
// Any remaining text on the tag, e.g. the description
|
||||
text?: string;
|
||||
} | {// no `tagName` for plain text documentation that occurs before any `@param` lines
|
||||
tagName?: undefined
|
||||
text?: string,
|
||||
} | {
|
||||
// no `tagName` for plain text documentation that occurs before any `@param` lines
|
||||
tagName?: undefined,
|
||||
text: string,
|
||||
};
|
||||
|
||||
/*
|
||||
* Serializes a `Tag` into a string.
|
||||
* Returns a string like " @foo {bar} baz" (note the leading whitespace before `@foo`).
|
||||
*/
|
||||
function tagToString(tag: JSDocTag): string {
|
||||
let out = '';
|
||||
if (tag.tagName) {
|
||||
out += ` @${tag.tagName}`;
|
||||
}
|
||||
if (tag.text) {
|
||||
if (tag.text.match(/\/\*|\*\//)) {
|
||||
throw new Error('JSDoc text cannot contain "/*" and "*/"');
|
||||
}
|
||||
out += ' ' + tag.text.replace(/@/g, '\\@');
|
||||
}
|
||||
return out;
|
||||
/*
|
||||
* Serializes a `Tag` into a string.
|
||||
* Returns a string like " @foo {bar} baz" (note the leading whitespace before `@foo`).
|
||||
*/
|
||||
function tagToString(tag: JSDocTag): string {
|
||||
let out = '';
|
||||
if (tag.tagName) {
|
||||
out += ` @${tag.tagName}`;
|
||||
}
|
||||
|
||||
function serializeTags(tags: JSDocTag[]): string {
|
||||
if (tags.length === 0) return '';
|
||||
|
||||
let out = '*\n';
|
||||
for (const tag of tags) {
|
||||
out += ' *';
|
||||
// If the tagToString is multi-line, insert " * " prefixes on subsequent lines.
|
||||
out += tagToString(tag).replace(/\n/g, '\n * ');
|
||||
out += '\n';
|
||||
if (tag.text) {
|
||||
if (tag.text.match(/\/\*|\*\//)) {
|
||||
throw new Error('JSDoc text cannot contain "/*" and "*/"');
|
||||
}
|
||||
out += ' ';
|
||||
return out;
|
||||
out += ' ' + tag.text.replace(/@/g, '\\@');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function serializeTags(tags: JSDocTag[]): string {
|
||||
if (tags.length === 0) return '';
|
||||
|
||||
let out = '*\n';
|
||||
for (const tag of tags) {
|
||||
out += ' *';
|
||||
// If the tagToString is multi-line, insert " * " prefixes on subsequent lines.
|
||||
out += tagToString(tag).replace(/\n/g, '\n * ');
|
||||
out += '\n';
|
||||
}
|
||||
out += ' ';
|
||||
return out;
|
||||
}
|
||||
|
|
|
@ -115,4 +115,4 @@ export class Identifiers {
|
|||
static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE};
|
||||
|
||||
static listener: o.ExternalReference = {name: 'ɵL', moduleName: CORE};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,10 @@ import {CssSelector} from '../selector';
|
|||
import {BindingParser} from '../template_parser/binding_parser';
|
||||
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, QueryMatch, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
|
||||
import {OutputContext, error} from '../util';
|
||||
|
||||
import {Identifiers as R3} from './r3_identifiers';
|
||||
import {BUILD_OPTIMIZER_COLOCATE, OutputMode} from './r3_types';
|
||||
|
||||
|
||||
|
||||
/** Name of the context parameter passed into a template function */
|
||||
const CONTEXT_NAME = 'ctx';
|
||||
|
||||
|
@ -40,6 +38,17 @@ const REFERENCE_PREFIX = '_r';
|
|||
/** The name of the implicit context reference */
|
||||
const IMPLICIT_REFERENCE = '$implicit';
|
||||
|
||||
/** Name of the i18n attributes **/
|
||||
const I18N_ATTR = 'i18n';
|
||||
const I18N_ATTR_PREFIX = 'i18n-';
|
||||
|
||||
/** I18n separators for metadata **/
|
||||
const MEANING_SEPARATOR = '|';
|
||||
const ID_SEPARATOR = '@@';
|
||||
|
||||
/** Closure functions **/
|
||||
const GOOG_GET_MSG = 'goog.getMsg';
|
||||
|
||||
export function compileDirective(
|
||||
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
|
||||
bindingParser: BindingParser, mode: OutputMode) {
|
||||
|
@ -302,10 +311,17 @@ class BindingScope {
|
|||
nestedScope(): BindingScope { return new BindingScope(this); }
|
||||
|
||||
freshReferenceName(): string {
|
||||
let current: BindingScope|null = this;
|
||||
let current: BindingScope = this;
|
||||
// Find the top scope as it maintains the global reference count
|
||||
while (current.parent) current = current.parent;
|
||||
return `${REFERENCE_PREFIX}${current.referenceNameIndex++}`;
|
||||
const ref = `${REFERENCE_PREFIX}${current.referenceNameIndex++}`;
|
||||
return ref;
|
||||
}
|
||||
|
||||
// closure variables holding i18n messages are name `MSG_[A-Z0-9]+`
|
||||
freshI18nName(): string {
|
||||
const name = this.freshReferenceName();
|
||||
return `MSG_${name}`.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,6 +344,12 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
private unsupported = unsupported;
|
||||
private invalid = invalid;
|
||||
|
||||
// Whether we are inside a translatable element (`<p i18n>... somewhere here ... </p>)
|
||||
private _inI18nSection: boolean = false;
|
||||
private _i18nSectionIndex = -1;
|
||||
// Maps of placeholder to node indexes for each of the i18n section
|
||||
private _phToNodeIdxes: {[phName: string]: number[]}[] = [{}];
|
||||
|
||||
constructor(
|
||||
private outputCtx: OutputContext, private constantPool: ConstantPool,
|
||||
private reflector: CompileReflector, private contextParameter: string,
|
||||
|
@ -422,6 +444,19 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
[o.ifStmt(o.variable(CREATION_MODE_FLAG), this._creationMode)] :
|
||||
[];
|
||||
|
||||
// Generate maps of placeholder name to node indexes
|
||||
// TODO(vicb): This is a WIP, not fully supported yet
|
||||
for (const phToNodeIdx of this._phToNodeIdxes) {
|
||||
if (Object.keys(phToNodeIdx).length > 0) {
|
||||
const scopedName = this.bindingScope.freshReferenceName();
|
||||
const phMap = o.variable(scopedName)
|
||||
.set(mapToExpression(phToNodeIdx, true))
|
||||
.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||
|
||||
this._prefix.push(phMap);
|
||||
}
|
||||
}
|
||||
|
||||
return o.fn(
|
||||
[
|
||||
new o.FnParam(this.contextParameter, null), new o.FnParam(CREATION_MODE_FLAG, o.BOOL_TYPE)
|
||||
|
@ -429,19 +464,14 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
[
|
||||
// Temporary variable declarations (i.e. let _t: any;)
|
||||
...this._prefix,
|
||||
|
||||
// Creating mode (i.e. if (cm) { ... })
|
||||
...creationMode,
|
||||
|
||||
// Binding mode (i.e. ɵp(...))
|
||||
...this._bindingMode,
|
||||
|
||||
// Host mode (i.e. Comp.h(...))
|
||||
...this._hostMode,
|
||||
|
||||
// Refresh mode (i.e. Comp.r(...))
|
||||
...this._refreshMode,
|
||||
|
||||
// Nested templates (i.e. function CompTemplate() {})
|
||||
...this._postfix
|
||||
],
|
||||
|
@ -480,14 +510,48 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
}
|
||||
|
||||
// TemplateAstVisitor
|
||||
visitElement(ast: ElementAst) {
|
||||
let bindingCount = 0;
|
||||
visitElement(element: ElementAst) {
|
||||
const elementIndex = this.allocateDataSlot();
|
||||
let componentIndex: number|undefined = undefined;
|
||||
const referenceDataSlots = new Map<string, number>();
|
||||
const wasInI18nSection = this._inI18nSection;
|
||||
|
||||
const outputAttrs: {[name: string]: string} = {};
|
||||
const attrI18nMetas: {[name: string]: string} = {};
|
||||
let i18nMeta: string = '';
|
||||
|
||||
// Elements inside i18n sections are replaced with placeholders
|
||||
// TODO(vicb): nested elements are a WIP in this phase
|
||||
if (this._inI18nSection) {
|
||||
const phName = element.name.toLowerCase();
|
||||
if (!this._phToNodeIdxes[this._i18nSectionIndex][phName]) {
|
||||
this._phToNodeIdxes[this._i18nSectionIndex][phName] = [];
|
||||
}
|
||||
this._phToNodeIdxes[this._i18nSectionIndex][phName].push(elementIndex);
|
||||
}
|
||||
|
||||
// Handle i18n attributes
|
||||
for (const attr of element.attrs) {
|
||||
const name = attr.name;
|
||||
const value = attr.value;
|
||||
if (name === I18N_ATTR) {
|
||||
if (this._inI18nSection) {
|
||||
throw new Error(
|
||||
`Could not mark an element as translatable inside of a translatable section`);
|
||||
}
|
||||
this._inI18nSection = true;
|
||||
this._i18nSectionIndex++;
|
||||
this._phToNodeIdxes[this._i18nSectionIndex] = {};
|
||||
i18nMeta = value;
|
||||
} else if (name.startsWith(I18N_ATTR_PREFIX)) {
|
||||
attrI18nMetas[name.slice(I18N_ATTR_PREFIX.length)] = value;
|
||||
} else {
|
||||
outputAttrs[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Element creation mode
|
||||
const component = findComponent(ast.directives);
|
||||
const component = findComponent(element.directives);
|
||||
const nullNode = o.literal(null, o.INFERRED_TYPE);
|
||||
const parameters: o.Expression[] = [o.literal(elementIndex)];
|
||||
|
||||
|
@ -496,21 +560,38 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
parameters.push(this.typeReference(component.directive.type.reference));
|
||||
componentIndex = this.allocateDataSlot();
|
||||
} else {
|
||||
parameters.push(o.literal(ast.name));
|
||||
parameters.push(o.literal(element.name));
|
||||
}
|
||||
|
||||
// Add attributes array
|
||||
// Add the attributes
|
||||
const i18nMessages: o.Statement[] = [];
|
||||
const attributes: o.Expression[] = [];
|
||||
for (let attr of ast.attrs) {
|
||||
attributes.push(o.literal(attr.name), o.literal(attr.value));
|
||||
let hasI18nAttr = false;
|
||||
|
||||
Object.getOwnPropertyNames(outputAttrs).forEach(name => {
|
||||
const value = outputAttrs[name];
|
||||
attributes.push(o.literal(name));
|
||||
if (attrI18nMetas.hasOwnProperty(name)) {
|
||||
hasI18nAttr = true;
|
||||
const {statements, variable} = this.genI18nMessageStmts(value, attrI18nMetas[name]);
|
||||
i18nMessages.push(...statements);
|
||||
attributes.push(variable);
|
||||
} else {
|
||||
attributes.push(o.literal(value));
|
||||
}
|
||||
});
|
||||
|
||||
let attrArg: o.Expression = nullNode;
|
||||
|
||||
if (attributes.length > 0) {
|
||||
attrArg = hasI18nAttr ? getLiteralFactory(this.outputCtx, o.literalArr(attributes)) :
|
||||
this.constantPool.getConstLiteral(o.literalArr(attributes), true);
|
||||
}
|
||||
parameters.push(
|
||||
attributes.length > 0 ?
|
||||
this.constantPool.getConstLiteral(o.literalArr(attributes), /* forceShared */ true) :
|
||||
nullNode);
|
||||
|
||||
parameters.push(attrArg);
|
||||
|
||||
// Add directives array
|
||||
const {directivesArray, directiveIndexMap} = this._computeDirectivesArray(ast.directives);
|
||||
const {directivesArray, directiveIndexMap} = this._computeDirectivesArray(element.directives);
|
||||
parameters.push(directiveIndexMap.size > 0 ? directivesArray : nullNode);
|
||||
|
||||
if (component && componentIndex != null) {
|
||||
|
@ -518,10 +599,9 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
directiveIndexMap.set(component.directive.type.reference, componentIndex);
|
||||
}
|
||||
|
||||
// Add references array
|
||||
if (ast.references && ast.references.length > 0) {
|
||||
if (element.references && element.references.length > 0) {
|
||||
const references =
|
||||
flatten(ast.references.map(reference => {
|
||||
flatten(element.references.map(reference => {
|
||||
const slot = this.allocateDataSlot();
|
||||
referenceDataSlots.set(reference.name, slot);
|
||||
// Generate the update temporary.
|
||||
|
@ -544,17 +624,19 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
}
|
||||
|
||||
// Generate the instruction create element instruction
|
||||
this.instruction(this._creationMode, ast.sourceSpan, R3.createElement, ...parameters);
|
||||
if (i18nMessages.length > 0) {
|
||||
this._creationMode.push(...i18nMessages);
|
||||
}
|
||||
this.instruction(this._creationMode, element.sourceSpan, R3.createElement, ...parameters);
|
||||
|
||||
const implicit = o.variable(this.contextParameter);
|
||||
|
||||
// Generate element input bindings
|
||||
for (let input of ast.inputs) {
|
||||
for (let input of element.inputs) {
|
||||
if (input.isAnimation) {
|
||||
this.unsupported('animations');
|
||||
}
|
||||
const convertedBinding = this.convertPropertyBinding(implicit, input.value);
|
||||
const parameters = [o.literal(elementIndex), o.literal(input.name), convertedBinding];
|
||||
const instruction = BINDING_INSTRUCTION_MAP[input.type];
|
||||
if (instruction) {
|
||||
// TODO(chuckj): runtime: security context?
|
||||
|
@ -567,13 +649,23 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
}
|
||||
|
||||
// Generate directives input bindings
|
||||
this._visitDirectives(ast.directives, implicit, elementIndex, directiveIndexMap);
|
||||
this._visitDirectives(element.directives, implicit, elementIndex, directiveIndexMap);
|
||||
|
||||
// Traverse element child nodes
|
||||
templateVisitAll(this, ast.children);
|
||||
if (this._inI18nSection && element.children.length == 1 &&
|
||||
element.children[0] instanceof TextAst) {
|
||||
const text = element.children[0] as TextAst;
|
||||
this.visitSingleI18nTextChild(text, i18nMeta);
|
||||
} else {
|
||||
templateVisitAll(this, element.children);
|
||||
}
|
||||
|
||||
// Finish element construction mode.
|
||||
this.instruction(this._creationMode, ast.endSourceSpan || ast.sourceSpan, R3.elementEnd);
|
||||
this.instruction(
|
||||
this._creationMode, element.endSourceSpan || element.sourceSpan, R3.elementEnd);
|
||||
|
||||
// Restore the state before exiting this node
|
||||
this._inI18nSection = wasInI18nSection;
|
||||
}
|
||||
|
||||
private _visitDirectives(
|
||||
|
@ -685,6 +777,25 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
o.literal(ast.value));
|
||||
}
|
||||
|
||||
// When the content of the element is a single text node the translation can be inlined:
|
||||
//
|
||||
// `<p i18n="desc|mean">some content</p>`
|
||||
// compiles to
|
||||
// ```
|
||||
// /**
|
||||
// * @desc desc
|
||||
// * @meaning mean
|
||||
// */
|
||||
// const MSG_XYZ = goog.getMsg('some content');
|
||||
// i0.ɵT(1, MSG_XYZ);
|
||||
// ```
|
||||
visitSingleI18nTextChild(text: TextAst, i18nMeta: string) {
|
||||
const {statements, variable} = this.genI18nMessageStmts(text.value, i18nMeta);
|
||||
this._creationMode.push(...statements);
|
||||
this.instruction(
|
||||
this._creationMode, text.sourceSpan, R3.text, o.literal(this.allocateDataSlot()), variable);
|
||||
}
|
||||
|
||||
// These should be handled in the template or element directly
|
||||
readonly visitDirective = invalid;
|
||||
readonly visitDirectiveProperty = invalid;
|
||||
|
@ -724,6 +835,35 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
|||
private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression {
|
||||
return this.convertPropertyBinding(implicit, value);
|
||||
}
|
||||
|
||||
// Transforms an i18n message into a const declaration.
|
||||
//
|
||||
// `message`
|
||||
// becomes
|
||||
// ```
|
||||
// /**
|
||||
// * @desc description?
|
||||
// * @meaning meaning?
|
||||
// */
|
||||
// const MSG_XYZ = goog.getMsg('message');
|
||||
// ```
|
||||
private genI18nMessageStmts(msg: string, meta: string):
|
||||
{statements: o.Statement[], variable: o.ReadVarExpr} {
|
||||
const statements: o.Statement[] = [];
|
||||
const m = parseI18nMeta(meta);
|
||||
const docStmt = i18nMetaToDocStmt(m);
|
||||
if (docStmt) {
|
||||
statements.push(docStmt);
|
||||
}
|
||||
|
||||
// Call closure to get the translation
|
||||
const variable = o.variable(this.bindingScope.freshI18nName());
|
||||
const fnCall = o.variable(GOOG_GET_MSG).callFn([o.literal(msg)]);
|
||||
const msgStmt = variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||
statements.push(msgStmt);
|
||||
|
||||
return {statements, variable};
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryPredicate(query: CompileQueryMetadata, outputCtx: OutputContext): o.Expression {
|
||||
|
@ -954,7 +1094,7 @@ class ValueConverter extends AstMemoryEfficientTransformer {
|
|||
|
||||
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
||||
return new BuiltinFunctionCall(ast.span, this.visitAll(ast.expressions), values => {
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// If the literal has calculated (non-literal) elements transform it into
|
||||
// calls to literal factories that compose the literal and will cache intermediate
|
||||
// values. Otherwise, just return an literal array that contains the values.
|
||||
const literal = o.literalArr(values);
|
||||
|
@ -1052,7 +1192,49 @@ function asLiteral(value: any): o.Expression {
|
|||
return o.literal(value, o.INFERRED_TYPE);
|
||||
}
|
||||
|
||||
function mapToExpression(map: {[key: string]: any}): o.Expression {
|
||||
return o.literalMap(Object.getOwnPropertyNames(map).map(
|
||||
key => ({key, quoted: false, value: o.literal(map[key])})));
|
||||
function mapToExpression(map: {[key: string]: any}, quoted = false): o.Expression {
|
||||
return o.literalMap(
|
||||
Object.getOwnPropertyNames(map).map(key => ({key, quoted, value: asLiteral(map[key])})));
|
||||
}
|
||||
|
||||
// Parse i18n metas like:
|
||||
// - "@@id",
|
||||
// - "description[@@id]",
|
||||
// - "meaning|description[@@id]"
|
||||
function parseI18nMeta(i18n?: string): {description?: string, id?: string, meaning?: string} {
|
||||
let meaning: string|undefined;
|
||||
let description: string|undefined;
|
||||
let id: string|undefined;
|
||||
|
||||
if (i18n) {
|
||||
// TODO(vicb): figure out how to force a message ID with closure ?
|
||||
const idIndex = i18n.indexOf(ID_SEPARATOR);
|
||||
|
||||
const descIndex = i18n.indexOf(MEANING_SEPARATOR);
|
||||
let meaningAndDesc: string;
|
||||
[meaningAndDesc, id] =
|
||||
(idIndex > -1) ? [i18n.slice(0, idIndex), i18n.slice(idIndex + 2)] : [i18n, ''];
|
||||
[meaning, description] = (descIndex > -1) ?
|
||||
[meaningAndDesc.slice(0, descIndex), meaningAndDesc.slice(descIndex + 1)] :
|
||||
['', meaningAndDesc];
|
||||
}
|
||||
|
||||
return {description, id, meaning};
|
||||
}
|
||||
|
||||
// Converts i18n meta informations for a message (description, meaning) to a JsDoc statement
|
||||
// formatted as expected by the Closure compiler.
|
||||
function i18nMetaToDocStmt(meta: {description?: string, id?: string, meaning?: string}):
|
||||
o.JSDocCommentStmt|null {
|
||||
const tags: o.JSDocTag[] = [];
|
||||
|
||||
if (meta.description) {
|
||||
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
|
||||
}
|
||||
|
||||
if (meta.meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||
}
|
||||
|
||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,9 @@ import {CompilerConfig} from '../config';
|
|||
import {SchemaMetadata} from '../core';
|
||||
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
|
||||
import {Parser} from '../expression_parser/parser';
|
||||
import {I18NHtmlParser} from '../i18n/i18n_html_parser';
|
||||
import {Identifiers, createTokenForExternalReference, createTokenForReference} from '../identifiers';
|
||||
import * as html from '../ml_parser/ast';
|
||||
import {ParseTreeResult} from '../ml_parser/html_parser';
|
||||
import {HtmlParser, ParseTreeResult} from '../ml_parser/html_parser';
|
||||
import {removeWhitespaces, replaceNgsp} from '../ml_parser/html_whitespaces';
|
||||
import {expandNodes} from '../ml_parser/icu_ast_expander';
|
||||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
|
@ -88,7 +87,7 @@ export class TemplateParser {
|
|||
constructor(
|
||||
private _config: CompilerConfig, private _reflector: CompileReflector,
|
||||
private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry,
|
||||
private _htmlParser: I18NHtmlParser, private _console: Console,
|
||||
private _htmlParser: HtmlParser, private _console: Console,
|
||||
public transforms: TemplateAstVisitor[]) {}
|
||||
|
||||
public get expressionParser() { return this._exprParser; }
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileIdentifierMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompileTypeMetadata, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, ParseError, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver, templateSourceUrl} from '@angular/compiler';
|
||||
import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileIdentifierMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompileTypeMetadata, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, Lexer, NgModuleResolver, ParseError, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver, templateSourceUrl} from '@angular/compiler';
|
||||
import {ViewEncapsulation} from '@angular/core';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
|
@ -21,7 +21,7 @@ import {OutputMode} from '../../src/render3/r3_types';
|
|||
import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler';
|
||||
import {BindingParser} from '../../src/template_parser/binding_parser';
|
||||
import {OutputContext} from '../../src/util';
|
||||
import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, expectNoDiagnostics, settings, setup, toMockFileArray} from '../aot/test_util';
|
||||
import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, expectNoDiagnostics, settings, toMockFileArray} from '../aot/test_util';
|
||||
|
||||
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
|
||||
const OPERATOR =
|
||||
|
@ -76,7 +76,7 @@ export function expectEmit(source: string, emitted: string, description: string)
|
|||
const expr = r(...pieces);
|
||||
if (!expr.test(source)) {
|
||||
let last: number = 0;
|
||||
for (let i = 1; i < pieces.length; i++) {
|
||||
for (let i = 1; i <= pieces.length; i++) {
|
||||
const t = r(...pieces.slice(0, i));
|
||||
const m = source.match(t);
|
||||
const expected = pieces[i - 1] == IDENT ? '<IDENT>' : pieces[i - 1];
|
||||
|
@ -145,7 +145,6 @@ function doCompile(
|
|||
|
||||
// TODO(chuckj): Replace with a variant of createAotCompiler() when the r3_view_compiler is
|
||||
// integrated
|
||||
const translations = options.translations || '';
|
||||
|
||||
const urlResolver = createAotUrlResolver(compilerHost);
|
||||
const symbolCache = new StaticSymbolCache();
|
||||
|
@ -153,8 +152,7 @@ function doCompile(
|
|||
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
|
||||
const staticReflector =
|
||||
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
|
||||
const htmlParser = new I18NHtmlParser(
|
||||
new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
|
||||
const htmlParser = new HtmlParser();
|
||||
const config = new CompilerConfig({
|
||||
defaultEncapsulation: ViewEncapsulation.Emulated,
|
||||
useJit: false,
|
||||
|
@ -341,4 +339,4 @@ export function createFactories(
|
|||
compileModuleFactory(outputCtx, module, getBackPatchReference, resolver);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* @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 {setup} from '../aot/test_util';
|
||||
import {compile, expectEmit} from './mock_compile';
|
||||
|
||||
describe('i18n support in the view compiler', () => {
|
||||
const angularFiles = setup({
|
||||
compileAngular: true,
|
||||
compileAnimations: false,
|
||||
compileCommon: true,
|
||||
});
|
||||
|
||||
describe('single text nodes', () => {
|
||||
it('should translate single text nodes with the i18n attribute', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div i18n>Hello world</div>
|
||||
<div>&</div>
|
||||
<div i18n>farewell</div>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const template = `
|
||||
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
||||
if (cm) {
|
||||
…
|
||||
const $g2$ = goog.getMsg('Hello world');
|
||||
$r3$.ɵT(1, $g2$);
|
||||
…
|
||||
$r3$.ɵT(3,'&');
|
||||
…
|
||||
const $g3$ = goog.getMsg('farewell');
|
||||
$r3$.ɵT(5, $g3$);
|
||||
…
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
|
||||
it('should add the meaning and description as JsDoc comments', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div i18n="meaning|desc@@id" i18n-title="desc" title="introduction">Hello world</div>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const template = `
|
||||
const $c1$ = ($a1$:any) => {
|
||||
return ['title', $a1$];
|
||||
};
|
||||
…
|
||||
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
||||
if (cm) {
|
||||
/**
|
||||
* @desc desc
|
||||
*/
|
||||
const $g1$ = goog.getMsg('introduction');
|
||||
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $g1$));
|
||||
/**
|
||||
* @desc desc
|
||||
* @meaning meaning
|
||||
*/
|
||||
const $g2$ = goog.getMsg('Hello world');
|
||||
$r3$.ɵT(1, $g2$);
|
||||
$r3$.ɵe();
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
});
|
||||
|
||||
describe('static attributes', () => {
|
||||
it('should translate static attributes', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div i18n id="static" i18n-title="m|d" title="introduction"></div>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const template = `
|
||||
const $c1$ = ($a1$:any) => {
|
||||
return ['id', 'static', 'title', $a1$];
|
||||
};
|
||||
…
|
||||
template: function MyComponent_Template(ctx: IDENT, cm: IDENT) {
|
||||
if (cm) {
|
||||
/**
|
||||
* @desc d
|
||||
* @meaning m
|
||||
*/
|
||||
const $g1$ = goog.getMsg('introduction');
|
||||
$r3$.ɵE(0, 'div', $r3$.ɵf1($c1$, $g1$));
|
||||
$r3$.ɵe();
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(vicb): this feature is not supported yet
|
||||
xdescribe('nested nodes', () => {
|
||||
it('should generate the placeholders maps', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div i18n>Hello <b>{{name}}<i>!</i><i>!</i></b></div>
|
||||
<div>Other</div>
|
||||
<div i18n>2nd</div>
|
||||
<div i18n><i>3rd</i></div>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const template = `
|
||||
const $r1$ = {'b':[2], 'i':[4, 6]};
|
||||
const $r2$ = {'i':[13]};
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, template, 'Incorrect template');
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('should throw on nested i18n sections', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-component',
|
||||
template: \`
|
||||
<div i18n><div i18n></div></div>
|
||||
\`
|
||||
})
|
||||
export class MyComponent {}
|
||||
|
||||
@NgModule({declarations: [MyComponent]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
expect(() => compile(files, angularFiles))
|
||||
.toThrowError(
|
||||
'Could not mark an element as translatable inside of a translatable section');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -121,5 +121,4 @@ describe('r3_view_compiler', () => {
|
|||
expectEmit(result.source, bV_call, 'Incorrect bV call');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue