202 lines
7.3 KiB
TypeScript
Raw Normal View History

2016-07-21 13:56:58 -07:00
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ml from '../../ml_parser/ast';
import {HtmlParser} from '../../ml_parser/html_parser';
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
import {XmlParser} from '../../ml_parser/xml_parser';
2016-07-21 13:56:58 -07:00
import {ParseError} from '../../parse_util';
import * as i18n from '../i18n_ast';
import {MessageBundle} from '../message_bundle';
2016-07-29 11:10:43 -07:00
import {I18nError} from '../parse_util';
2016-07-21 13:56:58 -07:00
import {Serializer} from './serializer';
import {digest} from './xmb';
2016-07-21 13:56:58 -07:00
const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation';
const _PLACEHOLDER_TAG = 'ph';
export class Xtb implements Serializer {
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
write(messages: i18n.Message[]): string { throw new Error('Unsupported'); }
2016-07-21 13:56:58 -07:00
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} {
2016-07-21 13:56:58 -07:00
// Parse the xtb file into xml nodes
2016-11-01 18:02:29 -07:00
const result = new XmlParser().parse(content, url, true);
2016-07-21 13:56:58 -07:00
if (result.errors.length) {
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`);
}
// Replace the placeholders, messages are now string
const {messages, errors} = new _Visitor(this).parse(result.rootNodes, messageBundle);
2016-07-21 13:56:58 -07:00
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
}
// Convert the string messages to html ast
// TODO(vicb): map error message back to the original message in xtb
const messageMap: {[id: string]: ml.Node[]} = {};
const parseErrors: ParseError[] = [];
2016-07-21 13:56:58 -07:00
Object.keys(messages).forEach((msgId) => {
const res = this._htmlParser.parse(messages[msgId], url, true, this._interpolationConfig);
2016-07-21 13:56:58 -07:00
parseErrors.push(...res.errors);
messageMap[msgId] = res.rootNodes;
2016-07-21 13:56:58 -07:00
});
if (parseErrors.length) {
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
}
return messageMap;
}
digest(message: i18n.Message): string {
// we must use the same digest as xmb
return digest(message);
}
2016-07-21 13:56:58 -07:00
}
2016-08-12 20:14:52 -07:00
class _Visitor implements ml.Visitor {
private _messageNodes: [string, ml.Node[]][];
private _translatedMessages: {[id: string]: string};
2016-07-21 13:56:58 -07:00
private _bundleDepth: number;
private _translationDepth: number;
private _errors: I18nError[];
private _sourceMessage: i18n.Message;
constructor(private _serializer: Serializer) {}
2016-07-21 13:56:58 -07:00
parse(nodes: ml.Node[], messageBundle: MessageBundle):
2016-07-21 13:56:58 -07:00
{messages: {[k: string]: string}, errors: I18nError[]} {
// Tuple [<message id>, [ml nodes]]
this._messageNodes = [];
this._translatedMessages = {};
2016-07-21 13:56:58 -07:00
this._bundleDepth = 0;
this._translationDepth = 0;
this._errors = [];
// load all translations
ml.visitAll(this, nodes, null);
2016-07-21 13:56:58 -07:00
const messageMap: {[msgId: string]: i18n.Message} = {};
messageBundle.getMessages().forEach(m => messageMap[this._serializer.digest(m)] = m);
this._messageNodes
.filter(message => {
// Remove any messages that is not present in the source message bundle.
return messageMap.hasOwnProperty(message[0]);
})
.sort((a, b) => {
// Because there could be no ICU placeholders inside an ICU message,
// we do not need to take into account the `placeholderToMsgIds` of the referenced
// messages, those would always be empty
// TODO(vicb): overkill - create 2 buckets and [...woDeps, ...wDeps].process()
if (Object.keys(messageMap[a[0]].placeholderToMessage).length == 0) {
return -1;
}
if (Object.keys(messageMap[b[0]].placeholderToMessage).length == 0) {
return 1;
}
return 0;
})
.forEach(message => {
const msgId = message[0];
this._sourceMessage = messageMap[msgId];
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
this._translatedMessages[msgId] = ml.visitAll(this, message[1]).join('');
});
return {messages: this._translatedMessages, errors: this._errors};
2016-07-21 13:56:58 -07:00
}
visitElement(element: ml.Element, context: any): any {
2016-07-21 13:56:58 -07:00
switch (element.name) {
case _TRANSLATIONS_TAG:
this._bundleDepth++;
if (this._bundleDepth > 1) {
this._addError(element, `<${_TRANSLATIONS_TAG}> elements can not be nested`);
}
ml.visitAll(this, element.children, null);
2016-07-21 13:56:58 -07:00
this._bundleDepth--;
break;
case _TRANSLATION_TAG:
this._translationDepth++;
if (this._translationDepth > 1) {
this._addError(element, `<${_TRANSLATION_TAG}> elements can not be nested`);
}
const idAttr = element.attrs.find((attr) => attr.name === 'id');
if (!idAttr) {
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
} else {
// ICU placeholders are reference to other messages.
// The referenced message might not have been decoded yet.
// We need to have all messages available to make sure deps are decoded first.
// TODO(vicb): report an error on duplicate id
this._messageNodes.push([idAttr.value, element.children]);
2016-07-21 13:56:58 -07:00
}
this._translationDepth--;
break;
case _PLACEHOLDER_TAG:
const nameAttr = element.attrs.find((attr) => attr.name === 'name');
if (!nameAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
const phName = nameAttr.value;
if (this._sourceMessage.placeholders.hasOwnProperty(phName)) {
return this._sourceMessage.placeholders[phName];
}
if (this._sourceMessage.placeholderToMessage.hasOwnProperty(phName)) {
const refMsg = this._sourceMessage.placeholderToMessage[phName];
const refMsgId = this._serializer.digest(refMsg);
if (this._translatedMessages.hasOwnProperty(refMsgId)) {
return this._translatedMessages[refMsgId];
}
2016-07-21 13:56:58 -07:00
}
// TODO(vicb): better error message for when
// !this._translatedMessages.hasOwnProperty(refMessageId)
2016-07-21 13:56:58 -07:00
this._addError(
element, `The placeholder "${phName}" does not exists in the source message`);
2016-07-21 13:56:58 -07:00
}
break;
default:
this._addError(element, 'Unexpected tag');
}
}
visitAttribute(attribute: ml.Attribute, context: any): any {
2016-07-21 13:56:58 -07:00
throw new Error('unreachable code');
}
visitText(text: ml.Text, context: any): any { return text.value; }
2016-07-21 13:56:58 -07:00
visitComment(comment: ml.Comment, context: any): any { return ''; }
2016-07-21 13:56:58 -07:00
visitExpansion(expansion: ml.Expansion, context: any): any {
2016-07-21 13:56:58 -07:00
const strCases = expansion.cases.map(c => c.visit(this, null));
2016-11-01 18:02:29 -07:00
return `{${expansion.switchValue}, ${expansion.type}, ${strCases.join(' ')}}`;
2016-07-21 13:56:58 -07:00
}
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {
2016-11-01 18:02:29 -07:00
return `${expansionCase.value} {${ml.visitAll(this, expansionCase.expression, null).join('')}}`;
2016-07-21 13:56:58 -07:00
}
private _addError(node: ml.Node, message: string): void {
2016-07-21 13:56:58 -07:00
this._errors.push(new I18nError(node.sourceSpan, message));
}
}