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
|
|
|
|
|
*/
|
|
|
|
|
|
2016-08-01 12:19:09 -07:00
|
|
|
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';
|
2016-08-09 21:05:04 -07:00
|
|
|
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
|
|
|
|
2016-08-09 21:05:04 -07:00
|
|
|
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer';
|
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(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); }
|
|
|
|
|
|
2016-08-09 21:05:04 -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
|
|
|
|
|
const result = new XmlParser().parse(content, url);
|
|
|
|
|
|
|
|
|
|
if (result.errors.length) {
|
|
|
|
|
throw new Error(`xtb parse errors:\n${result.errors.join('\n')}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace the placeholders, messages are now string
|
2016-08-12 20:14:52 -07:00
|
|
|
const {messages, errors} = new _Visitor().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
|
2016-11-12 14:08:58 +01:00
|
|
|
const messageMap: {[id: string]: ml.Node[]} = {};
|
2016-08-09 21:05:04 -07:00
|
|
|
const parseErrors: ParseError[] = [];
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-08-04 19:35:41 +02:00
|
|
|
Object.keys(messages).forEach((id) => {
|
2016-07-21 13:56:58 -07:00
|
|
|
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
|
|
|
|
|
parseErrors.push(...res.errors);
|
|
|
|
|
messageMap[id] = res.rootNodes;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (parseErrors.length) {
|
|
|
|
|
throw new Error(`xtb parse errors:\n${parseErrors.join('\n')}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return messageMap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-12 20:14:52 -07:00
|
|
|
class _Visitor implements ml.Visitor {
|
2016-08-09 21:05:04 -07:00
|
|
|
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[];
|
2016-08-09 21:05:04 -07:00
|
|
|
private _placeholders: {[name: string]: string};
|
|
|
|
|
private _placeholderToIds: {[name: string]: string};
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-08-09 21:05:04 -07:00
|
|
|
parse(nodes: ml.Node[], messageBundle: MessageBundle):
|
2016-07-21 13:56:58 -07:00
|
|
|
{messages: {[k: string]: string}, errors: I18nError[]} {
|
2016-08-09 21:05:04 -07:00
|
|
|
this._messageNodes = [];
|
|
|
|
|
this._translatedMessages = {};
|
2016-07-21 13:56:58 -07:00
|
|
|
this._bundleDepth = 0;
|
|
|
|
|
this._translationDepth = 0;
|
|
|
|
|
this._errors = [];
|
|
|
|
|
|
2016-08-09 21:05:04 -07:00
|
|
|
// Find all messages
|
2016-08-01 12:19:09 -07:00
|
|
|
ml.visitAll(this, nodes, null);
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-08-09 21:05:04 -07:00
|
|
|
const messageMap = messageBundle.getMessageMap();
|
|
|
|
|
const placeholders = extractPlaceholders(messageBundle);
|
|
|
|
|
const placeholderToIds = extractPlaceholderToIds(messageBundle);
|
|
|
|
|
|
|
|
|
|
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]].placeholderToMsgIds).length == 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
})
|
|
|
|
|
.forEach(message => {
|
|
|
|
|
const id = message[0];
|
|
|
|
|
this._placeholders = placeholders[id] || {};
|
|
|
|
|
this._placeholderToIds = placeholderToIds[id] || {};
|
|
|
|
|
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
|
|
|
|
|
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {messages: this._translatedMessages, errors: this._errors};
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-01 12:19:09 -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`);
|
|
|
|
|
}
|
2016-08-01 12:19:09 -07:00
|
|
|
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 {
|
2016-08-09 21:05:04 -07:00
|
|
|
// 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 {
|
2016-08-09 21:05:04 -07:00
|
|
|
const name = nameAttr.value;
|
|
|
|
|
if (this._placeholders.hasOwnProperty(name)) {
|
|
|
|
|
return this._placeholders[name];
|
|
|
|
|
}
|
|
|
|
|
if (this._placeholderToIds.hasOwnProperty(name) &&
|
|
|
|
|
this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])) {
|
|
|
|
|
return this._translatedMessages[this._placeholderToIds[name]];
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
2016-08-09 21:05:04 -07:00
|
|
|
// TODO(vicb): better error message for when
|
|
|
|
|
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])
|
2016-07-21 13:56:58 -07:00
|
|
|
this._addError(
|
2016-08-09 21:05:04 -07:00
|
|
|
element, `The placeholder "${name}" does not exists in the source message`);
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
this._addError(element, 'Unexpected tag');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-01 12:19:09 -07:00
|
|
|
visitAttribute(attribute: ml.Attribute, context: any): any {
|
2016-07-21 13:56:58 -07:00
|
|
|
throw new Error('unreachable code');
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-01 12:19:09 -07:00
|
|
|
visitText(text: ml.Text, context: any): any { return text.value; }
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-08-01 12:19:09 -07:00
|
|
|
visitComment(comment: ml.Comment, context: any): any { return ''; }
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-08-01 12:19:09 -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));
|
|
|
|
|
|
|
|
|
|
return `{${expansion.switchValue}, ${expansion.type}, strCases.join(' ')}`;
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-01 12:19:09 -07:00
|
|
|
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {
|
|
|
|
|
return `${expansionCase.value} {${ml.visitAll(this, expansionCase.expression, null)}}`;
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
|
2016-08-01 12:19:09 -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));
|
|
|
|
|
}
|
|
|
|
|
}
|