refactor(compiler): [i18n] move dedup and placeholder mapping to the `MessageBundle`

It makes implementing a `Serializer` simpler as implementations do not have to
care any more about message dedup and placeholder mapping.
This commit is contained in:
Victor Berchet 2017-01-19 14:42:25 -08:00 committed by Alex Rickabaugh
parent d02eab498f
commit c3e5ddbe20
5 changed files with 100 additions and 57 deletions

View File

@ -11,14 +11,15 @@ import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseError} from '../parse_util'; import {ParseError} from '../parse_util';
import {extractMessages} from './extractor_merger'; import {extractMessages} from './extractor_merger';
import {Message} from './i18n_ast'; import * as i18n from './i18n_ast';
import {Serializer} from './serializers/serializer'; import {PlaceholderMapper, Serializer} from './serializers/serializer';
/** /**
* A container for message extracted from the templates. * A container for message extracted from the templates.
*/ */
export class MessageBundle { export class MessageBundle {
private _messages: Message[] = []; private _messages: i18n.Message[] = [];
constructor( constructor(
private _htmlParser: HtmlParser, private _implicitTags: string[], private _htmlParser: HtmlParser, private _implicitTags: string[],
@ -42,7 +43,70 @@ export class MessageBundle {
this._messages.push(...i18nParserResult.messages); this._messages.push(...i18nParserResult.messages);
} }
getMessages(): Message[] { return this._messages; } // Return the message in the internal format
// The public (serialized) format might be different, see the `write` method.
getMessages(): i18n.Message[] { return this._messages; }
write(serializer: Serializer): string { return serializer.write(this._messages); } write(serializer: Serializer): string {
const messages: {[id: string]: i18n.Message} = {};
const mapperVisitor = new MapPlaceholderNames();
// Deduplicate messages based on their ID
this._messages.forEach(message => {
const id = serializer.digest(message);
if (!messages.hasOwnProperty(id)) {
messages[id] = message;
}
});
// Transform placeholder names using the serializer mapping
const msgList = Object.keys(messages).map(id => {
const mapper = serializer.createNameMapper(messages[id]);
const src = messages[id];
const nodes = mapper ? mapperVisitor.convert(src.nodes, mapper) : src.nodes;
return new i18n.Message(nodes, {}, {}, src.meaning, src.description, id);
});
return serializer.write(msgList);
}
}
// Transform an i18n AST by renaming the placeholder nodes with the given mapper
class MapPlaceholderNames implements i18n.Visitor {
convert(nodes: i18n.Node[], mapper: PlaceholderMapper): i18n.Node[] {
return mapper ? nodes.map(n => n.visit(this, mapper)) : nodes;
}
visitText(text: i18n.Text, mapper: PlaceholderMapper): i18n.Text {
return new i18n.Text(text.value, text.sourceSpan);
}
visitContainer(container: i18n.Container, mapper: PlaceholderMapper): i18n.Container {
const children = container.children.map(n => n.visit(this, mapper));
return new i18n.Container(children, container.sourceSpan);
}
visitIcu(icu: i18n.Icu, mapper: PlaceholderMapper): i18n.Icu {
const cases: {[k: string]: i18n.Node} = {};
Object.keys(icu.cases).forEach(key => cases[key] = icu.cases[key].visit(this, mapper));
const msg = new i18n.Icu(icu.expression, icu.type, cases, icu.sourceSpan);
msg.expressionPlaceholder = icu.expressionPlaceholder;
return msg;
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, mapper: PlaceholderMapper): i18n.TagPlaceholder {
const startName = mapper.toPublicName(ph.startName);
const closeName = ph.closeName ? mapper.toPublicName(ph.closeName) : ph.closeName;
const children = ph.children.map(n => n.visit(this, mapper));
return new i18n.TagPlaceholder(
ph.tag, ph.attrs, startName, closeName, children, ph.isVoid, ph.sourceSpan);
}
visitPlaceholder(ph: i18n.Placeholder, mapper: PlaceholderMapper): i18n.Placeholder {
return new i18n.Placeholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, mapper: PlaceholderMapper): i18n.IcuPlaceholder {
return new i18n.IcuPlaceholder(ph.value, mapper.toPublicName(ph.name), ph.sourceSpan);
}
} }

View File

@ -9,6 +9,9 @@
import * as i18n from '../i18n_ast'; import * as i18n from '../i18n_ast';
export abstract class Serializer { export abstract class Serializer {
// - The `placeholders` and `placeholderToMessage` properties are irrelevant in the input messages
// - The `id` contains the message id that the serializer is expected to use
// - Placeholder names are already map to public names using the provided mapper
abstract write(messages: i18n.Message[]): string; abstract write(messages: i18n.Message[]): string;
abstract load(content: string, url: string): {[msgId: string]: i18n.Node[]}; abstract load(content: string, url: string): {[msgId: string]: i18n.Node[]};

View File

@ -30,17 +30,10 @@ const _UNIT_TAG = 'trans-unit';
export class Xliff extends Serializer { export class Xliff extends Serializer {
write(messages: i18n.Message[]): string { write(messages: i18n.Message[]): string {
const visitor = new _WriteVisitor(); const visitor = new _WriteVisitor();
const visited: {[id: string]: boolean} = {};
const transUnits: xml.Node[] = []; const transUnits: xml.Node[] = [];
messages.forEach(message => { messages.forEach(message => {
const id = this.digest(message); const transUnit = new xml.Tag(_UNIT_TAG, {id: message.id, datatype: 'html'});
// deduplicate messages
if (visited[id]) return;
visited[id] = true;
const transUnit = new xml.Tag(_UNIT_TAG, {id, datatype: 'html'});
transUnit.children.push( transUnit.children.push(
new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)), new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)),
new xml.CR(8), new xml.Tag(_TARGET_TAG)); new xml.CR(8), new xml.Tag(_TARGET_TAG));

View File

@ -41,19 +41,10 @@ export class Xmb extends Serializer {
write(messages: i18n.Message[]): string { write(messages: i18n.Message[]): string {
const exampleVisitor = new ExampleVisitor(); const exampleVisitor = new ExampleVisitor();
const visitor = new _Visitor(); const visitor = new _Visitor();
const visited: {[id: string]: boolean} = {};
let rootNode = new xml.Tag(_MESSAGES_TAG); let rootNode = new xml.Tag(_MESSAGES_TAG);
messages.forEach(message => { messages.forEach(message => {
const id = this.digest(message); const attrs: {[k: string]: string} = {id: message.id};
// deduplicate messages
if (visited[id]) return;
visited[id] = true;
const mapper = this.createNameMapper(message);
const attrs: {[k: string]: string} = {id};
if (message.description) { if (message.description) {
attrs['desc'] = message.description; attrs['desc'] = message.description;
@ -64,8 +55,7 @@ export class Xmb extends Serializer {
} }
rootNode.children.push( rootNode.children.push(
new xml.CR(2), new xml.CR(2), new xml.Tag(_MESSAGE_TAG, attrs, visitor.serialize(message.nodes)));
new xml.Tag(_MESSAGE_TAG, attrs, visitor.serialize(message.nodes, {mapper})));
}); });
rootNode.children.push(new xml.CR()); rootNode.children.push(new xml.CR());
@ -93,21 +83,19 @@ export class Xmb extends Serializer {
} }
class _Visitor implements i18n.Visitor { class _Visitor implements i18n.Visitor {
visitText(text: i18n.Text, ctx: {mapper: PlaceholderMapper}): xml.Node[] { visitText(text: i18n.Text, context?: any): xml.Node[] { return [new xml.Text(text.value)]; }
return [new xml.Text(text.value)];
}
visitContainer(container: i18n.Container, ctx: any): xml.Node[] { visitContainer(container: i18n.Container, context: any): xml.Node[] {
const nodes: xml.Node[] = []; const nodes: xml.Node[] = [];
container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this, ctx))); container.children.forEach((node: i18n.Node) => nodes.push(...node.visit(this)));
return nodes; return nodes;
} }
visitIcu(icu: i18n.Icu, ctx: {mapper: PlaceholderMapper}): xml.Node[] { visitIcu(icu: i18n.Icu, context?: any): xml.Node[] {
const nodes = [new xml.Text(`{${icu.expressionPlaceholder}, ${icu.type}, `)]; const nodes = [new xml.Text(`{${icu.expressionPlaceholder}, ${icu.type}, `)];
Object.keys(icu.cases).forEach((c: string) => { Object.keys(icu.cases).forEach((c: string) => {
nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this, ctx), new xml.Text(`} `)); nodes.push(new xml.Text(`${c} {`), ...icu.cases[c].visit(this), new xml.Text(`} `));
}); });
nodes.push(new xml.Text(`}`)); nodes.push(new xml.Text(`}`));
@ -115,34 +103,30 @@ class _Visitor implements i18n.Visitor {
return nodes; return nodes;
} }
visitTagPlaceholder(ph: i18n.TagPlaceholder, ctx: {mapper: PlaceholderMapper}): xml.Node[] { visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): xml.Node[] {
const startEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`<${ph.tag}>`)]); const startEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`<${ph.tag}>`)]);
let name = ctx.mapper.toPublicName(ph.startName); const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.startName}, [startEx]);
const startTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name}, [startEx]);
if (ph.isVoid) { if (ph.isVoid) {
// void tags have no children nor closing tags // void tags have no children nor closing tags
return [startTagPh]; return [startTagPh];
} }
const closeEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`</${ph.tag}>`)]); const closeEx = new xml.Tag(_EXEMPLE_TAG, {}, [new xml.Text(`</${ph.tag}>`)]);
name = ctx.mapper.toPublicName(ph.closeName); const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name: ph.closeName}, [closeEx]);
const closeTagPh = new xml.Tag(_PLACEHOLDER_TAG, {name}, [closeEx]);
return [startTagPh, ...this.serialize(ph.children, ctx), closeTagPh]; return [startTagPh, ...this.serialize(ph.children), closeTagPh];
} }
visitPlaceholder(ph: i18n.Placeholder, ctx: {mapper: PlaceholderMapper}): xml.Node[] { visitPlaceholder(ph: i18n.Placeholder, context?: any): xml.Node[] {
const name = ctx.mapper.toPublicName(ph.name); return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
return [new xml.Tag(_PLACEHOLDER_TAG, {name})];
} }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, ctx: {mapper: PlaceholderMapper}): xml.Node[] { visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): xml.Node[] {
const name = ctx.mapper.toPublicName(ph.name); return [new xml.Tag(_PLACEHOLDER_TAG, {name: ph.name})];
return [new xml.Tag(_PLACEHOLDER_TAG, {name})];
} }
serialize(nodes: i18n.Node[], ctx: {mapper: PlaceholderMapper}): xml.Node[] { serialize(nodes: i18n.Node[]): xml.Node[] {
return [].concat(...nodes.map(node => node.visit(this, ctx))); return [].concat(...nodes.map(node => node.visit(this)));
} }
} }
@ -197,25 +181,25 @@ export class XmbPlaceholderMapper implements PlaceholderMapper, i18n.Visitor {
return this.xmbToInternal.hasOwnProperty(publicName) ? this.xmbToInternal[publicName] : null; return this.xmbToInternal.hasOwnProperty(publicName) ? this.xmbToInternal[publicName] : null;
} }
visitText(text: i18n.Text, ctx?: any): any { return null; } visitText(text: i18n.Text, context?: any): any { return null; }
visitContainer(container: i18n.Container, ctx?: any): any { visitContainer(container: i18n.Container, context?: any): any {
container.children.forEach(child => child.visit(this)); container.children.forEach(child => child.visit(this));
} }
visitIcu(icu: i18n.Icu, ctx?: any): any { visitIcu(icu: i18n.Icu, context?: any): any {
Object.keys(icu.cases).forEach(k => { icu.cases[k].visit(this); }); Object.keys(icu.cases).forEach(k => { icu.cases[k].visit(this); });
} }
visitTagPlaceholder(ph: i18n.TagPlaceholder, ctx?: any): any { visitTagPlaceholder(ph: i18n.TagPlaceholder, context?: any): any {
this.addPlaceholder(ph.startName); this.addPlaceholder(ph.startName);
ph.children.forEach(child => child.visit(this)); ph.children.forEach(child => child.visit(this));
this.addPlaceholder(ph.closeName); this.addPlaceholder(ph.closeName);
} }
visitPlaceholder(ph: i18n.Placeholder, ctx?: any): any { this.addPlaceholder(ph.name); } visitPlaceholder(ph: i18n.Placeholder, context?: any): any { this.addPlaceholder(ph.name); }
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, ctx?: 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 _ // XMB placeholders could only contains A-Z, 0-9 and _
private addPlaceholder(internalName: string): void { private addPlaceholder(internalName: string): void {

View File

@ -28,14 +28,13 @@ export function main(): void {
]); ]);
}); });
it('should extract the all messages and duplicates', () => { it('should extract and dedup messages', () => {
messages.updateFromTemplate( messages.updateFromTemplate(
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p><p i18n>Translate Me</p>', 'url', '<p i18n="m|d@@1">Translate Me</p><p i18n="@@2">Translate Me</p><p i18n="@@2">Translate Me</p>',
DEFAULT_INTERPOLATION_CONFIG); 'url', DEFAULT_INTERPOLATION_CONFIG);
expect(humanizeMessages(messages)).toEqual([ expect(humanizeMessages(messages)).toEqual([
'Translate Me (m|d)', 'Translate Me (m|d)',
'Translate Me (|)', 'Translate Me (|)',
'Translate Me (|)',
]); ]);
}); });
}); });
@ -50,7 +49,7 @@ class _TestSerializer extends Serializer {
load(content: string, url: string): {} { return null; } load(content: string, url: string): {} { return null; }
digest(msg: i18n.Message): string { return 'unused'; } digest(msg: i18n.Message): string { return msg.id || `default`; }
} }
function humanizeMessages(catalog: MessageBundle): string[] { function humanizeMessages(catalog: MessageBundle): string[] {