From 204ba9d4133a2dd82f5e1cd30893c299b6198384 Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Wed, 14 Feb 2018 10:54:00 -0800 Subject: [PATCH] feat(compiler): add basic support for in ivy/i18n code generation (#22654) PR Close #22654 --- packages/compiler/src/aot/compiler_factory.ts | 10 +- packages/compiler/src/constant_pool.ts | 12 +- packages/compiler/src/output/output_ast.ts | 73 ++--- .../compiler/src/render3/r3_identifiers.ts | 2 +- .../compiler/src/render3/r3_view_compiler.ts | 250 +++++++++++++++--- .../src/template_parser/template_parser.ts | 5 +- .../compiler/test/render3/mock_compile.ts | 12 +- .../render3/r3_view_compiler_i18n_spec.ts | 216 +++++++++++++++ .../test/render3/r3_view_compiler_spec.ts | 1 - 9 files changed, 496 insertions(+), 85 deletions(-) create mode 100644 packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index acdb12021a..6122764f43 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -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, diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index 90b54d9a38..c3f236876c 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -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(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; -} \ No newline at end of file +} diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index d10ec23630..9234c64b11 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -252,7 +252,7 @@ export class ReadVarExpr extends Expression { this.builtin = null; } else { this.name = null; - this.builtin = 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; +} diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index bcb95ba672..2bddfd9523 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -115,4 +115,4 @@ export class Identifiers { static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; static listener: o.ExternalReference = {name: 'ɵL', moduleName: CORE}; -} \ No newline at end of file +} diff --git a/packages/compiler/src/render3/r3_view_compiler.ts b/packages/compiler/src/render3/r3_view_compiler.ts index dd0a10429d..6f372768b1 100644 --- a/packages/compiler/src/render3/r3_view_compiler.ts +++ b/packages/compiler/src/render3/r3_view_compiler.ts @@ -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 (`

... somewhere here ...

) + 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(); + 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: + // + // `

some content

` + // 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); } diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index 543f7ddbba..ea60d4ab1b 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -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; } diff --git a/packages/compiler/test/render3/mock_compile.ts b/packages/compiler/test/render3/mock_compile.ts index c036010aca..caec1d0083 100644 --- a/packages/compiler/test/render3/mock_compile.ts +++ b/packages/compiler/test/render3/mock_compile.ts @@ -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 ? '' : 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); } }); -} \ No newline at end of file +} diff --git a/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts b/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts new file mode 100644 index 0000000000..5f5f5ada68 --- /dev/null +++ b/packages/compiler/test/render3/r3_view_compiler_i18n_spec.ts @@ -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: \` +
Hello world
+
&
+
farewell
+ \` + }) + 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: \` +
Hello world
+ \` + }) + 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: \` +
+ \` + }) + 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: \` +
Hello {{name}}!!
+
Other
+
2nd
+
3rd
+ \` + }) + 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: \` +
+ \` + }) + 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'); + }); + + }); +}); diff --git a/packages/compiler/test/render3/r3_view_compiler_spec.ts b/packages/compiler/test/render3/r3_view_compiler_spec.ts index 658944ac17..192aee4b0b 100644 --- a/packages/compiler/test/render3/r3_view_compiler_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_spec.ts @@ -121,5 +121,4 @@ describe('r3_view_compiler', () => { expectEmit(result.source, bV_call, 'Incorrect bV call'); }); }); - });