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 {XmlParser} from '../../ml_parser/xml_parser';
|
2016-07-21 13:56:58 -07:00
|
|
|
import * as i18n from '../i18n_ast';
|
2016-07-29 11:10:43 -07:00
|
|
|
import {I18nError} from '../parse_util';
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-10-31 18:22:11 -07:00
|
|
|
import {Serializer} from './serializer';
|
2016-10-28 19:53:42 -07:00
|
|
|
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 {
|
2016-10-28 19:53:42 -07:00
|
|
|
write(messages: i18n.Message[]): string { throw new Error('Unsupported'); }
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
|
|
|
|
|
// xtb to xml nodes
|
|
|
|
|
const xtbParser = new XtbParser();
|
|
|
|
|
const {mlNodesByMsgId, errors} = xtbParser.parse(content, url);
|
|
|
|
|
|
|
|
|
|
// xml nodes to i18n nodes
|
|
|
|
|
const i18nNodesByMsgId: {[msgId: string]: i18n.Node[]} = {};
|
|
|
|
|
const converter = new XmlToI18n();
|
|
|
|
|
Object.keys(mlNodesByMsgId).forEach(msgId => {
|
|
|
|
|
const {i18nNodes, errors: e} = converter.convert(mlNodesByMsgId[msgId]);
|
|
|
|
|
errors.push(...e);
|
|
|
|
|
i18nNodesByMsgId[msgId] = i18nNodes;
|
|
|
|
|
});
|
2016-07-21 13:56:58 -07:00
|
|
|
|
|
|
|
|
if (errors.length) {
|
|
|
|
|
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
return i18nNodesByMsgId;
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
2016-10-28 19:53:42 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
digest(message: i18n.Message): string { return digest(message); }
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
// Extract messages as xml nodes from the xtb file
|
|
|
|
|
class XtbParser implements ml.Visitor {
|
2016-07-21 13:56:58 -07:00
|
|
|
private _bundleDepth: number;
|
|
|
|
|
private _errors: I18nError[];
|
2016-11-02 17:40:15 -07:00
|
|
|
private _mlNodesByMsgId: {[msgId: string]: ml.Node[]};
|
2016-10-28 19:53:42 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
parse(xtb: string, url: string) {
|
2016-07-21 13:56:58 -07:00
|
|
|
this._bundleDepth = 0;
|
2016-11-02 17:40:15 -07:00
|
|
|
this._mlNodesByMsgId = {};
|
2016-08-09 21:05:04 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
const xml = new XmlParser().parse(xtb, url, true);
|
2016-08-09 21:05:04 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
this._errors = xml.errors;
|
|
|
|
|
ml.visitAll(this, xml.rootNodes);
|
2016-08-09 21:05:04 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
return {
|
|
|
|
|
mlNodesByMsgId: this._mlNodesByMsgId,
|
|
|
|
|
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:
|
|
|
|
|
const idAttr = element.attrs.find((attr) => attr.name === 'id');
|
|
|
|
|
if (!idAttr) {
|
|
|
|
|
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
|
|
|
|
|
} else {
|
2016-11-02 17:40:15 -07:00
|
|
|
const id = idAttr.value;
|
|
|
|
|
if (this._mlNodesByMsgId.hasOwnProperty(id)) {
|
|
|
|
|
this._addError(element, `Duplicated translations for msg ${id}`);
|
|
|
|
|
} else {
|
|
|
|
|
this._mlNodesByMsgId[id] = element.children;
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
this._addError(element, 'Unexpected tag');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
visitAttribute(attribute: ml.Attribute, context: any): any {}
|
|
|
|
|
|
|
|
|
|
visitText(text: ml.Text, context: any): any {}
|
|
|
|
|
|
|
|
|
|
visitComment(comment: ml.Comment, context: any): any {}
|
|
|
|
|
|
|
|
|
|
visitExpansion(expansion: ml.Expansion, context: any): any {}
|
|
|
|
|
|
|
|
|
|
visitExpansionCase(expansionCase: ml.ExpansionCase, context: any): any {}
|
|
|
|
|
|
|
|
|
|
private _addError(node: ml.Node, message: string): void {
|
|
|
|
|
this._errors.push(new I18nError(node.sourceSpan, message));
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
2016-11-02 17:40:15 -07:00
|
|
|
}
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
// Convert ml nodes (xtb syntax) to i18n nodes
|
|
|
|
|
class XmlToI18n implements ml.Visitor {
|
|
|
|
|
private _errors: I18nError[];
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
convert(nodes: ml.Node[]) {
|
|
|
|
|
this._errors = [];
|
|
|
|
|
return {
|
|
|
|
|
i18nNodes: ml.visitAll(this, nodes),
|
|
|
|
|
errors: this._errors,
|
|
|
|
|
};
|
|
|
|
|
}
|
2016-07-21 13:56:58 -07:00
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
visitText(text: ml.Text, context: any) { return new i18n.Text(text.value, text.sourceSpan); }
|
|
|
|
|
|
|
|
|
|
visitExpansion(icu: ml.Expansion, context: any) {
|
|
|
|
|
const caseMap: {[value: string]: i18n.Node} = {};
|
|
|
|
|
|
|
|
|
|
ml.visitAll(this, icu.cases).forEach(c => {
|
|
|
|
|
caseMap[c.value] = new i18n.Container(c.nodes, icu.sourceSpan);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return new i18n.Icu(icu.switchValue, icu.type, caseMap, icu.sourceSpan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visitExpansionCase(icuCase: ml.ExpansionCase, context: any): any {
|
|
|
|
|
return {
|
|
|
|
|
value: icuCase.value,
|
|
|
|
|
nodes: ml.visitAll(this, icuCase.expression),
|
|
|
|
|
};
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
visitElement(el: ml.Element, context: any): i18n.Placeholder {
|
|
|
|
|
if (el.name === _PLACEHOLDER_TAG) {
|
|
|
|
|
const nameAttr = el.attrs.find((attr) => attr.name === 'name');
|
|
|
|
|
if (nameAttr) {
|
|
|
|
|
return new i18n.Placeholder('', nameAttr.value, el.sourceSpan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._addError(el, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
|
|
|
|
|
} else {
|
|
|
|
|
this._addError(el, `Unexpected tag`);
|
|
|
|
|
}
|
2016-07-21 13:56:58 -07:00
|
|
|
}
|
|
|
|
|
|
2016-11-02 17:40:15 -07:00
|
|
|
visitComment(comment: ml.Comment, context: any) {}
|
|
|
|
|
|
|
|
|
|
visitAttribute(attribute: ml.Attribute, context: any) {}
|
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|