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. //