From 827c3fe199310b548dc860868b0777d888d8234b Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 25 Jan 2017 23:26:49 -0800 Subject: [PATCH] fix(compiler): fix missing translations handling (#14113) PR Close #14113 --- .../compiler/src/aot/compiler_factory.ts | 8 +- modules/@angular/compiler/src/config.ts | 9 +- .../compiler/src/i18n/i18n_html_parser.ts | 10 +- .../@angular/compiler/src/i18n/parse_util.ts | 6 +- .../compiler/src/i18n/translation_bundle.ts | 105 ++++++++++-------- .../compiler/src/jit/compiler_factory.ts | 34 +++--- .../test/i18n/extractor_merger_spec.ts | 2 +- .../test/i18n/translation_bundle_spec.ts | 58 ++++++---- modules/@angular/core/src/core.ts | 2 +- modules/@angular/core/src/i18n/tokens.ts | 5 - modules/@angular/core/src/linker/compiler.ts | 2 + tools/public_api_guard/core/index.d.ts | 5 +- 12 files changed, 139 insertions(+), 107 deletions(-) diff --git a/modules/@angular/compiler/src/aot/compiler_factory.ts b/modules/@angular/compiler/src/aot/compiler_factory.ts index 6bb7f64dd6..36a2ad2c42 100644 --- a/modules/@angular/compiler/src/aot/compiler_factory.ts +++ b/modules/@angular/compiler/src/aot/compiler_factory.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ViewEncapsulation} from '@angular/core'; +import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core'; import {AnimationParser} from '../animation/animation_parser'; import {CompilerConfig} from '../config'; @@ -53,7 +53,10 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver); const staticReflector = new StaticReflector(symbolResolver); StaticAndDynamicReflectionCapabilities.install(staticReflector); - const htmlParser = new I18NHtmlParser(new HtmlParser(), translations, options.i18nFormat); + const console = new Console(); + const htmlParser = new I18NHtmlParser( + new HtmlParser(), translations, options.i18nFormat, MissingTranslationStrategy.Warning, + console); const config = new CompilerConfig({ genDebugInfo: options.debug === true, defaultEncapsulation: ViewEncapsulation.Emulated, @@ -64,7 +67,6 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config); const expressionParser = new Parser(new Lexer()); const elementSchemaRegistry = new DomElementSchemaRegistry(); - const console = new Console(); const tmplParser = new TemplateParser(expressionParser, elementSchemaRegistry, htmlParser, console, []); const resolver = new CompileMetadataResolver( diff --git a/modules/@angular/compiler/src/config.ts b/modules/@angular/compiler/src/config.ts index de4ed3e2b6..a81ec90fa3 100644 --- a/modules/@angular/compiler/src/config.ts +++ b/modules/@angular/compiler/src/config.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ViewEncapsulation, isDevMode} from '@angular/core'; +import {MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core'; import {CompileIdentifierMetadata} from './compile_metadata'; import {Identifiers, createIdentifier} from './identifiers'; @@ -21,21 +21,24 @@ export class CompilerConfig { private _genDebugInfo: boolean; private _logBindingUpdate: boolean; public useJit: boolean; + public missingTranslation: MissingTranslationStrategy; constructor( {renderTypes = new DefaultRenderTypes(), defaultEncapsulation = ViewEncapsulation.Emulated, - genDebugInfo, logBindingUpdate, useJit = true}: { + genDebugInfo, logBindingUpdate, useJit = true, missingTranslation}: { renderTypes?: RenderTypes, defaultEncapsulation?: ViewEncapsulation, genDebugInfo?: boolean, logBindingUpdate?: boolean, - useJit?: boolean + useJit?: boolean, + missingTranslation?: MissingTranslationStrategy, } = {}) { this.renderTypes = renderTypes; this.defaultEncapsulation = defaultEncapsulation; this._genDebugInfo = genDebugInfo; this._logBindingUpdate = logBindingUpdate; this.useJit = useJit; + this.missingTranslation = missingTranslation; } get genDebugInfo(): boolean { diff --git a/modules/@angular/compiler/src/i18n/i18n_html_parser.ts b/modules/@angular/compiler/src/i18n/i18n_html_parser.ts index 411939adb9..4e994967ca 100644 --- a/modules/@angular/compiler/src/i18n/i18n_html_parser.ts +++ b/modules/@angular/compiler/src/i18n/i18n_html_parser.ts @@ -11,6 +11,7 @@ import {MissingTranslationStrategy} from '@angular/core'; import {HtmlParser} from '../ml_parser/html_parser'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config'; import {ParseTreeResult} from '../ml_parser/parser'; +import {Console} from '../private_import_core'; import {mergeTranslations} from './extractor_merger'; import {Serializer} from './serializers/serializer'; @@ -23,14 +24,11 @@ export class I18NHtmlParser implements HtmlParser { // @override getTagDefinition: any; - // TODO(vicb): transB.load() should not need a msgB & add transB.resolve(msgB, - // interpolationConfig) - // TODO(vicb): remove the interpolationConfig from the Xtb serializer constructor( private _htmlParser: HtmlParser, private _translations?: string, private _translationsFormat?: string, - private _missingTranslationStrategy: - MissingTranslationStrategy = MissingTranslationStrategy.Error) {} + private _missingTranslation: MissingTranslationStrategy = MissingTranslationStrategy.Warning, + private _console?: Console) {} parse( source: string, url: string, parseExpansionForms: boolean = false, @@ -51,7 +49,7 @@ export class I18NHtmlParser implements HtmlParser { const serializer = this._createSerializer(); const translationBundle = TranslationBundle.load( - this._translations, url, serializer, this._missingTranslationStrategy); + this._translations, url, serializer, this._missingTranslation, this._console); return mergeTranslations(parseResult.rootNodes, translationBundle, interpolationConfig, [], {}); } diff --git a/modules/@angular/compiler/src/i18n/parse_util.ts b/modules/@angular/compiler/src/i18n/parse_util.ts index 928cba1038..37acd859ac 100644 --- a/modules/@angular/compiler/src/i18n/parse_util.ts +++ b/modules/@angular/compiler/src/i18n/parse_util.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; +import {ParseError, ParseSourceSpan} from '../parse_util'; /** * An i18n error. @@ -14,7 +14,3 @@ import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; export class I18nError extends ParseError { constructor(span: ParseSourceSpan, msg: string) { super(span, msg); } } - -export class I18nWarning extends ParseError { - constructor(span: ParseSourceSpan, msg: string) { super(span, msg, ParseErrorLevel.WARNING); } -} diff --git a/modules/@angular/compiler/src/i18n/translation_bundle.ts b/modules/@angular/compiler/src/i18n/translation_bundle.ts index f73deab9c8..bf2cc79a20 100644 --- a/modules/@angular/compiler/src/i18n/translation_bundle.ts +++ b/modules/@angular/compiler/src/i18n/translation_bundle.ts @@ -8,13 +8,12 @@ import {MissingTranslationStrategy} from '@angular/core'; -import {warn} from '../facade/lang'; import * as html from '../ml_parser/ast'; import {HtmlParser} from '../ml_parser/html_parser'; +import {Console} from '../private_import_core'; -import {serializeNodes} from './digest'; import * as i18n from './i18n_ast'; -import {I18nError, I18nWarning} from './parse_util'; +import {I18nError} from './parse_util'; import {PlaceholderMapper, Serializer} from './serializers/serializer'; /** @@ -27,30 +26,28 @@ export class TranslationBundle { private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, public digest: (m: i18n.Message) => string, public mapperFactory?: (m: i18n.Message) => PlaceholderMapper, - missingTranslationStrategy: MissingTranslationStrategy = MissingTranslationStrategy.Warning) { - this._i18nToHtml = - new I18nToHtmlVisitor(_i18nNodesByMsgId, digest, mapperFactory, missingTranslationStrategy); + missingTranslationStrategy: MissingTranslationStrategy = MissingTranslationStrategy.Warning, + console?: Console) { + this._i18nToHtml = new I18nToHtmlVisitor( + _i18nNodesByMsgId, digest, mapperFactory, missingTranslationStrategy, console); } // Creates a `TranslationBundle` by parsing the given `content` with the `serializer`. static load( content: string, url: string, serializer: Serializer, - missingTranslationStrategy: MissingTranslationStrategy): TranslationBundle { + missingTranslationStrategy: MissingTranslationStrategy, + console?: Console): TranslationBundle { const i18nNodesByMsgId = serializer.load(content, url); const digestFn = (m: i18n.Message) => serializer.digest(m); const mapperFactory = (m: i18n.Message) => serializer.createNameMapper(m); return new TranslationBundle( - i18nNodesByMsgId, digestFn, mapperFactory, missingTranslationStrategy); + i18nNodesByMsgId, digestFn, mapperFactory, missingTranslationStrategy, console); } // Returns the translation as HTML nodes from the given source message. get(srcMsg: i18n.Message): html.Node[] { const html = this._i18nToHtml.convert(srcMsg); - if (html.warnings.length) { - warn(html.warnings.join('\n')); - } - if (html.errors.length) { throw new Error(html.errors.join('\n')); } @@ -66,16 +63,15 @@ class I18nToHtmlVisitor implements i18n.Visitor { private _contextStack: {msg: i18n.Message, mapper: (name: string) => string}[] = []; private _errors: I18nError[] = []; private _mapper: (name: string) => string; - private _warnings: I18nWarning[] = []; constructor( private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, private _digest: (m: i18n.Message) => string, private _mapperFactory: (m: i18n.Message) => PlaceholderMapper, - private _missingTranslationStrategy: MissingTranslationStrategy) {} + private _missingTranslationStrategy: MissingTranslationStrategy, private _console?: Console) { + } - convert(srcMsg: i18n.Message): - {nodes: html.Node[], errors: I18nError[], warnings: I18nWarning[]} { + convert(srcMsg: i18n.Message): {nodes: html.Node[], errors: I18nError[]} { this._contextStack.length = 0; this._errors.length = 0; @@ -89,7 +85,6 @@ class I18nToHtmlVisitor implements i18n.Visitor { return { nodes: html.rootNodes, errors: [...this._errors, ...html.errors], - warnings: this._warnings }; } @@ -121,13 +116,30 @@ class I18nToHtmlVisitor implements i18n.Visitor { return this._convertToText(this._srcMsg.placeholderToMessage[phName]); } - this._addError(ph, `Unknown placeholder`); + this._addError(ph, `Unknown placeholder "${ph.name}"`); return ''; } - visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): any { throw 'unreachable code'; } + // Loaded message contains only placeholders (vs tag and icu placeholders). + // However when a translation can not be found, we need to serialize the source message + // which can contain tag placeholders + visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): string { + const tag = `${ph.tag}`; + const attrs = Object.keys(ph.attrs).map(name => `${name}="${ph.attrs[name]}"`).join(' '); + if (ph.isVoid) { + return `<${tag} ${attrs}/>`; + } + const children = ph.children.map((c: i18n.Node) => c.visit(this)).join(''); + return `<${tag} ${attrs}>${children}`; + } - visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { throw 'unreachable code'; } + // Loaded message contains only placeholders (vs tag and icu placeholders). + // However when a translation can not be found, we need to serialize the source message + // which can contain tag placeholders + visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): string { + // An ICU placeholder references the source message to be serialized + return this._convertToText(this._srcMsg.placeholderToMessage[ph.name]); + } /** * Convert a source message to a translated text string: @@ -136,38 +148,41 @@ class I18nToHtmlVisitor implements i18n.Visitor { * - ICU nodes are converted to ICU expressions. */ private _convertToText(srcMsg: i18n.Message): string { - const digest = this._digest(srcMsg); + const id = this._digest(srcMsg); const mapper = this._mapperFactory ? this._mapperFactory(srcMsg) : null; + let nodes: i18n.Node[]; - if (this._i18nNodesByMsgId.hasOwnProperty(digest)) { - this._contextStack.push({msg: this._srcMsg, mapper: this._mapper}); - this._srcMsg = srcMsg; + this._contextStack.push({msg: this._srcMsg, mapper: this._mapper}); + this._srcMsg = srcMsg; + + if (this._i18nNodesByMsgId.hasOwnProperty(id)) { + // When there is a translation use its nodes as the source + // And create a mapper to convert serialized placeholder names to internal names + nodes = this._i18nNodesByMsgId[id]; this._mapper = (name: string) => mapper ? mapper.toInternalName(name) : name; - - const nodes = this._i18nNodesByMsgId[digest]; - const text = nodes.map(node => node.visit(this)).join(''); - const context = this._contextStack.pop(); - this._srcMsg = context.msg; - this._mapper = context.mapper; - return text; + } else { + // When no translation has been found + // - report an error / a warning / nothing, + // - use the nodes from the original message + // - placeholders are already internal and need no mapper + if (this._missingTranslationStrategy === MissingTranslationStrategy.Error) { + this._addError(srcMsg.nodes[0], `Missing translation for message "${id}"`); + } else if ( + this._console && + this._missingTranslationStrategy === MissingTranslationStrategy.Warning) { + this._console.warn(`Missing translation for message "${id}"`); + } + nodes = srcMsg.nodes; + this._mapper = (name: string) => name; } - - // No valid translation found - if (this._missingTranslationStrategy === MissingTranslationStrategy.Error) { - this._addError(srcMsg.nodes[0], `Missing translation for message ${digest}`); - } else if (this._missingTranslationStrategy === MissingTranslationStrategy.Warning) { - this._addWarning(srcMsg.nodes[0], `Missing translation for message ${digest}`); - } - - // In an case, Warning, Error or Ignore, return the srcMsg without translation - return serializeNodes(srcMsg.nodes).join(''); + const text = nodes.map(node => node.visit(this)).join(''); + const context = this._contextStack.pop(); + this._srcMsg = context.msg; + this._mapper = context.mapper; + return text; } private _addError(el: i18n.Node, msg: string) { this._errors.push(new I18nError(el.sourceSpan, msg)); } - - private _addWarning(el: i18n.Node, msg: string) { - this._warnings.push(new I18nWarning(el.sourceSpan, msg)); - } } diff --git a/modules/@angular/compiler/src/jit/compiler_factory.ts b/modules/@angular/compiler/src/jit/compiler_factory.ts index 74d5be6184..0da4a2ff10 100644 --- a/modules/@angular/compiler/src/jit/compiler_factory.ts +++ b/modules/@angular/compiler/src/jit/compiler_factory.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, InjectionToken, MISSING_TRANSLATION_STRATEGY, MissingTranslationStrategy, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; +import {COMPILER_OPTIONS, Compiler, CompilerFactory, CompilerOptions, Inject, InjectionToken, MissingTranslationStrategy, Optional, PLATFORM_INITIALIZER, PlatformRef, Provider, ReflectiveInjector, TRANSLATIONS, TRANSLATIONS_FORMAT, Type, ViewEncapsulation, createPlatformFactory, isDevMode, platformCore} from '@angular/core'; import {AnimationParser} from '../animation/animation_parser'; import {CompilerConfig} from '../config'; @@ -60,15 +60,16 @@ export const COMPILER_PROVIDERS: Array|{[k: string]: any}|any[]> = }, { provide: i18n.I18NHtmlParser, - useFactory: - (parser: HtmlParser, translations: string, format: string, - missingTranslationStrategy: MissingTranslationStrategy) => - new i18n.I18NHtmlParser(parser, translations, format, missingTranslationStrategy), + useFactory: (parser: HtmlParser, translations: string, format: string, config: CompilerConfig, + console: Console) => + new i18n.I18NHtmlParser( + parser, translations, format, config.missingTranslation, console), deps: [ baseHtmlParser, [new Optional(), new Inject(TRANSLATIONS)], [new Optional(), new Inject(TRANSLATIONS_FORMAT)], - [new Optional(), new Inject(MISSING_TRANSLATION_STRATEGY)], + [CompilerConfig], + [Console], ] }, { @@ -92,7 +93,7 @@ export const COMPILER_PROVIDERS: Array|{[k: string]: any}|any[]> = DirectiveResolver, PipeResolver, NgModuleResolver, - AnimationParser + AnimationParser, ]; @@ -103,11 +104,12 @@ export class JitCompilerFactory implements CompilerFactory { this._defaultOptions = [{ useDebug: isDevMode(), useJit: true, - defaultEncapsulation: ViewEncapsulation.Emulated + defaultEncapsulation: ViewEncapsulation.Emulated, + missingTranslation: MissingTranslationStrategy.Warning, }].concat(defaultOptions); } createCompiler(options: CompilerOptions[] = []): Compiler { - const mergedOptions = _mergeOptions(this._defaultOptions.concat(options)); + const opts = _mergeOptions(this._defaultOptions.concat(options)); const injector = ReflectiveInjector.resolveAndCreate([ COMPILER_PROVIDERS, { provide: CompilerConfig, @@ -115,19 +117,20 @@ export class JitCompilerFactory implements CompilerFactory { return new CompilerConfig({ // let explicit values from the compiler options overwrite options // from the app providers. E.g. important for the testing platform. - genDebugInfo: mergedOptions.useDebug, + genDebugInfo: opts.useDebug, // let explicit values from the compiler options overwrite options // from the app providers - useJit: mergedOptions.useJit, + useJit: opts.useJit, // let explicit values from the compiler options overwrite options // from the app providers - defaultEncapsulation: mergedOptions.defaultEncapsulation, - logBindingUpdate: mergedOptions.useDebug + defaultEncapsulation: opts.defaultEncapsulation, + logBindingUpdate: opts.useDebug, + missingTranslation: opts.missingTranslation, }); }, deps: [] }, - mergedOptions.providers + opts.providers ]); return injector.get(Compiler); } @@ -153,7 +156,8 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions { useDebug: _lastDefined(optionsArr.map(options => options.useDebug)), useJit: _lastDefined(optionsArr.map(options => options.useJit)), defaultEncapsulation: _lastDefined(optionsArr.map(options => options.defaultEncapsulation)), - providers: _mergeArrays(optionsArr.map(options => options.providers)) + providers: _mergeArrays(optionsArr.map(options => options.providers)), + missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)), }; } diff --git a/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts b/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts index 0b785645b1..e7f607c3d4 100644 --- a/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts +++ b/modules/@angular/compiler/test/i18n/extractor_merger_spec.ts @@ -488,7 +488,7 @@ function fakeTranslate( i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null)]; }); - const translations = new TranslationBundle(i18nMsgMap, digest, null); + const translations = new TranslationBundle(i18nMsgMap, digest); const output = mergeTranslations( htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs); diff --git a/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts b/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts index 68b17d3ddc..8bfae120b1 100644 --- a/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts +++ b/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts @@ -12,6 +12,7 @@ import * as i18n from '../../src/i18n/i18n_ast'; import {TranslationBundle} from '../../src/i18n/translation_bundle'; import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util'; import {serializeNodes} from '../ml_parser/ast_serializer_spec'; +import {_extractMessages} from './i18n_parser_spec'; export function main(): void { describe('TranslationBundle', () => { @@ -22,7 +23,7 @@ export function main(): void { it('should translate a plain message', () => { const msgMap = {foo: [new i18n.Text('bar', null)]}; - const tb = new TranslationBundle(msgMap, (_) => 'foo', null); + const tb = new TranslationBundle(msgMap, (_) => 'foo'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); expect(serializeNodes(tb.get(msg))).toEqual(['bar']); }); @@ -37,7 +38,7 @@ export function main(): void { const phMap = { ph1: '*phContent*', }; - const tb = new TranslationBundle(msgMap, (_) => 'foo', null); + const tb = new TranslationBundle(msgMap, (_) => 'foo'); const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i'); expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']); }); @@ -57,12 +58,29 @@ export function main(): void { const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i'); let count = 0; const digest = (_: any) => count++ ? 'ref' : 'foo'; - const tb = new TranslationBundle(msgMap, digest, null); + const tb = new TranslationBundle(msgMap, digest); expect(serializeNodes(tb.get(msg))).toEqual(['--*refMsg*++']); }); - describe('errors', () => { + it('should use the original message or throw when a translation is not found', () => { + const src = + `some text{{ some_expression }}{count, plural, =0 {no} few {a few}}`; + const messages = _extractMessages(`
${src}
`); + + const digest = (_: any) => `no matching id`; + // Empty message map -> use source messages in Ignore mode + let tb = new TranslationBundle({}, digest, null, MissingTranslationStrategy.Ignore); + expect(serializeNodes(tb.get(messages[0])).join('')).toEqual(src); + // Empty message map -> use source messages in Warning mode + tb = new TranslationBundle({}, digest, null, MissingTranslationStrategy.Warning); + expect(serializeNodes(tb.get(messages[0])).join('')).toEqual(src); + // Empty message map -> throw in Error mode + tb = new TranslationBundle({}, digest, null, MissingTranslationStrategy.Error); + expect(() => serializeNodes(tb.get(messages[0])).join('')).toThrow(); + }); + + describe('errors reporting', () => { it('should report unknown placeholders', () => { const msgMap = { foo: [ @@ -70,34 +88,35 @@ export function main(): void { new i18n.Placeholder('', 'ph1', span), ] }; - const tb = new TranslationBundle(msgMap, (_) => 'foo', null); + const tb = new TranslationBundle(msgMap, (_) => 'foo'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/); }); it('should report missing translation', () => { - const tb = new TranslationBundle( - {}, (_) => 'foo', null, MissingTranslationStrategy.Error); + const tb = new TranslationBundle({}, (_) => 'foo', null, MissingTranslationStrategy.Error); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); - expect(() => tb.get(msg)).toThrowError(/Missing translation for message foo/); + expect(() => tb.get(msg)).toThrowError(/Missing translation for message "foo"/); }); it('should report missing translation with MissingTranslationStrategy.Warning', () => { + const log: string[] = []; + const console = { + log: (msg: string) => { throw `unexpected`; }, + warn: (msg: string) => log.push(msg), + }; + const tb = new TranslationBundle( - {}, (_) => 'foo', null, MissingTranslationStrategy.Warning); + {}, (_) => 'foo', null, MissingTranslationStrategy.Warning, console); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); - const warn = console.warn; - const consoleWarnSpy = spyOn(console, 'warn').and.callThrough(); expect(() => tb.get(msg)).not.toThrowError(); - expect(consoleWarnSpy.calls.mostRecent().args[0]) - .toMatch(/Missing translation for message foo/); - console.warn = warn; + expect(log.length).toEqual(1); + expect(log[0]).toMatch(/Missing translation for message "foo"/); }); it('should not report missing translation with MissingTranslationStrategy.Ignore', () => { - const tb = new TranslationBundle( - {}, (_) => 'foo', null, MissingTranslationStrategy.Ignore); + const tb = new TranslationBundle({}, (_) => 'foo', null, MissingTranslationStrategy.Ignore); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); expect(() => tb.get(msg)).not.toThrowError(); }); @@ -110,9 +129,8 @@ export function main(): void { const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i'); let count = 0; const digest = (_: any) => count++ ? 'ref' : 'foo'; - const tb = - new TranslationBundle(msgMap, digest, null, MissingTranslationStrategy.Error); - expect(() => tb.get(msg)).toThrowError(/Missing translation for message ref/); + const tb = new TranslationBundle(msgMap, digest, null, MissingTranslationStrategy.Error); + expect(() => tb.get(msg)).toThrowError(/Missing translation for message "ref"/); }); it('should report invalid translated html', () => { @@ -125,7 +143,7 @@ export function main(): void { const phMap = { ph1: '', }; - const tb = new TranslationBundle(msgMap, (_) => 'foo', null); + const tb = new TranslationBundle(msgMap, (_) => 'foo'); const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i'); expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/); }); diff --git a/modules/@angular/core/src/core.ts b/modules/@angular/core/src/core.ts index b62f94d89b..0d0105cf58 100644 --- a/modules/@angular/core/src/core.ts +++ b/modules/@angular/core/src/core.ts @@ -25,7 +25,7 @@ export {DebugElement, DebugNode, asNativeElements, getDebugNode} from './debug/d export {GetTestability, Testability, TestabilityRegistry, setTestabilityGetter} from './testability/testability'; export * from './change_detection'; export * from './platform_core_providers'; -export {TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, MISSING_TRANSLATION_STRATEGY, MissingTranslationStrategy} from './i18n/tokens'; +export {TRANSLATIONS, TRANSLATIONS_FORMAT, LOCALE_ID, MissingTranslationStrategy} from './i18n/tokens'; export {ApplicationModule} from './application_module'; export {wtfCreateScope, wtfLeave, wtfStartTimeRange, wtfEndTimeRange, WtfScopeFn} from './profile/profile'; export {Type} from './type'; diff --git a/modules/@angular/core/src/i18n/tokens.ts b/modules/@angular/core/src/i18n/tokens.ts index 3733ae5908..0dd471bd5c 100644 --- a/modules/@angular/core/src/i18n/tokens.ts +++ b/modules/@angular/core/src/i18n/tokens.ts @@ -23,11 +23,6 @@ export const TRANSLATIONS = new InjectionToken('Translations'); */ export const TRANSLATIONS_FORMAT = new InjectionToken('TranslationsFormat'); -/** - * @experimental i18n support is experimental. - */ -export const MISSING_TRANSLATION_STRATEGY = new InjectionToken('MissingTranslationStrategy'); - /** * @experimental i18n support is experimental. */ diff --git a/modules/@angular/core/src/linker/compiler.ts b/modules/@angular/core/src/linker/compiler.ts index c210078535..80c8e398d8 100644 --- a/modules/@angular/core/src/linker/compiler.ts +++ b/modules/@angular/core/src/linker/compiler.ts @@ -9,6 +9,7 @@ import {Injectable, InjectionToken} from '../di'; import {BaseError} from '../facade/errors'; import {stringify} from '../facade/lang'; +import {MissingTranslationStrategy} from '../i18n/tokens'; import {ViewEncapsulation} from '../metadata'; import {Type} from '../type'; @@ -112,6 +113,7 @@ export type CompilerOptions = { useJit?: boolean, defaultEncapsulation?: ViewEncapsulation, providers?: any[], + missingTranslation?: MissingTranslationStrategy, }; /** diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 15e842cb61..045dddcaac 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -243,6 +243,7 @@ export declare const COMPILER_OPTIONS: InjectionToken<{ useJit?: boolean; defaultEncapsulation?: ViewEncapsulation; providers?: any[]; + missingTranslation?: MissingTranslationStrategy; }[]>; /** @experimental */ @@ -256,6 +257,7 @@ export declare type CompilerOptions = { useJit?: boolean; defaultEncapsulation?: ViewEncapsulation; providers?: any[]; + missingTranslation?: MissingTranslationStrategy; }; /** @stable */ @@ -617,9 +619,6 @@ export declare class KeyValueDiffers { /** @experimental */ export declare const LOCALE_ID: InjectionToken; -/** @experimental */ -export declare const MISSING_TRANSLATION_STRATEGY: OpaqueToken; - /** @experimental */ export declare enum MissingTranslationStrategy { Error = 0,