Commits

Moriyoshi Koizumi  committed 8d50f77 Draft

Initial.

  • Participants

Comments (0)

Files changed (13)

+syntax: glob
+
+bin
+parts
+.installed.cfg
+*.egg-info
+*.pyc
+develop-eggs
+dist
+.mr.developer.cfg
+CHANGES
+*******
+
+0.0.0
+==================
+
+- Initial release.
+i18n-js
+Copyright (c) 2012, Nando Vieira
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+recursive-include js/i18n/resources *.*
+recursive-include js/i18n *.txt
+include *.txt
+js.i18n
+*******
+
+Introduction
+============
+
+This library packages `i18n-js`_ for `fanstatic`_. It is aware of different modes (normal, minified).
+
+.. _`fanstatic`: http://fanstatic.org
+.. _`i18n-js`: http://github.com/fnando/i18n-js/
+
+This requires integration between your web framework and ``fanstatic``,
+and making sure that the original resources (shipped in the ``resources``
+directory in ``js.i18n``) are published to some URL.

File bootstrap.py

+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    quote = str
+
+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
+stdout, stderr = subprocess.Popen(
+    [sys.executable, '-Sc',
+     'try:\n'
+     '    import ConfigParser\n'
+     'except ImportError:\n'
+     '    print 1\n'
+     'else:\n'
+     '    print 0\n'],
+    stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+has_broken_dash_S = bool(int(stdout.strip()))
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded.  This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient.  However, we'll start with that:
+if not has_broken_dash_S and 'site' in sys.modules:
+    # We will restart with python -S.
+    args = sys.argv[:]
+    args[0:0] = [sys.executable, '-S']
+    args = map(quote, args)
+    os.execv(sys.executable, args)
+# Now we are running with -S.  We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in sys.modules.items():
+    if k in ('setuptools', 'pkg_resources') or (
+        hasattr(v, '__path__') and
+        len(v.__path__)==1 and
+        not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+        # This is a namespace package.  Remove it.
+        sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+    if value:
+        if '://' not in value: # It doesn't smell like a URL.
+            value = 'file://%s' % (
+                urllib.pathname2url(
+                    os.path.abspath(os.path.expanduser(value))),)
+        if opt_str == '--download-base' and not value.endswith('/'):
+            # Download base needs a trailing slash to make the world happy.
+            value += '/'
+    else:
+        value = None
+    name = opt_str[2:].replace('-', '_')
+    setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="use_distribute", default=False,
+                   help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or file location for the setup file. "
+                        "If you use Setuptools, this will default to " +
+                        setuptools_source + "; if you use Distribute, this "
+                        "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or directory for downloading "
+                        "zc.buildout and either Setuptools or Distribute. "
+                        "Defaults to PyPI."))
+parser.add_option("--eggs",
+                  help=("Specify a directory for storing eggs.  Defaults to "
+                        "a temporary directory that is deleted when the "
+                        "bootstrap script completes."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.eggs:
+    eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+    eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+    if options.use_distribute:
+        options.setup_source = distribute_source
+    else:
+        options.setup_source = setuptools_source
+
+if options.accept_buildout_test_releases:
+    args.append('buildout:accept-buildout-test-releases=true')
+args.append('bootstrap')
+
+try:
+    import pkg_resources
+    import setuptools # A flag.  Sometimes pkg_resources is installed alone.
+    if not hasattr(pkg_resources, '_distribute'):
+        raise ImportError
+except ImportError:
+    ez_code = urllib2.urlopen(
+        options.setup_source).read().replace('\r\n', '\n')
+    ez = {}
+    exec ez_code in ez
+    setup_args = dict(to_dir=eggs_dir, download_delay=0)
+    if options.download_base:
+        setup_args['download_base'] = options.download_base
+    if options.use_distribute:
+        setup_args['no_fake'] = True
+    ez['use_setuptools'](**setup_args)
+    if 'pkg_resources' in sys.modules:
+        reload(sys.modules['pkg_resources'])
+    import pkg_resources
+    # This does not (always?) update the default working set.  We will
+    # do it.
+    for path in sys.path:
+        if path not in pkg_resources.working_set.entries:
+            pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+       '-c',
+       quote('from setuptools.command.easy_install import main; main()'),
+       '-mqNxd',
+       quote(eggs_dir)]
+
+if not has_broken_dash_S:
+    cmd.insert(1, '-S')
+
+find_links = options.download_base
+if not find_links:
+    find_links = os.environ.get('bootstrap-testing-find-links')
+if find_links:
+    cmd.extend(['-f', quote(find_links)])
+
+if options.use_distribute:
+    setup_requirement = 'distribute'
+else:
+    setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+setup_requirement_path = ws.find(
+    pkg_resources.Requirement.parse(setup_requirement)).location
+env = dict(
+    os.environ,
+    PYTHONPATH=setup_requirement_path)
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+    def _final_version(parsed_version):
+        for part in parsed_version:
+            if (part[:1] == '*') and (part not in _final_parts):
+                return False
+        return True
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setup_requirement_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+if is_jython:
+    import subprocess
+    exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+    sys.stdout.flush()
+    sys.stderr.flush()
+    print ("An error occurred when trying to install zc.buildout. "
+           "Look above this message for any errors that "
+           "were output by easy_install.")
+    sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+    shutil.rmtree(eggs_dir)

File buildout.cfg

+[buildout]
+develop = .
+parts = py.test download
+versions = versions
+
+[py.test]
+recipe = z3c.recipe.scripts
+# Need exit status code for buildbot.
+# See https://bugs.launchpad.net/zc.buildout/+bug/164629
+script-initialization =
+  import pytest
+  if __name__ == '__main__': sys.exit(pytest.main())
+eggs = js.i18n
+       pytest
+
+[download]
+recipe = z3c.recipe.scripts
+eggs = js.i18n
+
+[scripts]

File js/__init__.py

+# Make this package a namespace package
+__import__('pkg_resources').declare_namespace(__name__)

File js/i18n/__init__.py

+from fanstatic import Library, Resource
+
+# This code is auto-generated and not PEP8 compliant
+library = Library('i18n', 'resources')
+
+i18n = Resource(library, 'i18n.js', minified='i18n.min.js')

File js/i18n/resources/i18n.js

+// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
+if (!Array.prototype.indexOf) {
+  Array.prototype.indexOf = function(searchElement /*, fromIndex */) {
+    "use strict";
+
+    if (this === void 0 || this === null) {
+      throw new TypeError();
+    }
+
+    var t = Object(this);
+    var len = t.length >>> 0;
+
+    if (len === 0) {
+      return -1;
+    }
+
+    var n = 0;
+    if (arguments.length > 0) {
+      n = Number(arguments[1]);
+      if (n !== n) { // shortcut for verifying if it's NaN
+        n = 0;
+      } else if (n !== 0 && n !== (Infinity) && n !== -(Infinity)) {
+        n = (n > 0 || -1) * Math.floor(Math.abs(n));
+      }
+    }
+
+    if (n >= len) {
+      return -1;
+    }
+
+    var k = n >= 0
+          ? n
+          : Math.max(len - Math.abs(n), 0);
+
+    for (; k < len; k++) {
+      if (k in t && t[k] === searchElement) {
+        return k;
+      }
+    }
+
+    return -1;
+  };
+}
+
+// Instantiate the object
+var I18n = I18n || {};
+
+// Set default locale to english
+I18n.defaultLocale = "en";
+
+// Set default handling of translation fallbacks to false
+I18n.fallbacks = false;
+
+// Set default separator
+I18n.defaultSeparator = ".";
+
+// Set current locale to null
+I18n.locale = null;
+
+// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
+I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;
+
+I18n.fallbackRules = {
+};
+
+I18n.pluralizationRules = {
+  en: function (n) {
+    return n == 0 ? ["zero", "none", "other"] : n == 1 ? "one" : "other";
+  }
+};
+
+I18n.getFallbacks = function(locale) {
+  if (locale === I18n.defaultLocale) {
+    return [];
+  } else if (!I18n.fallbackRules[locale]) {
+    var rules = []
+      , components = locale.split("-");
+
+    for (var l = 1; l < components.length; l++) {
+      rules.push(components.slice(0, l).join("-"));
+    }
+
+    rules.push(I18n.defaultLocale);
+
+    I18n.fallbackRules[locale] = rules;
+  }
+
+  return I18n.fallbackRules[locale];
+}
+
+I18n.isValidNode = function(obj, node, undefined) {
+  return obj[node] !== null && obj[node] !== undefined;
+};
+
+I18n.lookup = function(scope, options) {
+  var options = options || {}
+    , lookupInitialScope = scope
+    , translations = this.prepareOptions(I18n.translations)
+    , locale = options.locale || I18n.currentLocale()
+    , messages = translations[locale] || {}
+    , options = this.prepareOptions(options)
+    , currentScope
+  ;
+
+  if (typeof(scope) == "object") {
+    scope = scope.join(this.defaultSeparator);
+  }
+
+  if (options.scope) {
+    scope = options.scope.toString() + this.defaultSeparator + scope;
+  }
+
+  scope = scope.split(this.defaultSeparator);
+
+  while (messages && scope.length > 0) {
+    currentScope = scope.shift();
+    messages = messages[currentScope];
+  }
+
+  if (!messages) {
+    if (I18n.fallbacks) {
+      var fallbacks = this.getFallbacks(locale);
+      for (var fallback = 0; fallback < fallbacks.length; fallbacks++) {
+        messages = I18n.lookup(lookupInitialScope, this.prepareOptions({locale: fallbacks[fallback]}, options));
+        if (messages) {
+          break;
+        }
+      }
+    }
+
+    if (!messages && this.isValidNode(options, "defaultValue")) {
+        messages = options.defaultValue;
+    }
+  }
+
+  return messages;
+};
+
+// Merge serveral hash options, checking if value is set before
+// overwriting any value. The precedence is from left to right.
+//
+//   I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
+//   #=> {name: "John Doe", role: "user"}
+//
+I18n.prepareOptions = function() {
+  var options = {}
+    , opts
+    , count = arguments.length
+  ;
+
+  for (var i = 0; i < count; i++) {
+    opts = arguments[i];
+
+    if (!opts) {
+      continue;
+    }
+
+    for (var key in opts) {
+      if (!this.isValidNode(options, key)) {
+        options[key] = opts[key];
+      }
+    }
+  }
+
+  return options;
+};
+
+I18n.interpolate = function(message, options) {
+  options = this.prepareOptions(options);
+  var matches = message.match(this.PLACEHOLDER)
+    , placeholder
+    , value
+    , name
+  ;
+
+  if (!matches) {
+    return message;
+  }
+
+  for (var i = 0; placeholder = matches[i]; i++) {
+    name = placeholder.replace(this.PLACEHOLDER, "$1");
+
+    value = options[name];
+
+    if (!this.isValidNode(options, name)) {
+      value = "[missing " + placeholder + " value]";
+    }
+
+    regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
+    message = message.replace(regex, value);
+  }
+
+  return message;
+};
+
+I18n.translate = function(scope, options) {
+  options = this.prepareOptions(options);
+  var translation = this.lookup(scope, options);
+
+  try {
+    if (typeof(translation) == "object") {
+      if (typeof(options.count) == "number") {
+        return this.pluralize(options.count, scope, options);
+      } else {
+        return translation;
+      }
+    } else {
+      return this.interpolate(translation, options);
+    }
+  } catch(err) {
+    return this.missingTranslation(scope);
+  }
+};
+
+I18n.localize = function(scope, value) {
+  switch (scope) {
+    case "currency":
+      return this.toCurrency(value);
+    case "number":
+      scope = this.lookup("number.format");
+      return this.toNumber(value, scope);
+    case "percentage":
+      return this.toPercentage(value);
+    default:
+      if (scope.match(/^(date|time)/)) {
+        return this.toTime(scope, value);
+      } else {
+        return value.toString();
+      }
+  }
+};
+
+I18n.parseDate = function(date) {
+  var matches, convertedDate;
+
+  // we have a date, so just return it.
+  if (typeof(date) == "object") {
+    return date;
+  };
+
+  // it matches the following formats:
+  //   yyyy-mm-dd
+  //   yyyy-mm-dd[ T]hh:mm::ss
+  //   yyyy-mm-dd[ T]hh:mm::ss
+  //   yyyy-mm-dd[ T]hh:mm::ssZ
+  //   yyyy-mm-dd[ T]hh:mm::ss+0000
+  //
+  matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);
+
+  if (matches) {
+    for (var i = 1; i <= 6; i++) {
+      matches[i] = parseInt(matches[i], 10) || 0;
+    }
+
+    // month starts on 0
+    matches[2] -= 1;
+
+    if (matches[7]) {
+      convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));
+    } else {
+      convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]);
+    }
+  } else if (typeof(date) == "number") {
+    // UNIX timestamp
+    convertedDate = new Date();
+    convertedDate.setTime(date);
+  } else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
+    // a valid javascript format with timezone info
+    convertedDate = new Date();
+    convertedDate.setTime(Date.parse(date))
+  } else {
+    // an arbitrary javascript string
+    convertedDate = new Date();
+    convertedDate.setTime(Date.parse(date));
+  }
+
+  return convertedDate;
+};
+
+I18n.toTime = function(scope, d) {
+  var date = this.parseDate(d)
+    , format = this.lookup(scope)
+  ;
+
+  if (date.toString().match(/invalid/i)) {
+    return date.toString();
+  }
+
+  if (!format) {
+    return date.toString();
+  }
+
+  return this.strftime(date, format);
+};
+
+I18n.strftime = function(date, format) {
+  var options = this.lookup("date");
+
+  if (!options) {
+    return date.toString();
+  }
+
+  options.meridian = options.meridian || ["AM", "PM"];
+
+  var weekDay = date.getDay()
+    , day = date.getDate()
+    , year = date.getFullYear()
+    , month = date.getMonth() + 1
+    , hour = date.getHours()
+    , hour12 = hour
+    , meridian = hour > 11 ? 1 : 0
+    , secs = date.getSeconds()
+    , mins = date.getMinutes()
+    , offset = date.getTimezoneOffset()
+    , absOffsetHours = Math.floor(Math.abs(offset / 60))
+    , absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60)
+    , timezoneoffset = (offset > 0 ? "-" : "+") + (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) + (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes)
+  ;
+
+  if (hour12 > 12) {
+    hour12 = hour12 - 12;
+  } else if (hour12 === 0) {
+    hour12 = 12;
+  }
+
+  var padding = function(n) {
+    var s = "0" + n.toString();
+    return s.substr(s.length - 2);
+  };
+
+  var f = format;
+  f = f.replace("%a", options.abbr_day_names[weekDay]);
+  f = f.replace("%A", options.day_names[weekDay]);
+  f = f.replace("%b", options.abbr_month_names[month]);
+  f = f.replace("%B", options.month_names[month]);
+  f = f.replace("%d", padding(day));
+  f = f.replace("%e", day);
+  f = f.replace("%-d", day);
+  f = f.replace("%H", padding(hour));
+  f = f.replace("%-H", hour);
+  f = f.replace("%I", padding(hour12));
+  f = f.replace("%-I", hour12);
+  f = f.replace("%m", padding(month));
+  f = f.replace("%-m", month);
+  f = f.replace("%M", padding(mins));
+  f = f.replace("%-M", mins);
+  f = f.replace("%p", options.meridian[meridian]);
+  f = f.replace("%S", padding(secs));
+  f = f.replace("%-S", secs);
+  f = f.replace("%w", weekDay);
+  f = f.replace("%y", padding(year));
+  f = f.replace("%-y", padding(year).replace(/^0+/, ""));
+  f = f.replace("%Y", year);
+  f = f.replace("%z", timezoneoffset);
+
+  return f;
+};
+
+I18n.toNumber = function(number, options) {
+  options = this.prepareOptions(
+    options,
+    this.lookup("number.format"),
+    {precision: 3, separator: ".", delimiter: ",", strip_insignificant_zeros: false}
+  );
+
+  var negative = number < 0
+    , string = Math.abs(number).toFixed(options.precision).toString()
+    , parts = string.split(".")
+    , precision
+    , buffer = []
+    , formattedNumber
+  ;
+
+  number = parts[0];
+  precision = parts[1];
+
+  while (number.length > 0) {
+    buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
+    number = number.substr(0, number.length -3);
+  }
+
+  formattedNumber = buffer.join(options.delimiter);
+
+  if (options.precision > 0) {
+    formattedNumber += options.separator + parts[1];
+  }
+
+  if (negative) {
+    formattedNumber = "-" + formattedNumber;
+  }
+
+  if (options.strip_insignificant_zeros) {
+    var regex = {
+        separator: new RegExp(options.separator.replace(/\./, "\\.") + "$")
+      , zeros: /0+$/
+    };
+
+    formattedNumber = formattedNumber
+      .replace(regex.zeros, "")
+      .replace(regex.separator, "")
+    ;
+  }
+
+  return formattedNumber;
+};
+
+I18n.toCurrency = function(number, options) {
+  options = this.prepareOptions(
+    options,
+    this.lookup("number.currency.format"),
+    this.lookup("number.format"),
+    {unit: "$", precision: 2, format: "%u%n", delimiter: ",", separator: "."}
+  );
+
+  number = this.toNumber(number, options);
+  number = options.format
+    .replace("%u", options.unit)
+    .replace("%n", number)
+  ;
+
+  return number;
+};
+
+I18n.toHumanSize = function(number, options) {
+  var kb = 1024
+    , size = number
+    , iterations = 0
+    , unit
+    , precision
+  ;
+
+  while (size >= kb && iterations < 4) {
+    size = size / kb;
+    iterations += 1;
+  }
+
+  if (iterations === 0) {
+    unit = this.t("number.human.storage_units.units.byte", {count: size});
+    precision = 0;
+  } else {
+    unit = this.t("number.human.storage_units.units." + [null, "kb", "mb", "gb", "tb"][iterations]);
+    precision = (size - Math.floor(size) === 0) ? 0 : 1;
+  }
+
+  options = this.prepareOptions(
+    options,
+    {precision: precision, format: "%n%u", delimiter: ""}
+  );
+
+  number = this.toNumber(size, options);
+  number = options.format
+    .replace("%u", unit)
+    .replace("%n", number)
+  ;
+
+  return number;
+};
+
+I18n.toPercentage = function(number, options) {
+  options = this.prepareOptions(
+    options,
+    this.lookup("number.percentage.format"),
+    this.lookup("number.format"),
+    {precision: 3, separator: ".", delimiter: ""}
+  );
+
+  number = this.toNumber(number, options);
+  return number + "%";
+};
+
+I18n.pluralizer = function(locale) {
+  pluralizer = this.pluralizationRules[locale];
+  if (pluralizer !== undefined) return pluralizer;
+  return this.pluralizationRules["en"];
+};
+
+I18n.findAndTranslateValidNode = function(keys, translation) {
+  for (i = 0; i < keys.length; i++) {
+    key = keys[i];
+    if (this.isValidNode(translation, key)) return translation[key];
+  }
+  return null;
+};
+
+I18n.pluralize = function(count, scope, options) {
+  var translation;
+
+  try {
+    translation = this.lookup(scope, options);
+  } catch (error) {}
+
+  if (!translation) {
+    return this.missingTranslation(scope);
+  }
+
+  var message;
+  options = this.prepareOptions(options);
+  options.count = count.toString();
+
+  pluralizer = this.pluralizer(this.currentLocale());
+  key = pluralizer(Math.abs(count));
+  keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key];
+
+  message = this.findAndTranslateValidNode(keys, translation);
+  if (message == null) message = this.missingTranslation(scope, keys[0]);
+
+  return this.interpolate(message, options);
+};
+
+I18n.missingTranslation = function() {
+  var message = '[missing "' + this.currentLocale()
+    , count = arguments.length
+  ;
+
+  for (var i = 0; i < count; i++) {
+    message += "." + arguments[i];
+  }
+
+  message += '" translation]';
+
+  return message;
+};
+
+I18n.currentLocale = function() {
+  return (I18n.locale || I18n.defaultLocale);
+};
+
+// shortcuts
+I18n.t = I18n.translate;
+I18n.l = I18n.localize;
+I18n.p = I18n.pluralize;

