feat(compiler): add basic support for in ivy/i18n code generation (#22654)

PR Close #22654
This commit is contained in:
Olivier Combe 2018-02-14 10:54:00 -08:00 committed by Matias Niemelä
parent e5e1b0da33
commit 204ba9d413
9 changed files with 496 additions and 85 deletions

View File

@ -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,

View File

@ -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,7 +263,7 @@ 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 {

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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; }

View File

@ -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,

View File

@ -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');
});
});
});

View File

@ -121,5 +121,4 @@ describe('r3_view_compiler', () => {
expectEmit(result.source, bV_call, 'Incorrect bV call');
});
});
});