fix(compiler): fix missing translations handling (#14113)

PR Close #14113
This commit is contained in:
Victor Berchet 2017-01-25 23:26:49 -08:00 committed by Miško Hevery
parent 8775ab9495
commit 827c3fe199
12 changed files with 139 additions and 107 deletions

View File

@ -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 {ViewEncapsulation} from '@angular/core'; import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core';
import {AnimationParser} from '../animation/animation_parser'; import {AnimationParser} from '../animation/animation_parser';
import {CompilerConfig} from '../config'; import {CompilerConfig} from '../config';
@ -53,7 +53,10 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver); const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
const staticReflector = new StaticReflector(symbolResolver); const staticReflector = new StaticReflector(symbolResolver);
StaticAndDynamicReflectionCapabilities.install(staticReflector); 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({ const config = new CompilerConfig({
genDebugInfo: options.debug === true, genDebugInfo: options.debug === true,
defaultEncapsulation: ViewEncapsulation.Emulated, defaultEncapsulation: ViewEncapsulation.Emulated,
@ -64,7 +67,6 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config); {get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);
const expressionParser = new Parser(new Lexer()); const expressionParser = new Parser(new Lexer());
const elementSchemaRegistry = new DomElementSchemaRegistry(); const elementSchemaRegistry = new DomElementSchemaRegistry();
const console = new Console();
const tmplParser = const tmplParser =
new TemplateParser(expressionParser, elementSchemaRegistry, htmlParser, console, []); new TemplateParser(expressionParser, elementSchemaRegistry, htmlParser, console, []);
const resolver = new CompileMetadataResolver( const resolver = new CompileMetadataResolver(

View File

@ -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 {ViewEncapsulation, isDevMode} from '@angular/core'; import {MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core';
import {CompileIdentifierMetadata} from './compile_metadata'; import {CompileIdentifierMetadata} from './compile_metadata';
import {Identifiers, createIdentifier} from './identifiers'; import {Identifiers, createIdentifier} from './identifiers';
@ -21,21 +21,24 @@ export class CompilerConfig {
private _genDebugInfo: boolean; private _genDebugInfo: boolean;
private _logBindingUpdate: boolean; private _logBindingUpdate: boolean;
public useJit: boolean; public useJit: boolean;
public missingTranslation: MissingTranslationStrategy;
constructor( constructor(
{renderTypes = new DefaultRenderTypes(), defaultEncapsulation = ViewEncapsulation.Emulated, {renderTypes = new DefaultRenderTypes(), defaultEncapsulation = ViewEncapsulation.Emulated,
genDebugInfo, logBindingUpdate, useJit = true}: { genDebugInfo, logBindingUpdate, useJit = true, missingTranslation}: {
renderTypes?: RenderTypes, renderTypes?: RenderTypes,
defaultEncapsulation?: ViewEncapsulation, defaultEncapsulation?: ViewEncapsulation,
genDebugInfo?: boolean, genDebugInfo?: boolean,
logBindingUpdate?: boolean, logBindingUpdate?: boolean,
useJit?: boolean useJit?: boolean,
missingTranslation?: MissingTranslationStrategy,
} = {}) { } = {}) {
this.renderTypes = renderTypes; this.renderTypes = renderTypes;
this.defaultEncapsulation = defaultEncapsulation; this.defaultEncapsulation = defaultEncapsulation;
this._genDebugInfo = genDebugInfo; this._genDebugInfo = genDebugInfo;
this._logBindingUpdate = logBindingUpdate; this._logBindingUpdate = logBindingUpdate;
this.useJit = useJit; this.useJit = useJit;
this.missingTranslation = missingTranslation;
} }
get genDebugInfo(): boolean { get genDebugInfo(): boolean {

View File

@ -11,6 +11,7 @@ import {MissingTranslationStrategy} from '@angular/core';
import {HtmlParser} from '../ml_parser/html_parser'; import {HtmlParser} from '../ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseTreeResult} from '../ml_parser/parser'; import {ParseTreeResult} from '../ml_parser/parser';
import {Console} from '../private_import_core';
import {mergeTranslations} from './extractor_merger'; import {mergeTranslations} from './extractor_merger';
import {Serializer} from './serializers/serializer'; import {Serializer} from './serializers/serializer';
@ -23,14 +24,11 @@ export class I18NHtmlParser implements HtmlParser {
// @override // @override
getTagDefinition: any; 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( constructor(
private _htmlParser: HtmlParser, private _translations?: string, private _htmlParser: HtmlParser, private _translations?: string,
private _translationsFormat?: string, private _translationsFormat?: string,
private _missingTranslationStrategy: private _missingTranslation: MissingTranslationStrategy = MissingTranslationStrategy.Warning,
MissingTranslationStrategy = MissingTranslationStrategy.Error) {} private _console?: Console) {}
parse( parse(
source: string, url: string, parseExpansionForms: boolean = false, source: string, url: string, parseExpansionForms: boolean = false,
@ -51,7 +49,7 @@ export class I18NHtmlParser implements HtmlParser {
const serializer = this._createSerializer(); const serializer = this._createSerializer();
const translationBundle = TranslationBundle.load( const translationBundle = TranslationBundle.load(
this._translations, url, serializer, this._missingTranslationStrategy); this._translations, url, serializer, this._missingTranslation, this._console);
return mergeTranslations(parseResult.rootNodes, translationBundle, interpolationConfig, [], {}); return mergeTranslations(parseResult.rootNodes, translationBundle, interpolationConfig, [], {});
} }

View File

@ -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 {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util'; import {ParseError, ParseSourceSpan} from '../parse_util';
/** /**
* An i18n error. * An i18n error.
@ -14,7 +14,3 @@ import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
export class I18nError extends ParseError { export class I18nError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); } constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
} }
export class I18nWarning extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg, ParseErrorLevel.WARNING); }
}

View File

@ -8,13 +8,12 @@
import {MissingTranslationStrategy} from '@angular/core'; import {MissingTranslationStrategy} from '@angular/core';
import {warn} from '../facade/lang';
import * as html from '../ml_parser/ast'; import * as html from '../ml_parser/ast';
import {HtmlParser} from '../ml_parser/html_parser'; import {HtmlParser} from '../ml_parser/html_parser';
import {Console} from '../private_import_core';
import {serializeNodes} from './digest';
import * as i18n from './i18n_ast'; import * as i18n from './i18n_ast';
import {I18nError, I18nWarning} from './parse_util'; import {I18nError} from './parse_util';
import {PlaceholderMapper, Serializer} from './serializers/serializer'; import {PlaceholderMapper, Serializer} from './serializers/serializer';
/** /**
@ -27,30 +26,28 @@ export class TranslationBundle {
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {},
public digest: (m: i18n.Message) => string, public digest: (m: i18n.Message) => string,
public mapperFactory?: (m: i18n.Message) => PlaceholderMapper, public mapperFactory?: (m: i18n.Message) => PlaceholderMapper,
missingTranslationStrategy: MissingTranslationStrategy = MissingTranslationStrategy.Warning) { missingTranslationStrategy: MissingTranslationStrategy = MissingTranslationStrategy.Warning,
this._i18nToHtml = console?: Console) {
new I18nToHtmlVisitor(_i18nNodesByMsgId, digest, mapperFactory, missingTranslationStrategy); this._i18nToHtml = new I18nToHtmlVisitor(
_i18nNodesByMsgId, digest, mapperFactory, missingTranslationStrategy, console);
} }
// Creates a `TranslationBundle` by parsing the given `content` with the `serializer`. // Creates a `TranslationBundle` by parsing the given `content` with the `serializer`.
static load( static load(
content: string, url: string, serializer: Serializer, content: string, url: string, serializer: Serializer,
missingTranslationStrategy: MissingTranslationStrategy): TranslationBundle { missingTranslationStrategy: MissingTranslationStrategy,
console?: Console): TranslationBundle {
const i18nNodesByMsgId = serializer.load(content, url); const i18nNodesByMsgId = serializer.load(content, url);
const digestFn = (m: i18n.Message) => serializer.digest(m); const digestFn = (m: i18n.Message) => serializer.digest(m);
const mapperFactory = (m: i18n.Message) => serializer.createNameMapper(m); const mapperFactory = (m: i18n.Message) => serializer.createNameMapper(m);
return new TranslationBundle( return new TranslationBundle(
i18nNodesByMsgId, digestFn, mapperFactory, missingTranslationStrategy); i18nNodesByMsgId, digestFn, mapperFactory, missingTranslationStrategy, console);
} }
// Returns the translation as HTML nodes from the given source message. // Returns the translation as HTML nodes from the given source message.
get(srcMsg: i18n.Message): html.Node[] { get(srcMsg: i18n.Message): html.Node[] {
const html = this._i18nToHtml.convert(srcMsg); const html = this._i18nToHtml.convert(srcMsg);
if (html.warnings.length) {
warn(html.warnings.join('\n'));
}
if (html.errors.length) { if (html.errors.length) {
throw new Error(html.errors.join('\n')); 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 _contextStack: {msg: i18n.Message, mapper: (name: string) => string}[] = [];
private _errors: I18nError[] = []; private _errors: I18nError[] = [];
private _mapper: (name: string) => string; private _mapper: (name: string) => string;
private _warnings: I18nWarning[] = [];
constructor( constructor(
private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {},
private _digest: (m: i18n.Message) => string, private _digest: (m: i18n.Message) => string,
private _mapperFactory: (m: i18n.Message) => PlaceholderMapper, private _mapperFactory: (m: i18n.Message) => PlaceholderMapper,
private _missingTranslationStrategy: MissingTranslationStrategy) {} private _missingTranslationStrategy: MissingTranslationStrategy, private _console?: Console) {
}
convert(srcMsg: i18n.Message): convert(srcMsg: i18n.Message): {nodes: html.Node[], errors: I18nError[]} {
{nodes: html.Node[], errors: I18nError[], warnings: I18nWarning[]} {
this._contextStack.length = 0; this._contextStack.length = 0;
this._errors.length = 0; this._errors.length = 0;
@ -89,7 +85,6 @@ class I18nToHtmlVisitor implements i18n.Visitor {
return { return {
nodes: html.rootNodes, nodes: html.rootNodes,
errors: [...this._errors, ...html.errors], errors: [...this._errors, ...html.errors],
warnings: this._warnings
}; };
} }
@ -121,13 +116,30 @@ class I18nToHtmlVisitor implements i18n.Visitor {
return this._convertToText(this._srcMsg.placeholderToMessage[phName]); return this._convertToText(this._srcMsg.placeholderToMessage[phName]);
} }
this._addError(ph, `Unknown placeholder`); this._addError(ph, `Unknown placeholder "${ph.name}"`);
return ''; 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}</${tag}>`;
}
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: * Convert a source message to a translated text string:
@ -136,15 +148,33 @@ class I18nToHtmlVisitor implements i18n.Visitor {
* - ICU nodes are converted to ICU expressions. * - ICU nodes are converted to ICU expressions.
*/ */
private _convertToText(srcMsg: i18n.Message): string { private _convertToText(srcMsg: i18n.Message): string {
const digest = this._digest(srcMsg); const id = this._digest(srcMsg);
const mapper = this._mapperFactory ? this._mapperFactory(srcMsg) : null; 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._contextStack.push({msg: this._srcMsg, mapper: this._mapper});
this._srcMsg = srcMsg; this._srcMsg = srcMsg;
this._mapper = (name: string) => mapper ? mapper.toInternalName(name) : name;
const nodes = this._i18nNodesByMsgId[digest]; 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;
} 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;
}
const text = nodes.map(node => node.visit(this)).join(''); const text = nodes.map(node => node.visit(this)).join('');
const context = this._contextStack.pop(); const context = this._contextStack.pop();
this._srcMsg = context.msg; this._srcMsg = context.msg;
@ -152,22 +182,7 @@ class I18nToHtmlVisitor implements i18n.Visitor {
return text; return text;
} }
// 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('');
}
private _addError(el: i18n.Node, msg: string) { private _addError(el: i18n.Node, msg: string) {
this._errors.push(new I18nError(el.sourceSpan, msg)); this._errors.push(new I18nError(el.sourceSpan, msg));
} }
private _addWarning(el: i18n.Node, msg: string) {
this._warnings.push(new I18nWarning(el.sourceSpan, msg));
}
} }

View File

@ -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 {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 {AnimationParser} from '../animation/animation_parser';
import {CompilerConfig} from '../config'; import {CompilerConfig} from '../config';
@ -60,15 +60,16 @@ export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> =
}, },
{ {
provide: i18n.I18NHtmlParser, provide: i18n.I18NHtmlParser,
useFactory: useFactory: (parser: HtmlParser, translations: string, format: string, config: CompilerConfig,
(parser: HtmlParser, translations: string, format: string, console: Console) =>
missingTranslationStrategy: MissingTranslationStrategy) => new i18n.I18NHtmlParser(
new i18n.I18NHtmlParser(parser, translations, format, missingTranslationStrategy), parser, translations, format, config.missingTranslation, console),
deps: [ deps: [
baseHtmlParser, baseHtmlParser,
[new Optional(), new Inject(TRANSLATIONS)], [new Optional(), new Inject(TRANSLATIONS)],
[new Optional(), new Inject(TRANSLATIONS_FORMAT)], [new Optional(), new Inject(TRANSLATIONS_FORMAT)],
[new Optional(), new Inject(MISSING_TRANSLATION_STRATEGY)], [CompilerConfig],
[Console],
] ]
}, },
{ {
@ -92,7 +93,7 @@ export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> =
DirectiveResolver, DirectiveResolver,
PipeResolver, PipeResolver,
NgModuleResolver, NgModuleResolver,
AnimationParser AnimationParser,
]; ];
@ -103,11 +104,12 @@ export class JitCompilerFactory implements CompilerFactory {
this._defaultOptions = [<CompilerOptions>{ this._defaultOptions = [<CompilerOptions>{
useDebug: isDevMode(), useDebug: isDevMode(),
useJit: true, useJit: true,
defaultEncapsulation: ViewEncapsulation.Emulated defaultEncapsulation: ViewEncapsulation.Emulated,
missingTranslation: MissingTranslationStrategy.Warning,
}].concat(defaultOptions); }].concat(defaultOptions);
} }
createCompiler(options: CompilerOptions[] = []): Compiler { createCompiler(options: CompilerOptions[] = []): Compiler {
const mergedOptions = _mergeOptions(this._defaultOptions.concat(options)); const opts = _mergeOptions(this._defaultOptions.concat(options));
const injector = ReflectiveInjector.resolveAndCreate([ const injector = ReflectiveInjector.resolveAndCreate([
COMPILER_PROVIDERS, { COMPILER_PROVIDERS, {
provide: CompilerConfig, provide: CompilerConfig,
@ -115,19 +117,20 @@ export class JitCompilerFactory implements CompilerFactory {
return new CompilerConfig({ return new CompilerConfig({
// let explicit values from the compiler options overwrite options // let explicit values from the compiler options overwrite options
// from the app providers. E.g. important for the testing platform. // 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 // let explicit values from the compiler options overwrite options
// from the app providers // from the app providers
useJit: mergedOptions.useJit, useJit: opts.useJit,
// let explicit values from the compiler options overwrite options // let explicit values from the compiler options overwrite options
// from the app providers // from the app providers
defaultEncapsulation: mergedOptions.defaultEncapsulation, defaultEncapsulation: opts.defaultEncapsulation,
logBindingUpdate: mergedOptions.useDebug logBindingUpdate: opts.useDebug,
missingTranslation: opts.missingTranslation,
}); });
}, },
deps: [] deps: []
}, },
mergedOptions.providers opts.providers
]); ]);
return injector.get(Compiler); return injector.get(Compiler);
} }
@ -153,7 +156,8 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions {
useDebug: _lastDefined(optionsArr.map(options => options.useDebug)), useDebug: _lastDefined(optionsArr.map(options => options.useDebug)),
useJit: _lastDefined(optionsArr.map(options => options.useJit)), useJit: _lastDefined(optionsArr.map(options => options.useJit)),
defaultEncapsulation: _lastDefined(optionsArr.map(options => options.defaultEncapsulation)), 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)),
}; };
} }

