app/assets/javascripts/i18n.js in i18n-js-3.0.0 vs app/assets/javascripts/i18n.js in i18n-js-3.0.1
- old
+ new
@@ -50,13 +50,23 @@
// 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?
// Borrowed from Underscore.js
var isArray = function(val) {
if (Array.isArray) {
return Array.isArray(val);
@@ -93,10 +103,18 @@
// 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)) {
@@ -171,64 +189,25 @@
// string is actually missing for testing purposes, you can prefix the
// 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();
// Return a list of all locales that must be tried before returning the
// missing translation message. By default, this will consider the inline option,
@@ -250,11 +229,11 @@
// Retrieve locales based on inline locale, current locale or default to
// 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);
}
if (isArray(result) === false) {
result = [result];
@@ -265,12 +244,10 @@
// The default locale list.
I18n.locales["default"] = function(locale) {
var locales = []
, list = []
- , countryCode
- , count
;
// Handle the inline locale option that can be provided to
// the `I18n.t` options.
if (locale) {
@@ -285,23 +262,89 @@
// Add the default locale if fallback strategy is enabled.
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];
+ secondFallback = [
+ localeParts[0],
+ localeParts[1]
+ ].join("-");
+ }
+ 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.
if (!locales.length) {
locales.push("en");
@@ -334,32 +377,31 @@
I18n.currentLocale = function() {
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(this.defaultSeparator);
translations = this.translations[locale];
if (!translations) {
continue;
}
@@ -374,12 +416,12 @@
if (translations !== undefined && translations !== null) {
return translations;
}
}
- if (this.isSet(options.defaultValue)) {
- return options.defaultValue;
+ if (isSet(options.defaultValue)) {
+ return lazyEvaluate(options.defaultValue, scope);
}
};
// lookup pluralization rule key into translations
I18n.pluralizationLookupWithoutFallback = function(count, locale, translations) {
@@ -389,11 +431,11 @@
, message;
if (isObject(translations)) {
while (pluralizerKeys.length) {
pluralizerKey = pluralizerKeys.shift();
- if (this.isSet(translations[pluralizerKey])) {
+ if (isSet(translations[pluralizerKey])) {
message = translations[pluralizerKey];
break;
}
}
}
@@ -401,11 +443,11 @@
return message;
};
// 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
@@ -435,11 +477,11 @@
break;
}
}
if (message == null || message == undefined) {
- if (this.isSet(options.defaultValue)) {
+ if (isSet(options.defaultValue)) {
if (isObject(options.defaultValue)) {
message = this.pluralizationLookupWithoutFallback(count, options.locale, options.defaultValue);
} else {
message = options.defaultValue;
}
@@ -490,11 +532,11 @@
for (var attr in subject) {
if (!subject.hasOwnProperty(attr)) {
continue;
}
- if (this.isSet(options[attr])) {
+ if (isSet(options[attr])) {
continue;
}
options[attr] = subject[attr];
}
@@ -509,40 +551,42 @@
I18n.createTranslationOptions = function(scope, options) {
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;
};
// 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 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)) {
+ translation = this.lookup(translationOption.scope, optionsWithoutDefault);
+ } else if (isSet(translationOption.message)) {
+ translation = lazyEvaluate(translationOption.message, scope);
}
if (translation !== undefined && translation !== null) {
return true;
}
@@ -552,20 +596,20 @@
return this.missingTranslation(scope, options);
}
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 (isObject(translation) && isSet(options.count)) {
+ translation = this.pluralize(options.count, scope, options);
}
return translation;
};
// This function interpolates the all variables in the given message.
I18n.interpolate = function(message, options) {
- options = this.prepareOptions(options);
+ options = options || {}
var matches = message.match(this.placeholder)
, placeholder
, value
, name
, regex
@@ -579,11 +623,11 @@
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);
} else {
value = this.missingPlaceholder(placeholder, message, options);
@@ -598,20 +642,18 @@
// 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);
+ options = this.prepareOptions({count: String(count)}, options)
var pluralizer, message, result;
result = this.pluralizationLookup(count, scope, options);
if (result.translations == undefined || result.translations == null) {
return this.missingTranslation(scope, options);
}
- options.count = String(count);
-
if (result.message != undefined && result.message != null) {
return this.interpolate(result.message, options);
}
else {
pluralizer = this.pluralization.get(options.locale);
@@ -980,13 +1022,13 @@
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) {
+ if (isArray(scope)) {
scope = scope.join(this.defaultSeparator);
}
// Deal with the scope option provided through the second argument.
//