225 lines
7.1 KiB
TypeScript
Raw Normal View History

2016-07-15 09:42:33 -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 {decimalDigest} from '../digest';
2016-07-21 13:56:58 -07:00
import * as i18n from '../i18n_ast';
2016-07-15 09:42:33 -07:00
import {PlaceholderMapper, Serializer} from './serializer';
2016-07-15 09:42:33 -07:00
import * as xml from './xml_helper';
const _MESSAGES_TAG = 'messagebundle';
const _MESSAGE_TAG = 'msg';
const _PLACEHOLDER_TAG = 'ph';
const _EXEMPLE_TAG = 'ex';
2016-07-29 13:07:01 -07:00
const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
<!ATTLIST messagebundle class CDATA #IMPLIED>
<!ELEMENT msg (#PCDATA|ph|source)*>
<!ATTLIST msg id CDATA #IMPLIED>
<!ATTLIST msg seq CDATA #IMPLIED>
<!ATTLIST msg name CDATA #IMPLIED>
<!ATTLIST msg desc CDATA #IMPLIED>
<!ATTLIST msg meaning CDATA #IMPLIED>
<!ATTLIST msg obsolete (obsolete) #IMPLIED>
<!ATTLIST msg xml:space (default|preserve) "default">
<!ATTLIST msg is_hidden CDATA #IMPLIED>
<!ELEMENT source (#PCDATA)>
<!ELEMENT ph (#PCDATA|ex)*>
<!ATTLIST ph name CDATA #REQUIRED>
<!ELEMENT ex (#PCDATA)>`;
export class Xmb extends Serializer {
write(messages: i18n.Message[]): string {
const exampleVisitor = new ExampleVisitor();
2016-07-15 09:42:33 -07:00
const visitor = new _Visitor();
2016-11-01 18:02:29 -07:00
let rootNode = new xml.Tag(_MESSAGES_TAG);
2016-07-15 09:42:33 -07:00
messages.forEach(message => {
const attrs: {[k: string]: string} = {id: message.id};
2016-07-15 09:42:33 -07:00
if (message.description) {
attrs['desc'] = message.description;
}
if (message.meaning) {
attrs['meaning'] = message.meaning;
}
rootNode.children.push(
new xml.CR(2), new xml.Tag(_MESSAGE_TAG, attrs, visitor.serialize(message.nodes)));
2016-07-15 09:42:33 -07:00
});
rootNode.children.push(new xml.CR());
2016-07-15 09:42:33 -07:00
return xml.serialize([
2016-07-29 13:07:01 -07:00
new xml.Declaration({version: '1.0', encoding: 'UTF-8'}),
new xml.CR(),
2016-07-29 13:07:01 -07:00
new xml.Doctype(_MESSAGES_TAG, _DOCTYPE),
new xml.CR(),
exampleVisitor.addDefaultExamples(rootNode),
new xml.CR(),
2016-07-15 09:42:33 -07:00
]);
}
load(content: string, url: string): {[msgId: string]: i18n.Node[]} {
2016-07-21 13:56:58 -07:00
throw new Error('Unsupported');
}
digest(message: i18n.Message): string { return digest(message); }
createNameMapper(message: i18n.Message): PlaceholderMapper {
return new XmbPlaceholderMapper(message);
}
2016-07-15 09:42:33 -07:00
}
2016-07-21 13:56:58 -07:00
class _Visitor implements i18n.Visitor {
visitText(text: i18n.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
2016-07-15 09:42:33 -07:00
visitContainer(container: i18n.Container, context: any): xml.Node[] {
2016-07-15 09:42:33 -07:00
const nodes: xml.Node[] = [];
container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this)));
2016-07-15 09:42:33 -07:00
return nodes;
}
visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
const nodes = [new xml.Text(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
2016-07-15 09:42:33 -07:00
Object.keys(icu.cases).forEach((c: string) => {
nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this), new xml.Text(`} `));
2016-07-15 09:42:33 -07:00
});
nodes.push(new xml.Text(`}`));
return nodes;
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): xml.Node[] {
2016-07-15 09:42:33 -07:00
const startEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`<${ph.tag}>`)]);
const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.startName}, [startEx]);
2016-07-15 09:42:33 -07:00
if (ph.isVoid) {
// void tags have no children nor closing tags
return [startTagPh];
}
const closeEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`</${ph.tag}>`)]);
const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.closeName}, [closeEx]);
2016-07-15 09:42:33 -07:00
return [startTagPh, ...this.serialize(ph.children), closeTagPh];
2016-07-15 09:42:33 -07:00
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
2016-07-15 09:42:33 -07:00
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
2016-07-15 09:42:33 -07:00
}
serialize(nodes: i18n.Node[]): xml.Node[] {
return [].concat(...nodes.map(node => node.visit(this)));
2016-07-15 09:42:33 -07:00
}
}
export function digest(message: i18n.Message): string {
return decimalDigest(message);
}
// TC requires at least one non-empty example on placeholders
class ExampleVisitor implements xml.IVisitor {
addDefaultExamples(node: xml.Node): xml.Node {
node.visit(this);
return node;
}
visitTag(tag: xml.Tag): void {
if (tag.name === _PLACEHOLDER_TAG) {
if (!tag.children || tag.children.length == 0) {
const exText = new xml.Text(tag.attrs['name'] || '...');
tag.children = [new xml.Tag(_EXEMPLE_TAG, {}, [exText])];
}
} else if (tag.children) {
tag.children.forEach(node => node.visit(this));
}
}
visitText(text: xml.Text): void {}
visitDeclaration(decl: xml.Declaration): void {}
visitDoctype(doctype: xml.Doctype): void {}
}
/**
* XMB/XTB placeholders can only contain A-Z, 0-9 and _
*
* Because such restrictions do not exist on placeholder names generated locally, the
* `PlaceholderMapper` is used to convert internal names to XMB names when the XMB file is
* serialized and back from XTB to internal names when an XTB is loaded.
*/
export class XmbPlaceholderMapper implements PlaceholderMapper, i18n.Visitor {
private internalToXmb: {[k: string]: string} = {};
private xmbToNextId: {[k: string]: number} = {};
private xmbToInternal: {[k: string]: string} = {};
// create a mapping from the message
constructor(message: i18n.Message) { message.nodes.forEach(node => node.visit(this)); }
toPublicName(internalName: string): string {
return this.internalToXmb.hasOwnProperty(internalName) ? this.internalToXmb[internalName] :
null;
}
toInternalName(publicName: string): string {
return this.xmbToInternal.hasOwnProperty(publicName) ? this.xmbToInternal[publicName] : null;
}
visitText(text: i18n.Text, context?: any): any { return null; }
visitContainer(container: i18n.Container, context?: any): any {
container.children.forEach(child => child.visit(this));
}
visitIcu(icu: i18n.Icu, context?: any): any {
Object.keys(icu.cases).forEach(k => { icu.cases[k].visit(this); });
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): any {
this.addPlaceholder(ph.startName);
ph.children.forEach(child => child.visit(this));
this.addPlaceholder(ph.closeName);
}
visitPlaceholder(ph: i18n.Placeholder, context?: any): any { this.addPlaceholder(ph.name); }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { this.addPlaceholder(ph.name); }
// XMB placeholders could only contains A-Z, 0-9 and _
private addPlaceholder(internalName: string): void {
if (!internalName || this.internalToXmb.hasOwnProperty(internalName)) {
return;
}
let xmbName = internalName.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
if (this.xmbToInternal.hasOwnProperty(xmbName)) {
// Create a new XMB when it has already been used
const nextId = this.xmbToNextId[xmbName];
this.xmbToNextId[xmbName] = nextId + 1;
xmbName = `${xmbName}_${nextId}`;
} else {
this.xmbToNextId[xmbName] = 1;
}
this.internalToXmb[internalName] = xmbName;
this.xmbToInternal[xmbName] = internalName;
}
}