File js/i18n/resources/i18n.min.js

+if(!Array.prototype.indexOf){Array.prototype.indexOf=function(c){if(this===void 0||this===null){throw new TypeError()}var d=Object(this);var a=d.length>>>0;if(a===0){return -1}var e=0;if(arguments.length>0){e=Number(arguments[1]);if(e!==e){e=0}else{if(e!==0&&e!==(Infinity)&&e!==-(Infinity)){e=(e>0||-1)*Math.floor(Math.abs(e))}}}if(e>=a){return -1}var b=e>=0?e:Math.max(a-Math.abs(e),0);for(;b<a;b++){if(b in d&&d[b]===c){return b}}return -1}}var I18n=I18n||{};I18n.defaultLocale="en";I18n.fallbacks=false;I18n.defaultSeparator=".";I18n.locale=null;I18n.PLACEHOLDER=/(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;I18n.fallbackRules={};I18n.pluralizationRules={en:function(a){return a==0?["zero","none","other"]:a==1?"one":"other"}};I18n.getFallbacks=function(a){if(a===I18n.defaultLocale){return[]}else{if(!I18n.fallbackRules[a]){var d=[],c=a.split("-");for(var b=1;b<c.length;b++){d.push(c.slice(0,b).join("-"))}d.push(I18n.defaultLocale);I18n.fallbackRules[a]=d}}return I18n.fallbackRules[a]};I18n.isValidNode=function(b,a,c){return b[a]!==null&&b[a]!==c};I18n.lookup=function(g,h){var h=h||{},d=g,b=this.prepareOptions(I18n.translations),f=h.locale||I18n.currentLocale(),a=b[f]||{},h=this.prepareOptions(h),j;if(typeof(g)=="object"){g=g.join(this.defaultSeparator)}if(h.scope){g=h.scope.toString()+this.defaultSeparator+g}g=g.split(this.defaultSeparator);while(a&&g.length>0){j=g.shift();a=a[j]}if(!a){if(I18n.fallbacks){var e=this.getFallbacks(f);for(var c=0;c<e.length;e++){a=I18n.lookup(d,this.prepareOptions({locale:e[c]},h));if(a){break}}}if(!a&&this.isValidNode(h,"defaultValue")){a=h.defaultValue}}return a};I18n.prepareOptions=function(){var a={},e,d=arguments.length;for(var c=0;c<d;c++){e=arguments[c];if(!e){continue}for(var b in e){if(!this.isValidNode(a,b)){a[b]=e[b]}}}return a};I18n.interpolate=function(d,b){b=this.prepareOptions(b);var f=d.match(this.PLACEHOLDER),g,e,a;if(!f){return d}for(var c=0;g=f[c];c++){a=g.replace(this.PLACEHOLDER,"$1");e=b[a];if(!this.isValidNode(b,a)){e="[missing "+g+" value]"}regex=new RegExp(g.replace(/\{/gm,"\\{").replace(/\}/gm,"\\}"));d=d.replace(regex,e)}return d};I18n.translate=function(b,a){a=this.prepareOptions(a);var d=this.lookup(b,a);try{if(typeof(d)=="object"){if(typeof(a.count)=="number"){return this.pluralize(a.count,b,a)}else{return d}}else{return this.interpolate(d,a)}}catch(c){return this.missingTranslation(b)}};I18n.localize=function(a,b){switch(a){case"currency":return this.toCurrency(b);case"number":a=this.lookup("number.format");return this.toNumber(b,a);case"percentage":return this.toPercentage(b);default:if(a.match(/^(date|time)/)){return this.toTime(a,b)}else{return b.toString()}}};I18n.parseDate=function(a){var d,c;if(typeof(a)=="object"){return a}d=a.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);if(d){for(var b=1;b<=6;b++){d[b]=parseInt(d[b],10)||0}d[2]-=1;if(d[7]){c=new Date(Date.UTC(d[1],d[2],d[3],d[4],d[5],d[6]))}else{c=new Date(d[1],d[2],d[3],d[4],d[5],d[6])}}else{if(typeof(a)=="number"){c=new Date();c.setTime(a)}else{if(a.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)){c=new Date();c.setTime(Date.parse(a))}else{c=new Date();c.setTime(Date.parse(a))}}}return c};I18n.toTime=function(b,e){var a=this.parseDate(e),c=this.lookup(b);if(a.toString().match(/invalid/i)){return a.toString()}if(!c){return a.toString()}return this.strftime(a,c)};I18n.strftime=function(r,o){var b=this.lookup("date");if(!b){return r.toString()}b.meridian=b.meridian||["AM","PM"];var l=r.getDay(),n=r.getDate(),k=r.getFullYear(),s=r.getMonth()+1,g=r.getHours(),h=g,q=g>11?1:0,d=r.getSeconds(),j=r.getMinutes(),e=r.getTimezoneOffset(),c=Math.floor(Math.abs(e/60)),a=Math.abs(e)-(c*60),t=(e>0?"-":"+")+(c.toString().length<2?"0"+c:c)+(a.toString().length<2?"0"+a:a);if(h>12){h=h-12}else{if(h===0){h=12}}var m=function(u){var f="0"+u.toString();return f.substr(f.length-2)};var p=o;p=p.replace("%a",b.abbr_day_names[l]);p=p.replace("%A",b.day_names[l]);p=p.replace("%b",b.abbr_month_names[s]);p=p.replace("%B",b.month_names[s]);p=p.replace("%d",m(n));p=p.replace("%e",n);p=p.replace("%-d",n);p=p.replace("%H",m(g));p=p.replace("%-H",g);p=p.replace("%I",m(h));p=p.replace("%-I",h);p=p.replace("%m",m(s));p=p.replace("%-m",s);p=p.replace("%M",m(j));p=p.replace("%-M",j);p=p.replace("%p",b.meridian[q]);p=p.replace("%S",m(d));p=p.replace("%-S",d);p=p.replace("%w",l);p=p.replace("%y",m(k));p=p.replace("%-y",m(k).replace(/^0+/,""));p=p.replace("%Y",k);p=p.replace("%z",t);return p};I18n.toNumber=function(a,j){j=this.prepareOptions(j,this.lookup("number.format"),{precision:3,separator:".",delimiter:",",strip_insignificant_zeros:false});var b=a<0,g=Math.abs(a).toFixed(j.precision).toString(),c=g.split("."),e,d=[],f;a=c[0];e=c[1];while(a.length>0){d.unshift(a.substr(Math.max(0,a.length-3),3));a=a.substr(0,a.length-3)}f=d.join(j.delimiter);if(j.precision>0){f+=j.separator+c[1]}if(b){f="-"+f}if(j.strip_insignificant_zeros){var h={separator:new RegExp(j.separator.replace(/\./,"\\.")+"$"),zeros:/0+$/};f=f.replace(h.zeros,"").replace(h.separator,"")}return f};I18n.toCurrency=function(b,a){a=this.prepareOptions(a,this.lookup("number.currency.format"),this.lookup("number.format"),{unit:"$",precision:2,format:"%u%n",delimiter:",",separator:"."});b=this.toNumber(b,a);b=a.format.replace("%u",a.unit).replace("%n",b);return b};I18n.toHumanSize=function(g,b){var d=1024,c=g,f=0,e,a;while(c>=d&&f<4){c=c/d;f+=1}if(f===0){e=this.t("number.human.storage_units.units.byte",{count:c});a=0}else{e=this.t("number.human.storage_units.units."+[null,"kb","mb","gb","tb"][f]);a=(c-Math.floor(c)===0)?0:1}b=this.prepareOptions(b,{precision:a,format:"%n%u",delimiter:""});g=this.toNumber(c,b);g=b.format.replace("%u",e).replace("%n",g);return g};I18n.toPercentage=function(b,a){a=this.prepareOptions(a,this.lookup("number.percentage.format"),this.lookup("number.format"),{precision:3,separator:".",delimiter:""});b=this.toNumber(b,a);return b+"%"};I18n.pluralizer=function(a){pluralizer=this.pluralizationRules[a];if(pluralizer!==undefined){return pluralizer}return this.pluralizationRules.en};I18n.findAndTranslateValidNode=function(a,b){for(i=0;i<a.length;i++){key=a[i];if(this.isValidNode(b,key)){return b[key]}}return null};I18n.pluralize=function(e,c,b){var f;try{f=this.lookup(c,b)}catch(a){}if(!f){return this.missingTranslation(c)}var d;b=this.prepareOptions(b);b.count=e.toString();pluralizer=this.pluralizer(this.currentLocale());key=pluralizer(Math.abs(e));keys=((typeof key=="object")&&(key instanceof Array))?key:[key];d=this.findAndTranslateValidNode(keys,f);if(d==null){d=this.missingTranslation(c,keys[0])}return this.interpolate(d,b)};I18n.missingTranslation=function(){var c='[missing "'+this.currentLocale(),b=arguments.length;for(var a=0;a<b;a++){c+="."+arguments[a]}c+='" translation]';return c};I18n.currentLocale=function(){return(I18n.locale||I18n.defaultLocale)};I18n.t=I18n.translate;I18n.l=I18n.localize;I18n.p=I18n.pluralize;

File js/i18n/test_i18n.txt

+How to use?
+===========
+
+You can import i18n-js from ``js.i18n``
+and ``need`` it where you want these resources to be included on a page::
+
+  >>> from js.i18n import i18n
+  >>> i18n.need()
+from setuptools import setup, find_packages
+import os
+
+version = '0.0.0'
+
+def read(*rnames):
+    return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = (
+    read('README.txt')
+    + '\n' +
+    read('js', 'i18n', 'test_i18n.txt')
+    + '\n' +
+    read('CHANGES.txt'))
+
+setup(
+    name='js.i18n',
+    version=version,
+    description="fanstatic i18n-js bundle",
+    long_description=long_description,
+    classifiers=[],
+    keywords='',
+    author='Moriyoshi Koizumi',
+    author_email='mozo@mozo.jp',
+    license='MIT',
+    packages=find_packages(),
+    namespace_packages=['js'],
+    include_package_data=True,
+    zip_safe=False,
+    install_requires=[
+        'fanstatic',
+        ],
+    entry_points={
+        'fanstatic.libraries': [
+            'i18n = js.i18n:library',
+            ],
+        #'console_scripts': [
+        #    'download_jqueryui = js.jqueryui.download:main'],
+        },
+    )