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 symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
|
||||||
const staticReflector =
|
const staticReflector =
|
||||||
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
|
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
|
||||||
const htmlParser = new I18NHtmlParser(
|
let htmlParser: I18NHtmlParser;
|
||||||
new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
|
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({
|
const config = new CompilerConfig({
|
||||||
defaultEncapsulation: ViewEncapsulation.Emulated,
|
defaultEncapsulation: ViewEncapsulation.Emulated,
|
||||||
useJit: false,
|
useJit: false,
|
||||||
|
|
|
@ -237,7 +237,13 @@ class KeyVisitor implements o.ExpressionVisitor {
|
||||||
`EX:${ast.value.runtime.name}`;
|
`EX:${ast.value.runtime.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitReadVarExpr = invalid;
|
visitReadVarExpr(ast: o.ReadVarExpr): string {
|
||||||
|
if (!ast.name) {
|
||||||
|
invalid(ast);
|
||||||
|
}
|
||||||
|
return ast.name as string;
|
||||||
|
}
|
||||||
|
|
||||||
visitWriteVarExpr = invalid;
|
visitWriteVarExpr = invalid;
|
||||||
visitWriteKeyExpr = invalid;
|
visitWriteKeyExpr = invalid;
|
||||||
visitWritePropExpr = invalid;
|
visitWritePropExpr = invalid;
|
||||||
|
@ -257,9 +263,9 @@ class KeyVisitor implements o.ExpressionVisitor {
|
||||||
|
|
||||||
function invalid<T>(arg: o.Expression | o.Statement): never {
|
function invalid<T>(arg: o.Expression | o.Statement): never {
|
||||||
throw new Error(
|
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 {
|
function isVariable(e: o.Expression): e is o.ReadVarExpr {
|
||||||
return e instanceof o.ReadVarExpr;
|
return e instanceof o.ReadVarExpr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -252,7 +252,7 @@ export class ReadVarExpr extends Expression {
|
||||||
this.builtin = null;
|
this.builtin = null;
|
||||||
} else {
|
} else {
|
||||||
this.name = null;
|
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.
|
// 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.
|
* 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 = {
|
export type JSDocTag = {
|
||||||
// `tagName` is e.g. "param" in an `@param` declaration
|
// `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
|
// Any remaining text on the tag, e.g. the description
|
||||||
text?: string;
|
text?: string,
|
||||||
} | {// no `tagName` for plain text documentation that occurs before any `@param` lines
|
} | {
|
||||||
tagName?: undefined
|
// no `tagName` for plain text documentation that occurs before any `@param` lines
|
||||||
|
tagName?: undefined,
|
||||||
text: string,
|
text: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Serializes a `Tag` into a string.
|
* Serializes a `Tag` into a string.
|
||||||
* Returns a string like " @foo {bar} baz" (note the leading whitespace before `@foo`).
|
* Returns a string like " @foo {bar} baz" (note the leading whitespace before `@foo`).
|
||||||
*/
|
*/
|
||||||
function tagToString(tag: JSDocTag): string {
|
function tagToString(tag: JSDocTag): string {
|
||||||
let out = '';
|
let out = '';
|
||||||
if (tag.tagName) {
|
if (tag.tagName) {
|
||||||
out += ` @${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;
|
|
||||||
}
|
}
|
||||||
|
if (tag.text) {
|
||||||
function serializeTags(tags: JSDocTag[]): string {
|
if (tag.text.match(/\/\*|\*\//)) {
|
||||||
if (tags.length === 0) return '';
|
throw new Error('JSDoc text cannot contain "/*" and "*/"');
|
||||||
|
|
||||||
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 += ' ';
|
out += ' ' + tag.text.replace(/@/g, '\\@');
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
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 NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE};
|
||||||
|
|
||||||
static listener: o.ExternalReference = {name: 'ɵL', 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 {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 {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 {OutputContext, error} from '../util';
|
||||||
|
|
||||||
import {Identifiers as R3} from './r3_identifiers';
|
import {Identifiers as R3} from './r3_identifiers';
|
||||||
import {BUILD_OPTIMIZER_COLOCATE, OutputMode} from './r3_types';
|
import {BUILD_OPTIMIZER_COLOCATE, OutputMode} from './r3_types';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Name of the context parameter passed into a template function */
|
/** Name of the context parameter passed into a template function */
|
||||||
const CONTEXT_NAME = 'ctx';
|
const CONTEXT_NAME = 'ctx';
|
||||||
|
|
||||||
|
@ -40,6 +38,17 @@ const REFERENCE_PREFIX = '_r';
|
||||||
/** The name of the implicit context reference */
|
/** The name of the implicit context reference */
|
||||||
const IMPLICIT_REFERENCE = '$implicit';
|
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(
|
export function compileDirective(
|
||||||
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
|
outputCtx: OutputContext, directive: CompileDirectiveMetadata, reflector: CompileReflector,
|
||||||
bindingParser: BindingParser, mode: OutputMode) {
|
bindingParser: BindingParser, mode: OutputMode) {
|
||||||
|
@ -302,10 +311,17 @@ class BindingScope {
|
||||||
nestedScope(): BindingScope { return new BindingScope(this); }
|
nestedScope(): BindingScope { return new BindingScope(this); }
|
||||||
|
|
||||||
freshReferenceName(): string {
|
freshReferenceName(): string {
|
||||||
let current: BindingScope|null = this;
|
let current: BindingScope = this;
|
||||||
// Find the top scope as it maintains the global reference count
|
// Find the top scope as it maintains the global reference count
|
||||||
while (current.parent) current = current.parent;
|
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 unsupported = unsupported;
|
||||||
private invalid = invalid;
|
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(
|
constructor(
|
||||||
private outputCtx: OutputContext, private constantPool: ConstantPool,
|
private outputCtx: OutputContext, private constantPool: ConstantPool,
|
||||||
private reflector: CompileReflector, private contextParameter: string,
|
private reflector: CompileReflector, private contextParameter: string,
|
||||||
|
@ -422,6 +444,19 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
[o.ifStmt(o.variable(CREATION_MODE_FLAG), this._creationMode)] :
|
[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(
|
return o.fn(
|
||||||
[
|
[
|
||||||
new o.FnParam(this.contextParameter, null), new o.FnParam(CREATION_MODE_FLAG, o.BOOL_TYPE)
|
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;)
|
// Temporary variable declarations (i.e. let _t: any;)
|
||||||
...this._prefix,
|
...this._prefix,
|
||||||
|
|
||||||
// Creating mode (i.e. if (cm) { ... })
|
// Creating mode (i.e. if (cm) { ... })
|
||||||
...creationMode,
|
...creationMode,
|
||||||
|
|
||||||
// Binding mode (i.e. ɵp(...))
|
// Binding mode (i.e. ɵp(...))
|
||||||
...this._bindingMode,
|
...this._bindingMode,
|
||||||
|
|
||||||
// Host mode (i.e. Comp.h(...))
|
// Host mode (i.e. Comp.h(...))
|
||||||
...this._hostMode,
|
...this._hostMode,
|
||||||
|
|
||||||
// Refresh mode (i.e. Comp.r(...))
|
// Refresh mode (i.e. Comp.r(...))
|
||||||
...this._refreshMode,
|
...this._refreshMode,
|
||||||
|
|
||||||
// Nested templates (i.e. function CompTemplate() {})
|
// Nested templates (i.e. function CompTemplate() {})
|
||||||
...this._postfix
|
...this._postfix
|
||||||
],
|
],
|
||||||
|
@ -480,14 +510,48 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateAstVisitor
|
// TemplateAstVisitor
|
||||||
visitElement(ast: ElementAst) {
|
visitElement(element: ElementAst) {
|
||||||
let bindingCount = 0;
|
|
||||||
const elementIndex = this.allocateDataSlot();
|
const elementIndex = this.allocateDataSlot();
|
||||||
let componentIndex: number|undefined = undefined;
|
let componentIndex: number|undefined = undefined;
|
||||||
const referenceDataSlots = new Map<string, number>();
|
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
|
// Element creation mode
|
||||||
const component = findComponent(ast.directives);
|
const component = findComponent(element.directives);
|
||||||
const nullNode = o.literal(null, o.INFERRED_TYPE);
|
const nullNode = o.literal(null, o.INFERRED_TYPE);
|
||||||
const parameters: o.Expression[] = [o.literal(elementIndex)];
|
const parameters: o.Expression[] = [o.literal(elementIndex)];
|
||||||
|
|
||||||
|
@ -496,21 +560,38 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
parameters.push(this.typeReference(component.directive.type.reference));
|
parameters.push(this.typeReference(component.directive.type.reference));
|
||||||
componentIndex = this.allocateDataSlot();
|
componentIndex = this.allocateDataSlot();
|
||||||
} else {
|
} 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[] = [];
|
const attributes: o.Expression[] = [];
|
||||||
for (let attr of ast.attrs) {
|
let hasI18nAttr = false;
|
||||||
attributes.push(o.literal(attr.name), o.literal(attr.value));
|
|
||||||
|
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 ?
|
parameters.push(attrArg);
|
||||||
this.constantPool.getConstLiteral(o.literalArr(attributes), /* forceShared */ true) :
|
|
||||||
nullNode);
|
|
||||||
|
|
||||||
// Add directives array
|
// Add directives array
|
||||||
const {directivesArray, directiveIndexMap} = this._computeDirectivesArray(ast.directives);
|
const {directivesArray, directiveIndexMap} = this._computeDirectivesArray(element.directives);
|
||||||
parameters.push(directiveIndexMap.size > 0 ? directivesArray : nullNode);
|
parameters.push(directiveIndexMap.size > 0 ? directivesArray : nullNode);
|
||||||
|
|
||||||
if (component && componentIndex != null) {
|
if (component && componentIndex != null) {
|
||||||
|
@ -518,10 +599,9 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
directiveIndexMap.set(component.directive.type.reference, componentIndex);
|
directiveIndexMap.set(component.directive.type.reference, componentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add references array
|
if (element.references && element.references.length > 0) {
|
||||||
if (ast.references && ast.references.length > 0) {
|
|
||||||
const references =
|
const references =
|
||||||
flatten(ast.references.map(reference => {
|
flatten(element.references.map(reference => {
|
||||||
const slot = this.allocateDataSlot();
|
const slot = this.allocateDataSlot();
|
||||||
referenceDataSlots.set(reference.name, slot);
|
referenceDataSlots.set(reference.name, slot);
|
||||||
// Generate the update temporary.
|
// Generate the update temporary.
|
||||||
|
@ -544,17 +624,19 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the instruction create element instruction
|
// 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);
|
const implicit = o.variable(this.contextParameter);
|
||||||
|
|
||||||
// Generate element input bindings
|
// Generate element input bindings
|
||||||
for (let input of ast.inputs) {
|
for (let input of element.inputs) {
|
||||||
if (input.isAnimation) {
|
if (input.isAnimation) {
|
||||||
this.unsupported('animations');
|
this.unsupported('animations');
|
||||||
}
|
}
|
||||||
const convertedBinding = this.convertPropertyBinding(implicit, input.value);
|
const convertedBinding = this.convertPropertyBinding(implicit, input.value);
|
||||||
const parameters = [o.literal(elementIndex), o.literal(input.name), convertedBinding];
|
|
||||||
const instruction = BINDING_INSTRUCTION_MAP[input.type];
|
const instruction = BINDING_INSTRUCTION_MAP[input.type];
|
||||||
if (instruction) {
|
if (instruction) {
|
||||||
// TODO(chuckj): runtime: security context?
|
// TODO(chuckj): runtime: security context?
|
||||||
|
@ -567,13 +649,23 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate directives input bindings
|
// Generate directives input bindings
|
||||||
this._visitDirectives(ast.directives, implicit, elementIndex, directiveIndexMap);
|
this._visitDirectives(element.directives, implicit, elementIndex, directiveIndexMap);
|
||||||
|
|
||||||
// Traverse element child nodes
|
// 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.
|
// 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(
|
private _visitDirectives(
|
||||||
|
@ -685,6 +777,25 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
o.literal(ast.value));
|
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
|
// These should be handled in the template or element directly
|
||||||
readonly visitDirective = invalid;
|
readonly visitDirective = invalid;
|
||||||
readonly visitDirectiveProperty = invalid;
|
readonly visitDirectiveProperty = invalid;
|
||||||
|
@ -724,6 +835,35 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
|
||||||
private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression {
|
private bind(implicit: o.Expression, value: AST, sourceSpan: ParseSourceSpan): o.Expression {
|
||||||
return this.convertPropertyBinding(implicit, value);
|
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 {
|
function getQueryPredicate(query: CompileQueryMetadata, outputCtx: OutputContext): o.Expression {
|
||||||
|
@ -954,7 +1094,7 @@ class ValueConverter extends AstMemoryEfficientTransformer {
|
||||||
|
|
||||||
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
||||||
return new BuiltinFunctionCall(ast.span, this.visitAll(ast.expressions), values => {
|
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
|
// calls to literal factories that compose the literal and will cache intermediate
|
||||||
// values. Otherwise, just return an literal array that contains the values.
|
// values. Otherwise, just return an literal array that contains the values.
|
||||||
const literal = o.literalArr(values);
|
const literal = o.literalArr(values);
|
||||||
|
@ -1052,7 +1192,49 @@ function asLiteral(value: any): o.Expression {
|
||||||
return o.literal(value, o.INFERRED_TYPE);
|
return o.literal(value, o.INFERRED_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapToExpression(map: {[key: string]: any}): o.Expression {
|
function mapToExpression(map: {[key: string]: any}, quoted = false): o.Expression {
|
||||||
return o.literalMap(Object.getOwnPropertyNames(map).map(
|
return o.literalMap(
|
||||||
key => ({key, quoted: false, value: o.literal(map[key])})));
|
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 {SchemaMetadata} from '../core';
|
||||||
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
|
import {AST, ASTWithSource, EmptyExpr} from '../expression_parser/ast';
|
||||||
import {Parser} from '../expression_parser/parser';
|
import {Parser} from '../expression_parser/parser';
|
||||||
import {I18NHtmlParser} from '../i18n/i18n_html_parser';
|
|
||||||
import {Identifiers, createTokenForExternalReference, createTokenForReference} from '../identifiers';
|
import {Identifiers, createTokenForExternalReference, createTokenForReference} from '../identifiers';
|
||||||
import * as html from '../ml_parser/ast';
|
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 {removeWhitespaces, replaceNgsp} from '../ml_parser/html_whitespaces';
|
||||||
import {expandNodes} from '../ml_parser/icu_ast_expander';
|
import {expandNodes} from '../ml_parser/icu_ast_expander';
|
||||||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||||
|
@ -88,7 +87,7 @@ export class TemplateParser {
|
||||||
constructor(
|
constructor(
|
||||||
private _config: CompilerConfig, private _reflector: CompileReflector,
|
private _config: CompilerConfig, private _reflector: CompileReflector,
|
||||||
private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry,
|
private _exprParser: Parser, private _schemaRegistry: ElementSchemaRegistry,
|
||||||
private _htmlParser: I18NHtmlParser, private _console: Console,
|
private _htmlParser: HtmlParser, private _console: Console,
|
||||||
public transforms: TemplateAstVisitor[]) {}
|
public transforms: TemplateAstVisitor[]) {}
|
||||||
|
|
||||||
public get expressionParser() { return this._exprParser; }
|
public get expressionParser() { return this._exprParser; }
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {ViewEncapsulation} from '@angular/core';
|
||||||
import * as ts from 'typescript';
|
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 {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler';
|
||||||
import {BindingParser} from '../../src/template_parser/binding_parser';
|
import {BindingParser} from '../../src/template_parser/binding_parser';
|
||||||
import {OutputContext} from '../../src/util';
|
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 IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
|
||||||
const OPERATOR =
|
const OPERATOR =
|
||||||
|
@ -76,7 +76,7 @@ export function expectEmit(source: string, emitted: string, description: string)
|
||||||
const expr = r(...pieces);
|
const expr = r(...pieces);
|
||||||
if (!expr.test(source)) {
|
if (!expr.test(source)) {
|
||||||
let last: number = 0;
|
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 t = r(...pieces.slice(0, i));
|
||||||
const m = source.match(t);
|
const m = source.match(t);
|
||||||
const expected = pieces[i - 1] == IDENT ? '<IDENT>' : pieces[i - 1];
|
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
|
// TODO(chuckj): Replace with a variant of createAotCompiler() when the r3_view_compiler is
|
||||||
// integrated
|
// integrated
|
||||||
const translations = options.translations || '';
|
|
||||||
|
|
||||||
const urlResolver = createAotUrlResolver(compilerHost);
|
const urlResolver = createAotUrlResolver(compilerHost);
|
||||||
const symbolCache = new StaticSymbolCache();
|
const symbolCache = new StaticSymbolCache();
|
||||||
|
@ -153,8 +152,7 @@ function doCompile(
|
||||||
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
|
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
|
||||||
const staticReflector =
|
const staticReflector =
|
||||||
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
|
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
|
||||||
const htmlParser = new I18NHtmlParser(
|
const htmlParser = new HtmlParser();
|
||||||
new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
|
|
||||||
const config = new CompilerConfig({
|
const config = new CompilerConfig({
|
||||||
defaultEncapsulation: ViewEncapsulation.Emulated,
|
defaultEncapsulation: ViewEncapsulation.Emulated,
|
||||||
useJit: false,
|
useJit: false,
|
||||||
|
@ -341,4 +339,4 @@ export function createFactories(
|
||||||
compileModuleFactory(outputCtx, module, getBackPatchReference, resolver);
|
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');
|
expectEmit(result.source, bV_call, 'Incorrect bV call');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue