/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/compiler/src/i18n/translation_bundle.ts
225 строк
8 KB
Kristiyan Kostadinov
refactor(compiler): remove I18nError (#62160)
23 июн 2025, 15:25
23 июн 2025, 15:25
d1cb44d
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {MissingTranslationStrategy} from '../core'; import * as html from '../ml_parser/ast'; import {HtmlParser} from '../ml_parser/html_parser'; import {ParseError} from '../parse_util'; import {Console} from '../util'; import * as i18n from './i18n_ast'; import {PlaceholderMapper, Serializer} from './serializers/serializer'; import {escapeXml} from './serializers/xml_helper'; /** * A container for translated messages */ export class TranslationBundle { private _i18nToHtml: I18nToHtmlVisitor; constructor( private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, locale: string | null, public digest: (m: i18n.Message) => string, public mapperFactory?: (m: i18n.Message) => PlaceholderMapper, missingTranslationStrategy: MissingTranslationStrategy = MissingTranslationStrategy.Warning, console?: Console, ) { this._i18nToHtml = new I18nToHtmlVisitor( _i18nNodesByMsgId, locale, 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, console?: Console, ): TranslationBundle { const {locale, 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, locale, 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.errors.length) { throw new Error(html.errors.join('\n')); } return html.nodes; } has(srcMsg: i18n.Message): boolean { return this.digest(srcMsg) in this._i18nNodesByMsgId; } } class I18nToHtmlVisitor implements i18n.Visitor { // using non-null assertions because they're (re)set by convert() private _srcMsg!: i18n.Message; private _errors: ParseError[] = []; private _contextStack: {msg: i18n.Message; mapper: (name: string) => string}[] = []; private _mapper!: (name: string) => string; constructor( private _i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {}, private _locale: string | null, private _digest: (m: i18n.Message) => string, private _mapperFactory: (m: i18n.Message) => PlaceholderMapper, private _missingTranslationStrategy: MissingTranslationStrategy, private _console?: Console, ) {} convert(srcMsg: i18n.Message): {nodes: html.Node[]; errors: ParseError[]} { this._contextStack.length = 0; this._errors.length = 0; // i18n to text const text = this._convertToText(srcMsg); // text to html const url = srcMsg.nodes[0].sourceSpan.start.file.url; const html = new HtmlParser().parse(text, url, {tokenizeExpansionForms: true}); return { nodes: html.rootNodes, errors: [...this._errors, ...html.errors], }; } visitText(text: i18n.Text, context?: any): string { // `convert()` uses an `HtmlParser` to return `html.Node`s // we should then make sure that any special characters are escaped return escapeXml(text.value); } visitContainer(container: i18n.Container, context?: any): any { return container.children.map((n) => n.visit(this)).join(''); } visitIcu(icu: i18n.Icu, context?: any): any { const cases = Object.keys(icu.cases).map((k) => `${k} {${icu.cases[k].visit(this)}}`); // TODO(vicb): Once all format switch to using expression placeholders // we should throw when the placeholder is not in the source message const exp = this._srcMsg.placeholders.hasOwnProperty(icu.expression) ? this._srcMsg.placeholders[icu.expression].text : icu.expression; return `{${exp}, ${icu.type}, ${cases.join(' ')}}`; } visitPlaceholder(ph: i18n.Placeholder, context?: any): string { const phName = this._mapper(ph.name); if (this._srcMsg.placeholders.hasOwnProperty(phName)) { return this._srcMsg.placeholders[phName].text; } if (this._srcMsg.placeholderToMessage.hasOwnProperty(phName)) { return this._convertToText(this._srcMsg.placeholderToMessage[phName]); } this._addError(ph, `Unknown placeholder "${ph.name}"`); return ''; } // 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}>`; } // 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]); } visitBlockPlaceholder(ph: i18n.BlockPlaceholder, context?: any): string { const params = ph.parameters.length === 0 ? '' : ` (${ph.parameters.join('; ')})`; const children = ph.children.map((c: i18n.Node) => c.visit(this)).join(''); return `@${ph.name}${params} {${children}}`; } /** * Convert a source message to a translated text string: * - text nodes are replaced with their translation, * - placeholders are replaced with their content, * - ICU nodes are converted to ICU expressions. */ private _convertToText(srcMsg: i18n.Message): string { const id = this._digest(srcMsg); const mapper = this._mapperFactory ? this._mapperFactory(srcMsg) : null; let nodes: i18n.Node[]; 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); } 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) { const ctx = this._locale ? ` for locale "${this._locale}"` : ''; this._addError(srcMsg.nodes[0], `Missing translation for message "${id}"${ctx}`); } else if ( this._console && this._missingTranslationStrategy === MissingTranslationStrategy.Warning ) { const ctx = this._locale ? ` for locale "${this._locale}"` : ''; this._console.warn(`Missing translation for message "${id}"${ctx}`); } nodes = srcMsg.nodes; this._mapper = (name: string) => name; } 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 ParseError(el.sourceSpan, msg)); } }