View File

@ -488,7 +488,7 @@ function fakeTranslate(
i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null)]; i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null)];
}); });
const translations = new TranslationBundle(i18nMsgMap, digest, null); const translations = new TranslationBundle(i18nMsgMap, digest);
const output = mergeTranslations( const output = mergeTranslations(
htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs); htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);

View File

@ -12,6 +12,7 @@ import * as i18n from '../../src/i18n/i18n_ast';
import {TranslationBundle} from '../../src/i18n/translation_bundle'; import {TranslationBundle} from '../../src/i18n/translation_bundle';
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util'; import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
import {serializeNodes} from '../ml_parser/ast_serializer_spec'; import {serializeNodes} from '../ml_parser/ast_serializer_spec';
import {_extractMessages} from './i18n_parser_spec';
export function main(): void { export function main(): void {
describe('TranslationBundle', () => { describe('TranslationBundle', () => {
@ -22,7 +23,7 @@ export function main(): void {
it('should translate a plain message', () => { it('should translate a plain message', () => {
const msgMap = {foo: [new i18n.Text('bar', null)]}; 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'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar']); expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
}); });
@ -37,7 +38,7 @@ export function main(): void {
const phMap = { const phMap = {
ph1: '*phContent*', 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'); const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i');
expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']); 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'); const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i');
let count = 0; let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo'; 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*++']); 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-tag>some text{{ some_expression }}</some-tag>{count, plural, =0 {no} few {a <b>few</b>}}`;
const messages = _extractMessages(`<div i18n>${src}</div>`);
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', () => { it('should report unknown placeholders', () => {
const msgMap = { const msgMap = {
foo: [ foo: [
@ -70,34 +88,35 @@ export function main(): void {
new i18n.Placeholder('', 'ph1', span), 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'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/); expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/);
}); });
it('should report missing translation', () => { it('should report missing translation', () => {
const tb = new TranslationBundle( const tb = new TranslationBundle({}, (_) => 'foo', null, MissingTranslationStrategy.Error);
{}, (_) => 'foo', null, MissingTranslationStrategy.Error);
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); 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', () => { 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( const tb = new TranslationBundle(
{}, (_) => 'foo', null, MissingTranslationStrategy.Warning); {}, (_) => 'foo', null, MissingTranslationStrategy.Warning, console);
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); 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(() => tb.get(msg)).not.toThrowError();
expect(consoleWarnSpy.calls.mostRecent().args[0]) expect(log.length).toEqual(1);
.toMatch(/Missing translation for message foo/); expect(log[0]).toMatch(/Missing translation for message "foo"/);
console.warn = warn;
}); });
it('should not report missing translation with MissingTranslationStrategy.Ignore', () => { it('should not report missing translation with MissingTranslationStrategy.Ignore', () => {
const tb = new TranslationBundle( const tb = new TranslationBundle({}, (_) => 'foo', null, MissingTranslationStrategy.Ignore);
{}, (_) => 'foo', null, MissingTranslationStrategy.Ignore);
const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i'); const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).not.toThrowError(); 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'); const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd', 'i');
let count = 0; let count = 0;
const digest = (_: any) => count++ ? 'ref' : 'foo'; const digest = (_: any) => count++ ? 'ref' : 'foo';
const tb = const tb = new TranslationBundle(msgMap, digest, null, MissingTranslationStrategy.Error);
new TranslationBundle(msgMap, digest, null, MissingTranslationStrategy.Error); expect(() => tb.get(msg)).toThrowError(/Missing translation for message "ref"/);
expect(() => tb.get(msg)).toThrowError(/Missing translation for message ref/);
}); });
it('should report invalid translated html', () => { it('should report invalid translated html', () => {
@ -125,7 +143,7 @@ export function main(): void {
const phMap = { const phMap = {
ph1: '</b>', ph1: '</b>',
}; };
const tb = new TranslationBundle(msgMap, (_) => 'foo', null); const tb = new TranslationBundle(msgMap, (_) => 'foo');
const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i'); const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd', 'i');
expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/); expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/);
}); });

View File

@ -25,7 +25,7 @@ export {DebugElement, DebugNode, asNativeElements, getDebugNode} from './debug/d
export {GetTestability, Testability, TestabilityRegistry, setTestabilityGetter} from './testability/testability'; export {GetTestability, Testability, TestabilityRegistry, setTestabilityGetter} from './testability/testability';
export * from './change_detection'; export * from './change_detection';
export * from './platform_core_providers'; 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 {ApplicationModule} from './application_module';
export {wtfCreateScope, wtfLeave, wtfStartTimeRange, wtfEndTimeRange, WtfScopeFn} from './profile/profile'; export {wtfCreateScope, wtfLeave, wtfStartTimeRange, wtfEndTimeRange, WtfScopeFn} from './profile/profile';
export {Type} from './type'; export {Type} from './type';

View File

@ -23,11 +23,6 @@ export const TRANSLATIONS = new InjectionToken<string>('Translations');
*/ */
export const TRANSLATIONS_FORMAT = new InjectionToken<string>('TranslationsFormat'); export const TRANSLATIONS_FORMAT = new InjectionToken<string>('TranslationsFormat');
/**
* @experimental i18n support is experimental.
*/
export const MISSING_TRANSLATION_STRATEGY = new InjectionToken('MissingTranslationStrategy');
/** /**
* @experimental i18n support is experimental. * @experimental i18n support is experimental.
*/ */

View File

@ -9,6 +9,7 @@
import {Injectable, InjectionToken} from '../di'; import {Injectable, InjectionToken} from '../di';
import {BaseError} from '../facade/errors'; import {BaseError} from '../facade/errors';
import {stringify} from '../facade/lang'; import {stringify} from '../facade/lang';
import {MissingTranslationStrategy} from '../i18n/tokens';
import {ViewEncapsulation} from '../metadata'; import {ViewEncapsulation} from '../metadata';
import {Type} from '../type'; import {Type} from '../type';
@ -112,6 +113,7 @@ export type CompilerOptions = {
useJit?: boolean, useJit?: boolean,
defaultEncapsulation?: ViewEncapsulation, defaultEncapsulation?: ViewEncapsulation,
providers?: any[], providers?: any[],
missingTranslation?: MissingTranslationStrategy,
}; };
/** /**

View File

@ -243,6 +243,7 @@ export declare const COMPILER_OPTIONS: InjectionToken<{
useJit?: boolean; useJit?: boolean;
defaultEncapsulation?: ViewEncapsulation; defaultEncapsulation?: ViewEncapsulation;
providers?: any[]; providers?: any[];
missingTranslation?: MissingTranslationStrategy;
}[]>; }[]>;
/** @experimental */ /** @experimental */
@ -256,6 +257,7 @@ export declare type CompilerOptions = {
useJit?: boolean; useJit?: boolean;
defaultEncapsulation?: ViewEncapsulation; defaultEncapsulation?: ViewEncapsulation;
providers?: any[]; providers?: any[];
missingTranslation?: MissingTranslationStrategy;
}; };
/** @stable */ /** @stable */
@ -617,9 +619,6 @@ export declare class KeyValueDiffers {
/** @experimental */ /** @experimental */
export declare const LOCALE_ID: InjectionToken<string>; export declare const LOCALE_ID: InjectionToken<string>;
/** @experimental */
export declare const MISSING_TRANSLATION_STRATEGY: OpaqueToken;
/** @experimental */ /** @experimental */
export declare enum MissingTranslationStrategy { export declare enum MissingTranslationStrategy {
Error = 0, Error = 0,