Source: messageformat/messages.js

/**
 * @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;
}