edgewall/trac

View on GitHub
trac/htdocs/js/babel.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * Babel JavaScript Support
 *
 * Copyright (C) 2008 Edgewall Software
 * All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at http://babel.edgewall.org/wiki/License.
 *
 * This software consists of voluntary contributions made by many
 * individuals. For the exact contribution history, see the revision
 * history and logs, available at http://babel.edgewall.org/log/.
 */

/**
 * A simple module that provides a gettext like translation interface.
 * The catalog passed to load() must be a object conforming to this
 * interface::
 *
 *    {
 *      messages:     an object of {msgid: translations} items where
 *                    translations is an array of messages or a single
 *                    string if the message is not pluralizable.
 *      plural_expr:  the plural expression for the language.
 *      locale:       the identifier for this locale.
 *      domain:       the name of the domain.
 *    }
 *
 * Missing elements in the object are ignored.
 *
 * Typical usage::
 *
 *    var translations = babel.Translations.load(...).install();
 */
var babel = new function() {

  var defaultPluralExpr = function(n) { return n == 1 ? 0 : 1; };
  var formatRegex = /%(?:(?:\(([^\)]+)\))?([disr])|%)/g;
  var translations = {};
  var merged;

  /**
   * A translations object implementing the gettext interface
   */
  var Translations = this.Translations = function(locale, domain) {
    this.messages = {};
    this.locale = locale || 'unknown';
    this.domain = domain || 'messages';
    this.pluralexpr = defaultPluralExpr;
  };

  /**
   * Create a new translations object from the catalog and return it.
   * See the babel-module comment for more details.
   */
  Translations.load = function(catalog) {
    var rv = new Translations();
    rv.load(catalog);
    translations[rv.domain] = rv;
    merged.load(catalog);
    return rv;
  };

  /**
   * Get a Translations instance from the loaded translations. If the
   * specified domain doesn't exist, returns a dummy Translations
   * instance.
   */
  Translations.get = function(domain) {
    return translations[domain] || (new Translations({domain: domain}));
  };

  Translations.prototype = {
    /**
     * translate a single string
     *
     * If extra parameters are given, use them to fill the format
     * specified by the string.
     */
    gettext: function(string) {
      var translated = this.messages[string];
      if (typeof translated == 'undefined')
        translated = string;
      else if (typeof translated != 'string')
        translated = translated[0];
      if (arguments.length > 1) {
        arguments[0] = translated;
        return babel.format.apply(this, arguments);
      }
      return translated;
    },

    /**
     * translate a pluralizable string
     *
     * If extra parameters are given, use them to fill the format
     * specified by the string.
     */
    ngettext: function(singular, plural, n) {
      var translated = this.messages[singular];
      if (typeof translated == 'undefined')
        translated = (n == 1) ? singular : plural;
      else
        translated = translated[this.pluralexpr(n)];
      if (arguments.length > 3) {
        var format_args = Array.prototype.slice.call(arguments, 3);
        format_args.unshift(translated);
        return babel.format.apply(this, format_args)
      }
      return translated;
    },

    /**
     * Install this translation document wide.  After this call, there are
     * three new methods on the window object: _, gettext and ngettext
     */
    install: function() {
      window._ = window.gettext = function() {
        return merged.gettext.apply(merged, arguments);
      };
      window.ngettext = function(singular, plural, n) {
        return merged.ngettext.apply(merged, arguments);
      };
      return this;
    },

    /**
     * Works like Translations.load but updates the instance rather
     * then creating a new one.
     */
    load: function(catalog) {
      if (catalog.messages)
        this.update(catalog.messages)
      if (catalog.plural_expr)
        this.setPluralExpr(catalog.plural_expr);
      if (catalog.locale)
        this.locale = catalog.locale;
      if (catalog.domain)
        this.domain = catalog.domain;
      return this;
    },

    /**
     * Updates the translations with the object of messages.
     */
    update: function(mapping) {
      for (var key in mapping)
        if (mapping.hasOwnProperty(key))
          this.messages[key] = mapping[key];
      return this;
    },

    /**
     * Sets the plural expression
     */
    setPluralExpr: function(expr) {
      this.pluralexpr = new Function('n', 'return +(' + expr + ')');
      return this;
    }
  };

  merged = new Translations({});

  /**
   * Translate a single string in the specified domain.
   *
   * If extra parameters are given, use them to fill the format
   * specified by the string.
   */
  window.dgettext = this.dgettext = function(domain, string) {
    var rv = translations[domain];
    var args = Array.prototype.slice.call(arguments, 1);
    if (typeof rv != 'undefined')
      return rv.gettext.apply(rv, args);
    if (arguments.length > 1)
      return babel.format.apply(this, args);
    return string;
  };

  /**
   * Translate a pluralizable string in the specified domain.
   *
   * If extra parameters are given, use them to fill the format
   * specified by the string.
   */
  window.dngettext = this.dngettext = function(domain, singular, plural, n) {
    var rv = translations[domain];
    if (typeof rv != 'undefined') {
      var args = Array.prototype.slice.call(arguments, 1);
      return rv.ngettext.apply(rv, args);
    }
    if (arguments.length > 4) {
      var args = Array.prototype.slice.call(arguments, 4);
      args.unshift(singular);
      return babel.format.apply(this, format_args)
    }
    return (n == 1) ? singular : plural;
  };

  /**
   * A python inspired string formatting function.  Supports named and
   * positional placeholders and "s", "d" and "i" as type characters
   * without any formatting specifications.
   *
   * Examples::
   *
   *    babel.format(_('Hello %s'), name)
   *    babel.format(_('Progress: %(percent)s%%'), {percent: 100})
   */
  this.format = function() {
    var arg, string = arguments[0], idx = 0;
    if (arguments.length == 1)
      return string;
    else if (arguments.length == 2 && typeof arguments[1] == 'object')
      arg = arguments[1];
    else {
      arg = [];
      for (var i = 1, n = arguments.length; i != n; ++i)
        arg[i - 1] = arguments[i];
    }
    return string.replace(formatRegex, function(all, name, type) {
      if (all == '%%') return '%';
      var value = arg[name || idx++];
      return (type == 'i' || type == 'd') ? +value : value;
    });
  }

};