fix(compiler): fix missing translations handling (#14113)
PR Close #14113
This commit is contained in:
parent
8775ab9495
commit
827c3fe199
@ -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(
|
||||||
|
@ -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 {
|
||||||
|
@ -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, [], {});
|
||||||
}
|
}
|
||||||
|
@ -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); }
|
|
||||||
}
|
|
||||||
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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"/);
|
||||||
});
|
});
|
||||||
|
@ -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';
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
5
tools/public_api_guard/core/index.d.ts
vendored
5
tools/public_api_guard/core/index.d.ts
vendored
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user