src/compiler.js
/*
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License.
See the accompanying LICENSE file for terms.
*/
/* jslint esnext: true */
import {existsIn} from './utils';
import {stringFormatFactory} from './compilerUtil';
export default Compiler;
function Compiler(locales, formats, pluralFn, opts) {
this.locales = locales;
this.formats = formats;
this.pluralFn = pluralFn;
this.requireOther = (opts && typeof opts.requireOther === 'boolean') ?
opts.requireOther :
true;
this.stringFormatFactory = (opts && opts.stringFormatFactory) || stringFormatFactory;
}
Compiler.prototype.compile = function (ast) {
this.pluralStack = [];
this.currentPlural = null;
this.pluralNumberFormat = null;
this.tagNames = [];
this.argIds = [];
return this.compileMessage(ast);
};
Compiler.prototype.compileMessage = function (ast) {
if (!(ast && ast.type === 'messageFormatPattern')) {
throw new Error('Message AST is not of type: "messageFormatPattern"');
}
var elements = ast.elements,
pattern = [];
var i, len, element;
for (i = 0, len = elements.length; i < len; i += 1) {
element = elements[i];
switch (element.type) {
case 'messageTextElement':
pattern.push(this.compileMessageText(element));
break;
case 'argumentElement':
pattern.push(this.compileArgument(element));
break;
case 'selfClosingTagElement':
pattern.push(this.compileSelfClosingTag(element));
break;
case 'tagElement':
pattern.push(this.compileTag(element));
break;
default:
throw new Error('Message element does not have a valid type');
}
}
return pattern;
};
Compiler.prototype.compileMessageText = function (element) {
// When this `element` is part of plural sub-pattern and its value contains
// an unescaped '#', use a `PluralOffsetString` helper to properly output
// the number with the correct offset in the string.
if (this.currentPlural && /(^|[^\\])#/g.test(element.value)) {
// Create a cache a NumberFormat instance that can be reused for any
// PluralOffsetString instance in this message.
if (!this.pluralNumberFormat) {
this.pluralNumberFormat = new Intl.NumberFormat(this.locales);
}
return new PluralOffsetString(
this.currentPlural.id,
this.currentPlural.format.offset,
this.pluralNumberFormat,
element.value);
}
// Unescape the escaped '#'s in the message text.
return element.value.replace(/\\#/g, '#');
};
Compiler.prototype.compileArgument = function (element) {
if (existsIn(this.tagNames, element.id)) {
throw new Error('Message has conflicting argument and tag name "' + element.id + '".');
}
this.argIds.push(element.id);
var format = element.format;
if (!format) {
return this.stringFormatFactory(element.id);
}
var formats = this.formats,
locales = this.locales,
pluralFn = this.pluralFn,
options;
switch (format.type) {
case 'numberFormat':
options = formats.number[format.style];
return {
id : element.id,
format: new Intl.NumberFormat(locales, options).format
};
case 'dateFormat':
options = formats.date[format.style];
return {
id : element.id,
format: new Intl.DateTimeFormat(locales, options).format
};
case 'timeFormat':
options = formats.time[format.style];
return {
id : element.id,
format: new Intl.DateTimeFormat(locales, options).format
};
case 'pluralFormat':
options = this.compileOptions(element);
return new PluralFormat(
element.id, format.ordinal, format.offset, options, pluralFn
);
case 'selectFormat':
options = this.compileOptions(element);
return new SelectFormat(element.id, options);
default:
throw new Error('Message element does not have a valid format type');
}
};
Compiler.prototype.compileOptions = function (element) {
var format = element.format,
options = format.options,
optionsHash = {};
if (this.requireOther === true && !element.hasOther) {
throw new Error(format.type + ' requires an `other` option, this can be disabled.');
}
// Save the current plural element, if any, then set it to a new value when
// compiling the options sub-patterns. This conforms the spec's algorithm
// for handling `"#"` syntax in message text.
this.pluralStack.push(this.currentPlural);
this.currentPlural = format.type === 'pluralFormat' ? element : null;
var i, len, option;
for (i = 0, len = options.length; i < len; i += 1) {
option = options[i];
// Compile the sub-pattern and save it under the options's selector.
optionsHash[option.selector] = this.compileMessage(option.value);
}
// Pop the plural stack to put back the original current plural value.
this.currentPlural = this.pluralStack.pop();
return optionsHash;
};
Compiler.prototype.createTag = function (element, pattern) {
if (existsIn(this.argIds, element.name)) {
throw new Error('Message has conflicting argument and tag name "' + element.name + '".');
}
this.tagNames.push(element.name);
return new TagFormat(element.name, pattern);
};
Compiler.prototype.compileTag = function (element) {
return this.createTag(element, this.compileMessage(element.value));
};
Compiler.prototype.compileSelfClosingTag = function (element) {
return this.createTag(element);
};
// -- Compiler Helper Classes --------------------------------------------------
function PluralFormat(id, useOrdinal, offset, options, pluralFn) {
this.id = id;
this.useOrdinal = useOrdinal;
this.offset = offset;
this.options = options;
this.pluralFn = pluralFn;
}
PluralFormat.prototype.getOption = function (value) {
var options = this.options;
var option = options['=' + value] ||
options[this.pluralFn(value - this.offset, this.useOrdinal)];
return option || options.other || [];
};
function PluralOffsetString(id, offset, numberFormat, string) {
this.id = id;
this.offset = offset;
this.numberFormat = numberFormat;
this.string = string;
}
PluralOffsetString.prototype.format = function (value) {
var number = this.numberFormat.format(value - this.offset);
return this.string
.replace(/(^|[^\\])#/g, '$1' + number)
.replace(/\\#/g, '#');
};
function SelectFormat(id, options) {
this.id = id;
this.options = options;
}
SelectFormat.prototype.getOption = function (value) {
var options = this.options;
return options[value] || options.other || [];
};
function TagFormat(id, pattern) {
this.id = id;
this.pattern = pattern;
}
TagFormat.prototype.format = function(value, content) {
if (typeof value !== 'function') {
throw new Error('tag values require a function');
}
if (typeof this.pattern === 'undefined') {
return value(); // self-closing tag
}
return value(content);
};