messageformat
Version:
PluralFormat and SelectFormat Message and i18n Tool - A JavaScript Implemenation of the ICU standards.
301 lines (287 loc) • 9.49 kB
JavaScript
/**
* @classdesc Accessor for compiled MessageFormat functions
*
* ```
* import Messages from 'messageformat/messages'
* ```
*
* @class
* @param {object} locales A map of locale codes to their function objects
* @param {string|null} [defaultLocale] If not defined, default and initial locale is the first entry of `locales`
*
* @example
* var fs = require('fs');
* var MessageFormat = require('messageformat');
* var mf = new MessageFormat(['en', 'fi']);
* var msgSet = {
* en: {
* a: 'A {TYPE} example.',
* b: 'This has {COUNT, plural, one{one user} other{# users}}.',
* c: {
* d: 'We have {P, number, percent} code coverage.'
* }
* },
* fi: {
* b: 'Tällä on {COUNT, plural, one{yksi käyttäjä} other{# käyttäjää}}.',
* e: 'Minä puhun vain suomea.'
* }
* };
* var cfStr = mf.compile(msgSet).toString('module.exports');
* fs.writeFileSync('messages.js', cfStr);
*
* ...
*
* var Messages = require('messageformat/messages');
* var msgData = require('./messages');
* var messages = new Messages(msgData, 'en');
*
* messages.hasMessage('a') // true
* messages.hasObject('c') // true
* messages.get('b', { COUNT: 3 }) // 'This has 3 users.'
* messages.get(['c', 'd'], { P: 0.314 }) // 'We have 31% code coverage.'
*
* messages.get('e') // 'e'
* messages.setFallback('en', ['foo', 'fi'])
* messages.get('e') // 'Minä puhun vain suomea.'
*
* messages.locale = 'fi'
* messages.hasMessage('a') // false
* messages.hasMessage('a', 'en') // true
* messages.hasMessage('a', null, true) // true
* messages.hasObject('c') // false
* messages.get('b', { COUNT: 3 }) // 'Tällä on 3 käyttäjää.'
* messages.get('c').d({ P: 0.628 }) // 'We have 63% code coverage.'
*/
function Messages(locales, defaultLocale) {
this._data = {};
this._fallback = {};
Object.keys(locales).forEach(function(lc) {
if (lc !== 'toString') {
this._data[lc] = locales[lc];
if (typeof defaultLocale === 'undefined') defaultLocale = lc;
}
}, this);
/**
* List of available locales
* @readonly
* @memberof Messages
* @member {string[]} availableLocales
*/
Object.defineProperty(this, 'availableLocales', {
get: function() {
return Object.keys(this._data);
}
});
/**
* Current locale
*
* One of Messages#availableLocales or `null`. Partial matches of language tags
* are supported, so e.g. with an `en` locale defined, it will be selected by
* `messages.locale = 'en-US'` and vice versa.
*
* @memberof Messages
* @member {string|null} locale
*/
Object.defineProperty(this, 'locale', {
get: function() {
return this._locale;
},
set: function(lc) {
this._locale = this.resolveLocale(lc);
}
});
this.locale = defaultLocale;
/**
* Default fallback locale
*
* One of Messages#availableLocales or `null`. Partial matches of language tags
* are supported, so e.g. with an `en` locale defined, it will be selected by
* `messages.defaultLocale = 'en-US'` and vice versa.
*
* @memberof Messages
* @member {string|null} defaultLocale
*/
Object.defineProperty(this, 'defaultLocale', {
get: function() {
return this._defaultLocale;
},
set: function(lc) {
this._defaultLocale = this.resolveLocale(lc);
}
});
this._defaultLocale = this._locale;
}
module.exports = Messages;
/**
* Add new messages to the accessor; useful if loading data dynamically
*
* The locale code `lc` should be an exact match for the locale being updated,
* or empty to default to the current locale. Use {@link #resolveLocale} for
* resolving partial locale strings.
*
* If `keypath` is empty, adds or sets the complete message object for the
* corresponding locale. If any keys in `keypath` do not exist, a new object
* will be created at that key.
*
* @param {function|object} data Hierarchical map of keys to functions, or a
* single message function
* @param {string} [lc] If empty or undefined, defaults to `this.locale`
* @param {string[]} [keypath] The keypath being added
* @returns {Messages} The Messages instance, to allow for chaining
*/
Messages.prototype.addMessages = function(data, lc, keypath) {
if (!lc) lc = this.locale;
if (typeof data !== 'function') {
data = Object.keys(data).reduce(function(map, key) {
if (key !== 'toString') map[key] = data[key];
return map;
}, {});
}
if (Array.isArray(keypath) && keypath.length > 0) {
var parent = this._data[lc];
for (var i = 0; i < keypath.length - 1; ++i) {
var key = keypath[i];
if (!parent[key]) parent[key] = {};
parent = parent[key];
}
parent[keypath[keypath.length - 1]] = data;
} else {
this._data[lc] = data;
}
return this;
};
/**
* Resolve `lc` to the key of an available locale or `null`, allowing for
* partial matches. For example, with an `en` locale defined, it will be
* selected by `messages.defaultLocale = 'en-US'` and vice versa.
*
* @param {string} lc Locale code
* @returns {string|null}
*/
Messages.prototype.resolveLocale = function(lc) {
if (this._data[lc]) return lc;
if (lc) {
var l = String(lc);
while ((l = l.replace(/[-_]?[^-_]*$/, ''))) {
if (this._data[l]) return l;
}
var ll = this.availableLocales;
var re = new RegExp('^' + lc + '[-_]');
for (var i = 0; i < ll.length; ++i) {
if (re.test(ll[i])) return ll[i];
}
}
return null;
};
/**
* Get the list of fallback locales
* @param {string} [lc] If empty or undefined, defaults to `this.locale`
* @returns {string[]}
*/
Messages.prototype.getFallback = function(lc) {
if (!lc) lc = this.locale;
return (
this._fallback[lc] ||
(lc === this.defaultLocale || !this.defaultLocale
? []
: [this.defaultLocale])
);
};
/**
* Set the fallback locale or locales for `lc`
*
* To disable fallback for the locale, use `setFallback(lc, [])`.
* To use the default fallback, use `setFallback(lc, null)`.
*
* @param {string} lc
* @param {string[]|null} fallback
* @returns {Messages} The Messages instance, to allow for chaining
*/
Messages.prototype.setFallback = function(lc, fallback) {
this._fallback[lc] = Array.isArray(fallback) ? fallback : null;
return this;
};
/**
* Check if `key` is a message function for the locale
*
* `key` may be a `string` for functions at the root level, or `string[]` for
* accessing hierarchical objects. If an exact match is not found and `fallback`
* is true, the fallback locales are checked for the first match.
*
* @param {string|string[]} key The key or keypath being sought
* @param {string} [lc] If empty or undefined, defaults to `this.locale`
* @param {boolean} [fallback=false] If true, also checks fallback locales
* @returns {boolean}
*/
Messages.prototype.hasMessage = function(key, lc, fallback) {
if (!lc) lc = this.locale;
var fb = fallback ? this.getFallback(lc) : null;
return _has(this._data, lc, key, fb, 'function');
};
/**
* Check if `key` is a message object for the locale
*
* `key` may be a `string` for functions at the root level, or `string[]` for
* accessing hierarchical objects. If an exact match is not found and `fallback`
* is true, the fallback locales are checked for the first match.
*
* @param {string|string[]} key The key or keypath being sought
* @param {string} [lc] If empty or undefined, defaults to `this.locale`
* @param {boolean} [fallback=false] If true, also checks fallback locales
* @returns {boolean}
*/
Messages.prototype.hasObject = function(key, lc, fallback) {
if (!lc) lc = this.locale;
var fb = fallback ? this.getFallback(lc) : null;
return _has(this._data, lc, key, fb, 'object');
};
/**
* Get the message or object corresponding to `key`
*
* `key` may be a `string` for functions at the root level, or `string[]` for
* accessing hierarchical objects. If an exact match is not found, the fallback
* locales are checked for the first match.
*
* If `key` maps to a message function, it will be called with `props`. If it
* maps to an object, the object is returned directly.
*
* @param {string|string[]} key The key or keypath being sought
* @param {object} [props] Optional properties passed to the function
* @param {string} [lc] If empty or undefined, defaults to `this.locale`
* @returns {string|Object<string,function|object>}
*/
Messages.prototype.get = function(key, props, lc) {
if (!lc) lc = this.locale;
var msg = _get(this._data[lc], key);
if (msg) return typeof msg == 'function' ? msg(props) : msg;
var fb = this.getFallback(lc);
for (var i = 0; i < fb.length; ++i) {
msg = _get(this._data[fb[i]], key);
if (msg) return typeof msg == 'function' ? msg(props) : msg;
}
return key;
};
/** @private */
function _get(obj, key) {
if (!obj) return null;
if (Array.isArray(key)) {
for (var i = 0; i < key.length; ++i) {
obj = obj[key[i]];
if (!obj) return null;
}
return obj;
}
return obj[key];
}
/** @private */
function _has(data, lc, key, fallback, type) {
var msg = _get(data[lc], key);
if (msg) return typeof msg === type;
if (fallback) {
for (var i = 0; i < fallback.length; ++i) {
msg = _get(data[fallback[i]], key);
if (msg) return typeof msg === type;
}
}
return false;
}