@messageformat/runtime
Version:
Runtime components of messageformat
324 lines (323 loc) • 11.8 kB
JavaScript
/**
* A collection of runtime utility functions
*
* @remarks
* This package should be marked as a dependency for any package that publishes the output of {@link @messageformat/core#compileModule},
* as it may be included in its ES module source output as a dependency.
*
* For applications that bundle their output using e.g. Webpack this is not necessary.
*
* The `Messages` accessor class is a completely optional addition.
* See also {@link @messageformat/react# | @messageformat/react} for a React-specific solution.
*
* @packageDocumentation
*/
/**
* Accessor class for compiled message functions generated by
* {@link @messageformat/core#compileModule}
*
* @public
* @remarks
* ```js
* import Messages from '@messageformat/runtime/messages'
* ```
*
* @example
* ```js
* // build.js
* import { writeFileSync } from 'fs';
* import MessageFormat from '@messageformat/core';
* import compileModule from '@messageformat/core/compile-module'
*
* const mf = new MessageFormat(['en', 'fi']);
* const 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.'
* }
* };
* writeFileSync('messages.js', compileModule(mf, msgSet));
* ```
*
* ```js
* // runtime.js
* import Messages from '@messageformat/runtime/messages';
* import msgData from './messages';
*
* const 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.'
* ```
*/
var Messages = /** @class */ (function () {
/**
* @param msgData - A map of locale codes to their function objects
* @param defaultLocale - If not defined, default and initial locale is the first key of `msgData`
*/
function Messages(msgData, defaultLocale) {
var _this = this;
/** @internal */
this._data = {};
/** @internal */
this._fallback = {};
/** @internal */
this._defaultLocale = null;
/** @internal */
this._locale = null;
Object.keys(msgData).forEach(function (lc) {
if (lc !== 'toString') {
_this._data[lc] = _withNullPrototype(msgData[lc]);
if (defaultLocale === undefined)
defaultLocale = lc;
}
});
this.locale = defaultLocale || null;
this._defaultLocale = this.locale;
}
Object.defineProperty(Messages.prototype, "availableLocales", {
/** Read-only list of available locales */
get: function () {
return Object.keys(this._data);
},
enumerable: false,
configurable: true
});
Object.defineProperty(Messages.prototype, "locale", {
/**
* Current locale
*
* @remarks
* One of {@link 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.
*/
get: function () {
return this._locale;
},
set: function (locale) {
this._locale = this.resolveLocale(locale);
},
enumerable: false,
configurable: true
});
Object.defineProperty(Messages.prototype, "defaultLocale", {
/**
* Default fallback locale
*
* @remarks
* One of {@link 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.
*/
get: function () {
return this._defaultLocale;
},
set: function (locale) {
this._defaultLocale = this.resolveLocale(locale);
},
enumerable: false,
configurable: true
});
/**
* Add new messages to the accessor; useful if loading data dynamically
*
* @remarks
* The locale code `lc` should be an exact match for the locale being updated, or empty to default to the current locale.
* Use {@link Messages.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 data - Hierarchical map of keys to functions, or a single message function
* @param locale - If empty or undefined, defaults to `this.locale`
* @param keypath - The keypath being added
*/
Messages.prototype.addMessages = function (data, locale, keypath) {
var lc = locale || String(this.locale);
data = _withNullPrototype(data);
if (Array.isArray(keypath) && keypath.length > 0) {
var parent_1 = this._data[lc];
for (var i = 0; i < keypath.length - 1; ++i) {
var key = keypath[i];
if (!parent_1[key])
parent_1[key] = Object.create(null);
parent_1 = parent_1[key];
}
parent_1[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.
*
* @remarks
* For example, with an `en` locale defined, it will be selected by `messages.defaultLocale = 'en-US'` and vice versa.
*/
Messages.prototype.resolveLocale = function (locale) {
var lc = String(locale);
if (this._data[lc])
return locale;
if (locale) {
while ((lc = lc.replace(/[-_]?[^-_]*$/, ''))) {
if (this._data[lc])
return lc;
}
var ll = this.availableLocales;
var re = new RegExp('^' + locale + '[-_]');
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 locale - If empty or undefined, defaults to `this.locale`
*/
Messages.prototype.getFallback = function (locale) {
var lc = locale || String(this.locale);
return (this._fallback[lc] ||
(lc === this.defaultLocale || !this.defaultLocale
? []
: [this.defaultLocale]));
};
/**
* Set the fallback locale or locales for `lc`
*
* @remarks
* To disable fallback for the locale, use `setFallback(lc, [])`.
* To use the default fallback, use `setFallback(lc, null)`.
*/
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
*
* @remarks
* `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 key - The key or keypath being sought
* @param locale - If empty or undefined, defaults to `this.locale`
* @param fallback - If true, also checks fallback locales
*/
Messages.prototype.hasMessage = function (key, locale, fallback) {
var lc = locale || String(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
*
* @remarks
* `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 key - The key or keypath being sought
* @param locale - If empty or undefined, defaults to `this.locale`
* @param fallback - If true, also checks fallback locales
*/
Messages.prototype.hasObject = function (key, locale, fallback) {
var lc = locale || String(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`
*
* @remarks
* `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, the returned value will be the result of calling it with `props`.
* If it maps to an object, the object is returned directly.
* If nothing is found, `key` is returned.
*
* @param key - The key or keypath being sought
* @param props - Optional properties passed to the function
* @param lc - If empty or undefined, defaults to `this.locale`
*/
Messages.prototype.get = function (key, props, locale) {
var lc = locale || String(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;
};
return Messages;
}());
export default Messages;
function _get(obj, key) {
if (!obj)
return null;
var res = obj;
if (Array.isArray(key)) {
for (var i = 0; i < key.length; ++i) {
if (typeof res !== 'object')
return null;
res = res[key[i]];
if (!res)
return null;
}
return res;
}
return typeof res === 'object' ? res[key] : null;
}
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;
}
function _withNullPrototype(value) {
if (!value || typeof value !== 'object')
return value;
var data = Object.create(null);
for (var _i = 0, _a = Object.entries(value); _i < _a.length; _i++) {
var _b = _a[_i], key = _b[0], value_ = _b[1];
data[key] = _withNullPrototype(value_);
}
return data;
}