cirandas.net

commit d7366d1f42d2f09b8065730c66792a8e0b68e51c

Author: Pedro Lucas Porcellis <porcellis@eletrotupi.com>

Update i18n lib with upstream

 public/javascripts/i18n.js | 386 +++++++++++++++++++++++----------------


diff --git a/public/javascripts/i18n.js b/public/javascripts/i18n.js
index e3ada2bbd99c42b4ea3f4dd24d6b515dddc75809..890192d52f29cb73133545e201238804ad1540f7 100644
--- a/public/javascripts/i18n.js
+++ b/public/javascripts/i18n.js
@@ -52,7 +52,17 @@   // Is a given variable an object?
   // Borrowed from Underscore.js
   var isObject = function(obj) {
     var type = typeof obj;
-    return type === 'function' || type === 'object' && !!obj;
+    return type === 'function' || type === 'object'
+  };
+
+  var isFunction = function(func) {
+    var type = typeof func;
+    return type === 'function'
+  };
+
+  // Check if value is different than undefined and null;
+  var isSet = function(value) {
+    return typeof(value) !== 'undefined' && value !== null;
   };
 
   // Is a given value an array?
@@ -60,22 +70,26 @@   // Borrowed from Underscore.js
   var isArray = function(val) {
     if (Array.isArray) {
       return Array.isArray(val);
-    };
+    }
     return Object.prototype.toString.call(val) === '[object Array]';
   };
 
   var isString = function(val) {
-    return typeof value == 'string' || Object.prototype.toString.call(val) === '[object String]';
+    return typeof val === 'string' || Object.prototype.toString.call(val) === '[object String]';
   };
 
   var isNumber = function(val) {
-    return typeof val == 'number' || Object.prototype.toString.call(val) === '[object Number]';
+    return typeof val === 'number' || Object.prototype.toString.call(val) === '[object Number]';
   };
 
   var isBoolean = function(val) {
     return val === true || val === false;
   };
 
