aboutsummaryrefslogtreecommitdiffstats
path: root/spec/javascript/support/i18n.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/javascript/support/i18n.js')
-rw-r--r--spec/javascript/support/i18n.js1067
1 files changed, 1067 insertions, 0 deletions
diff --git a/spec/javascript/support/i18n.js b/spec/javascript/support/i18n.js
new file mode 100644
index 000000000..b6f14687d
--- /dev/null
+++ b/spec/javascript/support/i18n.js
@@ -0,0 +1,1067 @@
+// I18n.js
+// =======
+//
+// This small library provides the Rails I18n API on the Javascript.
+// You don't actually have to use Rails (or even Ruby) to use I18n.js.
+// Just make sure you export all translations in an object like this:
+//
+// I18n.translations.en = {
+// hello: "Hello World"
+// };
+//
+// See tests for specific formatting like numbers and dates.
+//
+
+// Using UMD pattern from
+// https://github.com/umdjs/umd#regular-module
+// `returnExports.js` version
+;(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define("i18n", function(){ return factory(root);});
+ } else if (typeof module === 'object' && module.exports) {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(root);
+ } else {
+ // Browser globals (root is window)
+ root.I18n = factory(root);
+ }
+}(this, function(global) {
+ "use strict";
+
+ // Use previously defined object if exists in current scope
+ var I18n = global && global.I18n || {};
+
+ // Just cache the Array#slice function.
+ var slice = Array.prototype.slice;
+
+ // Apply number padding.
+ var padding = function(number) {
+ return ("0" + number.toString()).substr(-2);
+ };
+
+ // Improved toFixed number rounding function with support for unprecise floating points
+ // JavaScript's standard toFixed function does not round certain numbers correctly (for example 0.105 with precision 2).
+ var toFixed = function(number, precision) {
+ return decimalAdjust('round', number, -precision).toFixed(precision);
+ };
+
+ // Is a given variable an object?
+ // Borrowed from Underscore.js
+ var isObject = function(obj) {
+ var type = typeof 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);
+ };
+ return Object.prototype.toString.call(val) === '[object Array]';
+ };
+
+ var isString = function(val) {
+ return typeof value == 'string' || Object.prototype.toString.call(val) === '[object String]';
+ };
+
+ var isNumber = function(val) {
+ return typeof val == 'number' || Object.prototype.toString.call(val) === '[object Number]';
+ };
+
+ var isBoolean = function(val) {
+ return val === true || val === false;
+ };
+
+ var decimalAdjust = function(type, value, exp) {
+ // If the exp is undefined or zero...
+ if (typeof exp === 'undefined' || +exp === 0) {
+ return Math[type](value);
+ }
+ value = +value;
+ exp = +exp;
+ // If the value is not a number or the exp is not an integer...
+ if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
+ return NaN;
+ }
+ // Shift
+ value = value.toString().split('e');
+ 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) || isArray(value)) {
+ dest[key] = value;
+ } else {
+ if (dest[key] == null) dest[key] = {};
+ merge(dest[key], value);
+ }
+ }
+ return dest;
+ };
+
+ // Set default days/months translations.
+ var DATE = {
+ day_names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
+ , abbr_day_names: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
+ , month_names: [null, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
+ , abbr_month_names: [null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ , meridian: ["AM", "PM"]
+ };
+
+ // Set default number format.
+ var NUMBER_FORMAT = {
+ precision: 3
+ , separator: "."
+ , delimiter: ","
+ , strip_insignificant_zeros: false
+ };
+
+ // Set default currency format.
+ var CURRENCY_FORMAT = {
+ unit: "$"
+ , precision: 2
+ , format: "%u%n"
+ , sign_first: true
+ , delimiter: ","
+ , separator: "."
+ };
+
+ // Set default percentage format.
+ var PERCENTAGE_FORMAT = {
+ unit: "%"
+ , precision: 3
+ , format: "%n%u"
+ , separator: "."
+ , delimiter: ""
+ };
+
+ // Set default size units.
+ var SIZE_UNITS = [null, "kb", "mb", "gb", "tb"];
+
+ // Other default options
+ var DEFAULT_OPTIONS = {
+ // Set default locale. This locale will be used when fallback is enabled and
+ // the translation doesn't exist in a particular locale.
+ defaultLocale: "en"
+ // Set the current locale to `en`.
+ , locale: "en"
+ // Set the translation key separator.
+ , defaultSeparator: "."
+ // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
+ , placeholder: /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm
+ // Set if engine should fallback to the default locale when a translation
+ // is missing.
+ , fallbacks: false
+ // Set the default translation object.
+ , translations: {}
+ // Set missing translation behavior. 'message' will display a message
+ // that the translation is missing, 'guess' will try to guess the string
+ , missingBehaviour: 'message'
+ // if you use missingBehaviour with 'message', but want to know that the
+ // 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() {
+ 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() {
+ 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,
+ // current locale and fallback locale.
+ //
+ // I18n.locales.get("de-DE");
+ // // ["de-DE", "de", "en"]
+ //
+ // You can define custom rules for any locale. Just make sure you return a array
+ // containing all locales.
+ //
+ // // Default the Wookie locale to English.
+ // I18n.locales["wk"] = function(locale) {
+ // return ["en"];
+ // };
+ //
+ I18n.locales = {};
+
+ // 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 (isFunction(result)) {
+ result = result(locale);
+ }
+
+ if (isArray(result) === false) {
+ result = [result];
+ }
+
+ return result;
+ };
+
+ // The default locale list.
+ I18n.locales["default"] = function(locale) {
+ var locales = []
+ , list = []
+ ;
+
+ // Handle the inline locale option that can be provided to
+ // the `I18n.t` options.
+ if (locale) {
+ locales.push(locale);
+ }
+
+ // Add the current locale to the list.
+ if (!locale && I18n.locale) {
+ locales.push(I18n.locale);
+ }
+
+ // 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
+ // `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) === -1) {
+ list.push(locale);
+ }
+
+ 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");
+ }
+
+ return list;
+ };
+
+ // Hold pluralization rules.
+ I18n.pluralization = {};
+
+ // Return the pluralizer for a specific locale.
+ // If no specify locale is found, then I18n's default will be used.
+ I18n.pluralization.get = function(locale) {
+ return this[locale] || this[I18n.locale] || this["default"];
+ };
+
+ // The default pluralizer rule.
+ // It detects the `zero`, `one`, and `other` scopes.
+ I18n.pluralization["default"] = function(count) {
+ switch (count) {
+ case 0: return ["zero", "other"];
+ case 1: return ["one"];
+ default: return ["other"];
+ }
+ };
+
+ // Return current locale. If no locale has been set, then
+ // the current locale will be the default locale.
+ I18n.currentLocale = function() {
+ return this.locale || this.defaultLocale;
+ };
+
+ // Check if value is different than undefined and 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 = options || {}
+
+ var locales = this.locales.get(options.locale).slice()
+ , requestedLocale = locales[0]
+ , locale
+ , scopes
+ , fullScope
+ , translations
+ ;
+
+ fullScope = this.getFullScope(scope, options);
+
+ while (locales.length) {
+ locale = locales.shift();
+ scopes = fullScope.split(this.defaultSeparator);
+ translations = this.translations[locale];
+
+ if (!translations) {
+ continue;
+ }
+ while (scopes.length) {
+ translations = translations[scopes.shift()];
+
+ if (translations === undefined || translations === null) {
+ break;
+ }
+ }
+
+ if (translations !== undefined && translations !== null) {
+ return translations;
+ }
+ }
+
+ if (isSet(options.defaultValue)) {
+ return lazyEvaluate(options.defaultValue, scope);
+ }
+ };
+
+ // lookup pluralization rule key into translations
+ I18n.pluralizationLookupWithoutFallback = function(count, locale, translations) {
+ var pluralizer = this.pluralization.get(locale)
+ , pluralizerKeys = pluralizer(count)
+ , pluralizerKey
+ , message;
+
+ if (isObject(translations)) {
+ while (pluralizerKeys.length) {
+ pluralizerKey = pluralizerKeys.shift();
+ if (isSet(translations[pluralizerKey])) {
+ message = translations[pluralizerKey];
+ break;
+ }
+ }
+ }
+
+ return message;
+ };
+
+ // Lookup dedicated to pluralization
+ I18n.pluralizationLookup = function(count, scope, options) {
+ options = options || {}
+ var locales = this.locales.get(options.locale).slice()
+ , requestedLocale = locales[0]
+ , locale
+ , scopes
+ , translations
+ , message
+ ;
+ scope = this.getFullScope(scope, options);
+
+ while (locales.length) {
+ locale = locales.shift();
+ scopes = scope.split(this.defaultSeparator);
+ translations = this.translations[locale];
+
+ if (!translations) {
+ continue;
+ }
+
+ while (scopes.length) {
+ translations = translations[scopes.shift()];
+ if (!isObject(translations)) {
+ break;
+ }
+ if (scopes.length == 0) {
+ message = this.pluralizationLookupWithoutFallback(count, locale, translations);
+ }
+ }
+ if (message != null && message != undefined) {
+ break;
+ }
+ }
+
+ if (message == null || message == undefined) {
+ if (isSet(options.defaultValue)) {
+ if (isObject(options.defaultValue)) {
+ message = this.pluralizationLookupWithoutFallback(count, options.locale, options.defaultValue);
+ } else {
+ message = options.defaultValue;
+ }
+ translations = options.defaultValue;
+ }
+ }
+
+ return { message: message, translations: translations };
+ };
+
+ // Rails changed the way the meridian is stored.
+ // It started with `date.meridian` returning an array,
+ // then it switched to `time.am` and `time.pm`.
+ // This function abstracts this difference and returns
+ // the correct meridian or the default value when none is provided.
+ I18n.meridian = function() {
+ var time = this.lookup("time");
+ var date = this.lookup("date");
+
+ if (time && time.am && time.pm) {
+ return [time.am, time.pm];
+ } else if (date && date.meridian) {
+ return date.meridian;
+ } else {
+ return DATE.meridian;
+ }
+ };
+
+ // 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 args = slice.call(arguments)
+ , options = {}
+ , subject
+ ;
+
+ while (args.length) {
+ subject = args.shift();
+
+ if (typeof(subject) != "object") {
+ continue;
+ }
+
+ for (var attr in subject) {
+ if (!subject.hasOwnProperty(attr)) {
+ continue;
+ }
+
+ if (isSet(options[attr])) {
+ continue;
+ }
+
+ options[attr] = subject[attr];
+ }
+ }
+
+ return options;
+ };
+
+ // Generate a list of translation options for default fallbacks.
+ // `defaultValue` is also deleted from options as it is returned as part of
+ // the translationOptions array.
+ I18n.createTranslationOptions = function(scope, options) {
+ var translationOptions = [{scope: scope}];
+
+ // Defaults should be an array of hashes containing either
+ // fallback scopes or messages
+ 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 (isSet(options.defaultValue)) {
+ translationOptions.push({ message: options.defaultValue });
+ }
+
+ return translationOptions;
+ };
+
+ // Translate the given scope with the provided options.
+ I18n.translate = function(scope, options) {
+ options = 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 (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;
+ }
+ }, this);
+
+ if (!translationFound) {
+ return this.missingTranslation(scope, options);
+ }
+
+ if (typeof(translation) === "string") {
+ translation = this.interpolate(translation, options);
+ } 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 = options || {}
+ var matches = message.match(this.placeholder)
+ , placeholder
+ , value
+ , name
+ , regex
+ ;
+
+ if (!matches) {
+ return message;
+ }
+
+ var value;
+
+ while (matches.length) {
+ placeholder = matches.shift();
+ name = placeholder.replace(this.placeholder, "$1");
+
+ 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);
+ }
+
+ regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
+ message = message.replace(regex, value);
+ }
+
+ return message.replace(/_#\$#_/g, "$");
+ };
+
+ // 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({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);
+ }
+
+ if (result.message != undefined && result.message != null) {
+ return this.interpolate(result.message, options);
+ }
+ else {
+ pluralizer = this.pluralization.get(options.locale);
+ return this.missingTranslation(scope + '.' + pluralizer(count)[0], options);
+ }
+ };
+
+ // Return a missing translation message for the given parameters.
+ I18n.missingTranslation = function(scope, options) {
+ //guess intended string
+ 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,
+ 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);
+
+ return '[missing "' + fullScopeWithLocale + '" translation]';
+ };
+
+ // Return a missing placeholder message for given parameters
+ I18n.missingPlaceholder = function(placeholder, message, options) {
+ return "[missing " + placeholder + " value]";
+ };
+
+ I18n.nullPlaceholder = function() {
+ return I18n.missingPlaceholder.apply(I18n, arguments);
+ };
+
+ // Format number using localization rules.
+ // The options will be retrieved from the `number.format` scope.
+ // If this isn't present, then the following options will be used:
+ //
+ // - `precision`: `3`
+ // - `separator`: `"."`
+ // - `delimiter`: `","`
+ // - `strip_insignificant_zeros`: `false`
+ //
+ // You can also override these options by providing the `options` argument.
+ //
+ I18n.toNumber = function(number, options) {
+ options = this.prepareOptions(
+ options
+ , this.lookup("number.format")
+ , NUMBER_FORMAT
+ );
+
+ var negative = number < 0
+ , string = toFixed(Math.abs(number), options.precision).toString()
+ , parts = string.split(".")
+ , precision
+ , buffer = []
+ , formattedNumber
+ , format = options.format || "%n"
+ , sign = negative ? "-" : ""
+ ;
+
+ 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.strip_insignificant_zeros && precision) {
+ precision = precision.replace(/0+$/, "");
+ }
+
+ if (options.precision > 0 && precision) {
+ formattedNumber += options.separator + precision;
+ }
+
+ if (options.sign_first) {
+ format = "%s" + format;
+ }
+ else {
+ format = format.replace("%n", "%s%n");
+ }
+
+ formattedNumber = format
+ .replace("%u", options.unit)
+ .replace("%n", formattedNumber)
+ .replace("%s", sign)
+ ;
+
+ return formattedNumber;
+ };
+
+ // Format currency with localization rules.
+ // The options will be retrieved from the `number.currency.format` and
+ // `number.format` scopes, in that order.
+ //
+ // Any missing option will be retrieved from the `I18n.toNumber` defaults and
+ // the following options:
+ //
+ // - `unit`: `"$"`
+ // - `precision`: `2`
+ // - `format`: `"%u%n"`
+ // - `delimiter`: `","`
+ // - `separator`: `"."`
+ //
+ // You can also override these options by providing the `options` argument.
+ //
+ I18n.toCurrency = function(number, options) {
+ options = this.prepareOptions(
+ options
+ , this.lookup("number.currency.format")
+ , this.lookup("number.format")
+ , CURRENCY_FORMAT
+ );
+
+ return this.toNumber(number, options);
+ };
+
+ // Localize several values.
+ // You can provide the following scopes: `currency`, `number`, or `percentage`.
+ // If you provide a scope that matches the `/^(date|time)/` regular expression
+ // then the `value` will be converted by using the `I18n.toTime` function.
+ //
+ // It will default to the value's `toString` function.
+ //
+ I18n.localize = function(scope, value, options) {
+ options || (options = {});
+
+ 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:
+ var localizedValue;
+
+ if (scope.match(/^(date|time)/)) {
+ localizedValue = this.toTime(scope, value);
+ } else {
+ localizedValue = value.toString();
+ }
+
+ return this.interpolate(localizedValue, options);
+ }
+ };
+
+ // Parse a given `date` string into a JavaScript Date object.
+ // This function is time zone aware.
+ //
+ // The following string formats are recognized:
+ //
+ // 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
+ // yyyy-mm-dd[ T]hh:mm::ss+00:00
+ // yyyy-mm-dd[ T]hh:mm::ss.123Z
+ //
+ I18n.parseDate = function(date) {
+ var matches, convertedDate, fraction;
+ // we have a date, so just return it.
+ 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)?/);
+
+ if (matches) {
+ for (var i = 1; i <= 6; i++) {
+ matches[i] = parseInt(matches[i], 10) || 0;
+ }
+
+ // month starts on 0
+ matches[2] -= 1;
+
+ fraction = matches[7] ? 1000 * ("0" + matches[7]) : null;
+
+ if (matches[8]) {
+ convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction));
+ } else {
+ convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction);
+ }
+ } else if (typeof(date) == "number") {
+ // UNIX timestamp
+ convertedDate = new Date();
+ convertedDate.setTime(date);
+ } else if (date.match(/([A-Z][a-z]{2}) ([A-Z][a-z]{2}) (\d+) (\d+:\d+:\d+) ([+-]\d+) (\d+)/)) {
+ // This format `Wed Jul 20 13:03:39 +0000 2011` is parsed by
+ // webkit/firefox, but not by IE, so we must parse it manually.
+ convertedDate = new Date();
+ convertedDate.setTime(Date.parse([
+ RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$6, RegExp.$4, RegExp.$5
+ ].join(" ")));
+ } 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;
+ };
+
+ // Formats time according to the directives in the given format string.
+ // The directives begins with a percent (%) character. Any text not listed as a
+ // 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)
+ //
+ I18n.strftime = function(date, format) {
+ var options = this.lookup("date")
+ , meridianOptions = I18n.meridian()
+ ;
+
+ if (!options) {
+ options = {};
+ }
+
+ options = this.prepareOptions(options, DATE);
+
+ if (isNaN(date.getTime())) {
+ throw new Error('I18n.strftime() requires a valid date object, but received an invalid date.');
+ }
+
+ 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;
+ }
+
+ format = format.replace("%a", options.abbr_day_names[weekDay]);
+ format = format.replace("%A", options.day_names[weekDay]);
+ format = format.replace("%b", options.abbr_month_names[month]);
+ format = format.replace("%B", options.month_names[month]);
+ format = format.replace("%d", padding(day));
+ format = format.replace("%e", day);
+ format = format.replace("%-d", day);
+ format = format.replace("%H", padding(hour));
+ format = format.replace("%-H", hour);
+ format = format.replace("%I", padding(hour12));
+ format = format.replace("%-I", 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("%S", padding(secs));
+ format = format.replace("%-S", secs);
+ format = format.replace("%w", weekDay);
+ format = format.replace("%y", padding(year));
+ format = format.replace("%-y", padding(year).replace(/^0+/, ""));
+ format = format.replace("%Y", year);
+ format = format.replace("%z", timezoneoffset);
+
+ return format;
+ };
+
+ // Convert the given dateString into a formatted date.
+ I18n.toTime = function(scope, dateString) {
+ var date = this.parseDate(dateString)
+ , format = this.lookup(scope)
+ ;
+
+ if (date.toString().match(/invalid/i)) {
+ return date.toString();
+ }
+
+ if (!format) {
+ return date.toString();
+ }
+
+ return this.strftime(date, format);
+ };
+
+ // 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")
+ , PERCENTAGE_FORMAT
+ );
+
+ return this.toNumber(number, options);
+ };
+
+ // Convert a number into a readable size representation.
+ 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." + SIZE_UNITS[iterations]);
+ precision = (size - Math.floor(size) === 0) ? 0 : 1;
+ }
+
+ options = this.prepareOptions(
+ options
+ , {unit: unit, precision: precision, format: "%n%u", delimiter: ""}
+ );
+
+ return this.toNumber(size, options);
+ };
+
+ I18n.getFullScope = function(scope, options) {
+ options = options || {}
+
+ // Deal with the scope as an array.
+ if (isArray(scope)) {
+ scope = scope.join(this.defaultSeparator);
+ }
+
+ // Deal with the scope option provided through the second argument.
+ //
+ // I18n.t('hello', {scope: 'greetings'});
+ //
+ if (options.scope) {
+ scope = [options.scope, scope].join(this.defaultSeparator);
+ }
+
+ return scope;
+ };
+ /**
+ * Merge obj1 with obj2 (shallow merge), without modifying inputs
+ * @param {Object} obj1
+ * @param {Object} obj2
+ * @returns {Object} Merged values of obj1 and obj2
+ *
+ * In order to support ES3, `Object.prototype.hasOwnProperty.call` is used
+ * Idea is from:
+ * https://stackoverflow.com/questions/8157700/object-has-no-hasownproperty-method-i-e-its-undefined-ie8
+ */
+ I18n.extend = function ( obj1, obj2 ) {
+ if (typeof(obj1) === "undefined" && typeof(obj2) === "undefined") {
+ return {};
+ }
+ return merge(obj1, obj2);
+ };
+
+ // Set aliases, so we can save some typing.
+ I18n.t = I18n.translate;
+ I18n.l = I18n.localize;
+ I18n.p = I18n.pluralize;
+
+ return I18n;
+}));