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; }));