+  var isNull = function(val) {
+    return val === null;
+  };
+
   var decimalAdjust = function(type, value, exp) {
     // If the exp is undefined or zero...
     if (typeof exp === 'undefined' || +exp === 0) {
@@ -93,13 +107,21 @@     value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
     // Shift back
     value = value.toString().split('e');
     return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
-  }
+  };
+
+  var lazyEvaluate = function(message, scope) {
+    if (isFunction(message)) {
+      return message(scope);
+    } else {
+      return message;
+    }
+  };
 
   var merge = function (dest, obj) {
     var key, value;
     for (key in obj) if (obj.hasOwnProperty(key)) {
       value = obj[key];
-      if (isString(value) || isNumber(value) || isBoolean(value)) {
+      if (isString(value) || isNumber(value) || isBoolean(value) || isArray(value) || isNull(value)) {
         dest[key] = value;
       } else {
         if (dest[key] == null) dest[key] = {};
@@ -173,60 +195,21 @@     // guessed string by setting the value here. By default, no prefix!
     , missingTranslationPrefix: ''
   };
 
+  // Set default locale. This locale will be used when fallback is enabled and
+  // the translation doesn't exist in a particular locale.
   I18n.reset = function() {
-    // Set default locale. This locale will be used when fallback is enabled and
-    // the translation doesn't exist in a particular locale.
-    this.defaultLocale = DEFAULT_OPTIONS.defaultLocale;
-
-    // Set the current locale to `en`.
-    this.locale = DEFAULT_OPTIONS.locale;
-
-    // Set the translation key separator.
-    this.defaultSeparator = DEFAULT_OPTIONS.defaultSeparator;
-
-    // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
-    this.placeholder = DEFAULT_OPTIONS.placeholder;
-
-    // Set if engine should fallback to the default locale when a translation
-    // is missing.
-    this.fallbacks = DEFAULT_OPTIONS.fallbacks;
-
-    // Set the default translation object.
-    this.translations = DEFAULT_OPTIONS.translations;
-
-    // Set the default missing behaviour
-    this.missingBehaviour = DEFAULT_OPTIONS.missingBehaviour;
-
-    // Set the default missing string prefix for guess behaviour
-    this.missingTranslationPrefix = DEFAULT_OPTIONS.missingTranslationPrefix;
-
+    var key;
+    for (key in DEFAULT_OPTIONS) {
+      this[key] = DEFAULT_OPTIONS[key];
+    }
   };
 
   // Much like `reset`, but only assign options if not already assigned
   I18n.initializeOptions = function() {
-    if (typeof(this.defaultLocale) === "undefined" && this.defaultLocale !== null)
-      this.defaultLocale = DEFAULT_OPTIONS.defaultLocale;
-
-    if (typeof(this.locale) === "undefined" && this.locale !== null)
-      this.locale = DEFAULT_OPTIONS.locale;
-
-    if (typeof(this.defaultSeparator) === "undefined" && this.defaultSeparator !== null)
-      this.defaultSeparator = DEFAULT_OPTIONS.defaultSeparator;
-
-    if (typeof(this.placeholder) === "undefined" && this.placeholder !== null)
-      this.placeholder = DEFAULT_OPTIONS.placeholder;
-
-    if (typeof(this.fallbacks) === "undefined" && this.fallbacks !== null)
-      this.fallbacks = DEFAULT_OPTIONS.fallbacks;
-
-    if (typeof(this.translations) === "undefined" && this.translations !== null)
-      this.translations = DEFAULT_OPTIONS.translations;
-
-    if (typeof(this.missingBehaviour) === "undefined" && this.missingBehaviour !== null)
-      this.missingBehaviour = DEFAULT_OPTIONS.missingBehaviour;
-
-    if (typeof(this.missingTranslationPrefix) === "undefined" && this.missingTranslationPrefix !== null)
-      this.missingTranslationPrefix = DEFAULT_OPTIONS.missingTranslationPrefix;
+    var key;
+    for (key in DEFAULT_OPTIONS) if (!isSet(this[key])) {
+      this[key] = DEFAULT_OPTIONS[key];
+    }
   };
   I18n.initializeOptions();
 
@@ -252,7 +235,7 @@   // I18n's detection.
   I18n.locales.get = function(locale) {
     var result = this[locale] || this[I18n.locale] || this["default"];
 
-    if (typeof(result) === "function") {
+    if (isFunction(result)) {
       result = result(locale);
     }
 
@@ -267,8 +250,6 @@   // The default locale list.
   I18n.locales["default"] = function(locale) {
     var locales = []
       , list = []
-      , countryCode
-      , count
     ;
 
     // Handle the inline locale option that can be provided to
@@ -287,19 +268,85 @@     if (I18n.fallbacks && I18n.defaultLocale) {
       locales.push(I18n.defaultLocale);
     }
 
+    // Locale code format 1:
+    // According to RFC4646 (http://www.ietf.org/rfc/rfc4646.txt)
+    // language codes for Traditional Chinese should be `zh-Hant`
+    //
+    // But due to backward compatibility
+    // We use older version of IETF language tag
+    // @see http://www.w3.org/TR/html401/struct/dirlang.html
+    // @see http://en.wikipedia.org/wiki/IETF_language_tag
+    //
+    // Format: `language-code = primary-code ( "-" subcode )*`
+    //
+    // primary-code uses ISO639-1
+    // @see http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
+    // @see http://www.iso.org/iso/home/standards/language_codes.htm
+    //
+    // subcode uses ISO 3166-1 alpha-2
+    // @see http://en.wikipedia.org/wiki/ISO_3166
+    // @see http://www.iso.org/iso/country_codes.htm
+    //
+    // @note
+    //   subcode can be in upper case or lower case
+    //   defining it in upper case is a convention only
+
+
+    // Locale code format 2:
+    // Format: `code = primary-code ( "-" region-code )*`
+    // primary-code uses ISO 639-1
+    // script-code uses ISO 15924
+    // region-code uses ISO 3166-1 alpha-2
+    // Example: zh-Hant-TW, en-HK, zh-Hant-CN
+    //
+    // It is similar to RFC4646 (or actually the same),
+    // but seems to be limited to language, script, region
+
     // Compute each locale with its country code.
-    // So this will return an array containing both
-    // `de-DE` and `de` locales.
-    locales.forEach(function(locale){
-      countryCode = locale.split("-")[0];
+    // So this will return an array containing
+    // `de-DE` and `de`
+    // or
+    // `zh-hans-tw`, `zh-hans`, `zh`
+    // locales.
+    locales.forEach(function(locale) {
+      var localeParts = locale.split("-");
+      var firstFallback = null;
+      var secondFallback = null;
+      if (localeParts.length === 3) {
+        firstFallback = [
+          localeParts[0],
+          localeParts[1]
+        ].join("-");
+        secondFallback = localeParts[0];
+      }
+      else if (localeParts.length === 2) {
+        firstFallback = localeParts[0];
+      }
 
-      if (!~list.indexOf(locale)) {
+      if (list.indexOf(locale) === -1) {
         list.push(locale);
       }
 
-      if (I18n.fallbacks && countryCode && countryCode !== locale && !~list.indexOf(countryCode)) {
-        list.push(countryCode);
+      if (! I18n.fallbacks) {
+        return;
       }
+
+      [
+        firstFallback,
+        secondFallback
+      ].forEach(function(nullableFallbackLocale) {
+        // We don't want null values
+        if (typeof nullableFallbackLocale === "undefined") { return; }
+        if (nullableFallbackLocale === null) { return; }
+        // We don't want duplicate values
+        //
+        // Comparing with `locale` first is faster than
+        // checking whether value's presence in the list
+        if (nullableFallbackLocale === locale) { return; }
+        if (list.indexOf(nullableFallbackLocale) !== -1) { return; }
+
+        list.push(nullableFallbackLocale);
+      });
     });
 
     // No locales set? English it is.
@@ -336,28 +383,26 @@     return this.locale || this.defaultLocale;
   };
 
   // Check if value is different than undefined and null;
-  I18n.isSet = function(value) {
-    return value !== undefined && value !== null;
-  };
+  I18n.isSet = isSet;
 
   // Find and process the translation using the provided scope and options.
   // This is used internally by some functions and should not be used as an
   // public API.
   I18n.lookup = function(scope, options) {
-    options = this.prepareOptions(options);
+    options = options || {};
 
     var locales = this.locales.get(options.locale).slice()
-      , requestedLocale = locales[0]
       , locale
       , scopes
+      , fullScope
       , translations
     ;
 
-    scope = this.getFullScope(scope, options);
+    fullScope = this.getFullScope(scope, options);
 
     while (locales.length) {
       locale = locales.shift();
-      scopes = scope.split(this.defaultSeparator);
+      scopes = fullScope.split(options.separator || this.defaultSeparator);
       translations = this.translations[locale];
 
       if (!translations) {
@@ -376,8 +421,8 @@         return translations;
       }
     }
 
-    if (this.isSet(options.defaultValue)) {
-      return options.defaultValue;
+    if (isSet(options.defaultValue)) {
+      return lazyEvaluate(options.defaultValue, scope);
     }
   };
 
@@ -391,7 +436,7 @@
     if (isObject(translations)) {
       while (pluralizerKeys.length) {
         pluralizerKey = pluralizerKeys.shift();
-        if (this.isSet(translations[pluralizerKey])) {
+        if (isSet(translations[pluralizerKey])) {
           message = translations[pluralizerKey];
           break;
         }
@@ -403,9 +448,8 @@   };
 
   // Lookup dedicated to pluralization
   I18n.pluralizationLookup = function(count, scope, options) {
-    options = this.prepareOptions(options);
+    options = options || {};
     var locales = this.locales.get(options.locale).slice()
-      , requestedLocale = locales[0]
       , locale
       , scopes
       , translations
@@ -415,7 +459,7 @@     scope = this.getFullScope(scope, options);
 
     while (locales.length) {
       locale = locales.shift();
-      scopes = scope.split(this.defaultSeparator);
+      scopes = scope.split(options.separator || this.defaultSeparator);
       translations = this.translations[locale];
 
       if (!translations) {
@@ -427,17 +471,17 @@         translations = translations[scopes.shift()];
         if (!isObject(translations)) {
           break;
         }
-        if (scopes.length == 0) {
+        if (scopes.length === 0) {
           message = this.pluralizationLookupWithoutFallback(count, locale, translations);
         }
       }
-      if (message != null && message != undefined) {
+      if (typeof message !== "undefined" && message !== null) {
         break;
       }
     }
 
-    if (message == null || message == undefined) {
-      if (this.isSet(options.defaultValue)) {
+    if (typeof message === "undefined" || message === null) {
+      if (isSet(options.defaultValue)) {
         if (isObject(options.defaultValue)) {
           message = this.pluralizationLookupWithoutFallback(count, options.locale, options.defaultValue);
         } else {
@@ -492,7 +536,7 @@         if (!subject.hasOwnProperty(attr)) {
           continue;
         }
 
-        if (this.isSet(options[attr])) {
+        if (isSet(options[attr])) {
           continue;
         }
 
@@ -511,15 +555,14 @@     var translationOptions = [{scope: scope}];
 
     // Defaults should be an array of hashes containing either
     // fallback scopes or messages
-    if (this.isSet(options.defaults)) {
+    if (isSet(options.defaults)) {
       translationOptions = translationOptions.concat(options.defaults);
     }
 
     // Maintain support for defaultValue. Since it is always a message
     // insert it in to the translation options as such.
-    if (this.isSet(options.defaultValue)) {
+    if (isSet(options.defaultValue)) {
       translationOptions.push({ message: options.defaultValue });
-      delete options.defaultValue;
     }
 
     return translationOptions;
@@ -527,20 +570,25 @@   };
 
   // Translate the given scope with the provided options.
   I18n.translate = function(scope, options) {
-    options = this.prepareOptions(options);
+    options = options || {};
 
-    var copiedOptions = this.prepareOptions(options);
     var translationOptions = this.createTranslationOptions(scope, options);
 
     var translation;
+    var usedScope = scope;
+
+    var optionsWithoutDefault = this.prepareOptions(options)
+    delete optionsWithoutDefault.defaultValue
+
     // Iterate through the translation options until a translation
     // or message is found.
     var translationFound =
       translationOptions.some(function(translationOption) {
-        if (this.isSet(translationOption.scope)) {
-          translation = this.lookup(translationOption.scope, options);
-        } else if (this.isSet(translationOption.message)) {
-          translation = translationOption.message;
+        if (isSet(translationOption.scope)) {
+          usedScope = translationOption.scope;
+          translation = this.lookup(usedScope, optionsWithoutDefault);
+        } else if (isSet(translationOption.message)) {
+          translation = lazyEvaluate(translationOption.message, scope);
         }
 
         if (translation !== undefined && translation !== null) {
@@ -554,8 +602,12 @@     }
 
     if (typeof(translation) === "string") {
       translation = this.interpolate(translation, options);
-    } else if (isObject(translation) && this.isSet(options.count)) {
-      translation = this.pluralize(options.count, scope, copiedOptions);
+    } else if (isArray(translation)) {
+      translation = translation.map(function(t) {
+        return (typeof(t) === "string" ? this.interpolate(t, options) : t);
+      }, this);
+    } else if (isObject(translation) && isSet(options.count)) {
+      translation = this.pluralize(options.count, usedScope, options);
     }
 
     return translation;
@@ -563,7 +615,11 @@   };
 
   // This function interpolates the all variables in the given message.
   I18n.interpolate = function(message, options) {
-    options = this.prepareOptions(options);
+    if (message == null) {
+      return message;
+    }
+
+    options = options || {};
     var matches = message.match(this.placeholder)
       , placeholder
       , value
@@ -574,14 +630,12 @@
     if (!matches) {
       return message;
     }
-
-    var value;
 
     while (matches.length) {
       placeholder = matches.shift();
       name = placeholder.replace(this.placeholder, "$1");
 
-      if (this.isSet(options[name])) {
+      if (isSet(options[name])) {
         value = options[name].toString().replace(/\$/gm, "_#$#_");
       } else if (name in options) {
         value = this.nullPlaceholder(placeholder, message, options);
@@ -589,7 +643,7 @@       } else {
         value = this.missingPlaceholder(placeholder, message, options);
       }
 
-      regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
+      regex = new RegExp(placeholder.replace(/{/gm, "\\{").replace(/}/gm, "\\}"));
       message = message.replace(regex, value);
     }
 
@@ -600,17 +654,15 @@   // Pluralize the given scope using the `count` value.
   // The pluralized translation may have other placeholders,
   // which will be retrieved from `options`.
   I18n.pluralize = function(count, scope, options) {
-    options = this.prepareOptions(options);
-    var pluralizer, message, result;
+    options = this.prepareOptions({count: String(count)}, options)
+    var pluralizer, result;
 
     result = this.pluralizationLookup(count, scope, options);
-    if (result.translations == undefined || result.translations == null) {
+    if (typeof result.translations === "undefined" || result.translations == null) {
       return this.missingTranslation(scope, options);
     }
 
-    options.count = String(count);
-
-    if (result.message != undefined && result.message != null) {
+    if (typeof result.message !== "undefined" && result.message != null) {
       return this.interpolate(result.message, options);
     }
     else {
@@ -622,18 +674,18 @@
   // Return a missing translation message for the given parameters.
   I18n.missingTranslation = function(scope, options) {
     //guess intended string
-    if(this.missingBehaviour == 'guess'){
+    if(this.missingBehaviour === 'guess'){
       //get only the last portion of the scope
       var s = scope.split('.').slice(-1)[0];
       //replace underscore with space && camelcase with space and lowercase letter
       return (this.missingTranslationPrefix.length > 0 ? this.missingTranslationPrefix : '') +
-          s.replace('_',' ').replace(/([a-z])([A-Z])/g,
+          s.replace(/_/g,' ').replace(/([a-z])([A-Z])/g,
           function(match, p1, p2) {return p1 + ' ' + p2.toLowerCase()} );
     }
 
     var localeForTranslation = (options != null && options.locale != null) ? options.locale : this.currentLocale();
     var fullScope           = this.getFullScope(scope, options);
-    var fullScopeWithLocale = [localeForTranslation, fullScope].join(this.defaultSeparator);
+    var fullScopeWithLocale = [localeForTranslation, fullScope].join(options.separator || this.defaultSeparator);
 
     return '[missing "' + fullScopeWithLocale + '" translation]';
   };
@@ -727,8 +779,8 @@   //
   I18n.toCurrency = function(number, options) {
     options = this.prepareOptions(
         options
-      , this.lookup("number.currency.format")
-      , this.lookup("number.format")
+      , this.lookup("number.currency.format", options)
+      , this.lookup("number.format", options)
       , CURRENCY_FORMAT
     );
 
@@ -747,17 +799,17 @@     options || (options = {});
 
     switch (scope) {
       case "currency":
-        return this.toCurrency(value);
+        return this.toCurrency(value, options);
       case "number":
-        scope = this.lookup("number.format");
+        scope = this.lookup("number.format", options);
         return this.toNumber(value, scope);
       case "percentage":
-        return this.toPercentage(value);
+        return this.toPercentage(value, options);
       default:
         var localizedValue;
 
         if (scope.match(/^(date|time)/)) {
-          localizedValue = this.toTime(scope, value);
+          localizedValue = this.toTime(scope, value, options);
         } else {
           localizedValue = value.toString();
         }
@@ -781,10 +833,14 @@   //    yyyy-mm-dd[ T]hh:mm::ss.123Z
   //
   I18n.parseDate = function(date) {
     var matches, convertedDate, fraction;
+    // A date input of `null` or `undefined` will be returned as-is
+    if (date == null) {
+      return date;
+    }
     // we have a date, so just return it.
-    if (typeof(date) == "object") {
+    if (typeof(date) === "object") {
       return date;
-    };
+    }
 
     matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})([\.,]\d{1,3})?)?(Z|\+00:?00)?/);
 
@@ -833,32 +889,33 @@   // directive will be passed through to the output string.
   //
   // The accepted formats are:
   //
-  //     %a  - The abbreviated weekday name (Sun)
-  //     %A  - The full weekday name (Sunday)
-  //     %b  - The abbreviated month name (Jan)
-  //     %B  - The full month name (January)
-  //     %c  - The preferred local date and time representation
-  //     %d  - Day of the month (01..31)
-  //     %-d - Day of the month (1..31)
-  //     %H  - Hour of the day, 24-hour clock (00..23)
-  //     %-H - Hour of the day, 24-hour clock (0..23)
-  //     %I  - Hour of the day, 12-hour clock (01..12)
-  //     %-I - Hour of the day, 12-hour clock (1..12)
-  //     %m  - Month of the year (01..12)
-  //     %-m - Month of the year (1..12)
-  //     %M  - Minute of the hour (00..59)
-  //     %-M - Minute of the hour (0..59)
-  //     %p  - Meridian indicator (AM  or  PM)
-  //     %S  - Second of the minute (00..60)
-  //     %-S - Second of the minute (0..60)
-  //     %w  - Day of the week (Sunday is 0, 0..6)
-  //     %y  - Year without a century (00..99)
-  //     %-y - Year without a century (0..99)
-  //     %Y  - Year with century
-  //     %z  - Timezone offset (+0545)
+  //     %a     - The abbreviated weekday name (Sun)
+  //     %A     - The full weekday name (Sunday)
+  //     %b     - The abbreviated month name (Jan)
+  //     %B     - The full month name (January)
+  //     %c     - The preferred local date and time representation
+  //     %d     - Day of the month (01..31)
+  //     %-d    - Day of the month (1..31)
+  //     %H     - Hour of the day, 24-hour clock (00..23)
+  //     %-H/%k - Hour of the day, 24-hour clock (0..23)
+  //     %I     - Hour of the day, 12-hour clock (01..12)
+  //     %-I/%l - Hour of the day, 12-hour clock (1..12)
+  //     %m     - Month of the year (01..12)
+  //     %-m    - Month of the year (1..12)
+  //     %M     - Minute of the hour (00..59)
+  //     %-M    - Minute of the hour (0..59)
+  //     %p     - Meridian indicator (AM  or  PM)
+  //     %P     - Meridian indicator (am  or  pm)
+  //     %S     - Second of the minute (00..60)
+  //     %-S    - Second of the minute (0..60)
+  //     %w     - Day of the week (Sunday is 0, 0..6)
+  //     %y     - Year without a century (00..99)
+  //     %-y    - Year without a century (0..99)
+  //     %Y     - Year with century
+  //     %z/%Z  - Timezone offset (+0545)
   //
-  I18n.strftime = function(date, format) {
-    var options = this.lookup("date")
+  I18n.strftime = function(date, format, options) {
+    var options = this.lookup("date", options)
       , meridianOptions = I18n.meridian()
     ;
 
@@ -904,13 +961,16 @@     format = format.replace("%e", day);
     format = format.replace("%-d", day);
     format = format.replace("%H", padding(hour));
     format = format.replace("%-H", hour);
+    format = format.replace("%k", hour);
     format = format.replace("%I", padding(hour12));
     format = format.replace("%-I", hour12);
+    format = format.replace("%l", hour12);
     format = format.replace("%m", padding(month));
     format = format.replace("%-m", month);
     format = format.replace("%M", padding(mins));
     format = format.replace("%-M", mins);
     format = format.replace("%p", meridianOptions[meridian]);
+    format = format.replace("%P", meridianOptions[meridian].toLowerCase());
     format = format.replace("%S", padding(secs));
     format = format.replace("%-S", secs);
     format = format.replace("%w", weekDay);
@@ -918,33 +978,40 @@     format = format.replace("%y", padding(year));
     format = format.replace("%-y", padding(year).replace(/^0+/, ""));
     format = format.replace("%Y", year);
     format = format.replace("%z", timezoneoffset);
+    format = format.replace("%Z", timezoneoffset);
 
     return format;
   };
 
   // Convert the given dateString into a formatted date.
-  I18n.toTime = function(scope, dateString) {
+  I18n.toTime = function(scope, dateString, options) {
     var date = this.parseDate(dateString)
-      , format = this.lookup(scope)
+      , format = this.lookup(scope, options)
     ;
 
-    if (date.toString().match(/invalid/i)) {
-      return date.toString();
+    // A date input of `null` or `undefined` will be returned as-is
+    if (date == null) {
+      return date;
+    }
+
+    var date_string = date.toString()
+    if (date_string.match(/invalid/i)) {
+      return date_string;
     }
 
     if (!format) {
-      return date.toString();
+      return date_string;
     }
 
-    return this.strftime(date, format);
+    return this.strftime(date, format, options);
   };
 
   // Convert a number into a formatted percentage value.
   I18n.toPercentage = function(number, options) {
     options = this.prepareOptions(
         options
-      , this.lookup("number.percentage.format")
-      , this.lookup("number.format")
+      , this.lookup("number.percentage.format", options)
+      , this.lookup("number.format", options)
       , PERCENTAGE_FORMAT
     );
 
@@ -958,6 +1025,7 @@       , size = number
       , iterations = 0
       , unit
       , precision
+      , fullScope
     ;
 
     while (size >= kb && iterations < 4) {
@@ -966,10 +1034,12 @@       iterations += 1;
     }
 
     if (iterations === 0) {
-      unit = this.t("number.human.storage_units.units.byte", {count: size});
+      fullScope = this.getFullScope("number.human.storage_units.units.byte", options);
+      unit = this.t(fullScope, {count: size});
       precision = 0;
     } else {
-      unit = this.t("number.human.storage_units.units." + SIZE_UNITS[iterations]);
+      fullScope = this.getFullScope("number.human.storage_units.units." + SIZE_UNITS[iterations], options);
+      unit = this.t(fullScope);
       precision = (size - Math.floor(size) === 0) ? 0 : 1;
     }
 
@@ -982,11 +1052,11 @@     return this.toNumber(size, options);
   };
 
   I18n.getFullScope = function(scope, options) {
-    options = this.prepareOptions(options);
+    options = options || {};
 
     // Deal with the scope as an array.
-    if (scope.constructor === Array) {
-      scope = scope.join(this.defaultSeparator);
+    if (isArray(scope)) {
+      scope = scope.join(options.separator || this.defaultSeparator);
     }
 
     // Deal with the scope option provided through the second argument.
@@ -994,7 +1064,7 @@     //
     //    I18n.t('hello', {scope: 'greetings'});
     //
     if (options.scope) {
-      scope = [options.scope, scope].join(this.defaultSeparator);
+      scope = [options.scope, scope].join(options.separator || this.defaultSeparator);
     }
 
     return scope;
@@ -1017,9 +1087,9 @@     return merge(obj1, obj2);
   };
 
   // Set aliases, so we can save some typing.
-  I18n.t = I18n.translate;
-  I18n.l = I18n.localize;
-  I18n.p = I18n.pluralize;
+  I18n.t = I18n.translate.bind(I18n);
+  I18n.l = I18n.localize.bind(I18n);
+  I18n.p = I18n.pluralize.bind(I18n);
 
   return I18n;
 }));