// 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 val === '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 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) { 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) || isNull(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 (https://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 https://www.w3.org/TR/html401/struct/dirlang.html // @see https://en.wikipedia.org/wiki/IETF_language_tag // // Format: `language-code = primary-code ( "-" subcode )*` // // primary-code uses ISO639-1 // @see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes // @see https://www.iso.org/iso/home/standards/language_codes.htm // // subcode uses ISO 3166-1 alpha-2 // @see https://en.wikipedia.org/wiki/ISO_3166 // @see https://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() , locale , scopes , fullScope , translations ; fullScope = this.getFullScope(scope, options); while (locales.length) { locale = locales.shift(); scopes = fullScope.split(options.separator || 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() , locale , scopes , translations , message ; scope = this.getFullScope(scope, options); while (locales.length) { locale = locales.shift(); scopes = scope.split(options.separator || 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 (typeof message !== "undefined" && message !== null) { break; } } if (typeof message === "undefined" || message === null) { 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 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 (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) { return true; } }, this); if (!translationFound) { return this.missingTranslation(scope, options); } if (typeof(translation) === "string") { translation = this.interpolate(translation, options); } 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; }; // This function interpolates the all variables in the given message. I18n.interpolate = function(message, options) { if (message == null) { return message; } options = options || {}; var matches = message.match(this.placeholder) , placeholder , value , name , regex ; if (!matches) { return message; } 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, result; result = this.pluralizationLookup(count, scope, options); if (typeof result.translations === "undefined" || result.translations == null) { return this.missingTranslation(scope, options); } if (typeof 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(/_/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(options.separator || 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", options) , this.lookup("number.format", options) , 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, options); case "number": scope = this.lookup("number.format", options); return this.toNumber(value, scope); case "percentage": return this.toPercentage(value, options); default: var localizedValue; if (scope.match(/^(date|time)/)) { localizedValue = this.toTime(scope, value, options); } 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; // 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") { 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/%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, options) { var options = this.lookup("date", options) , 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("%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); 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, options) { var date = this.parseDate(dateString) , format = this.lookup(scope, options) ; // 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_string; } 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", options) , this.lookup("number.format", options) , 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 , fullScope ; while (size >= kb && iterations < 4) { size = size / kb; iterations += 1; } if (iterations === 0) { fullScope = this.getFullScope("number.human.storage_units.units.byte", options); unit = this.t(fullScope, {count: size}); precision = 0; } else { fullScope = this.getFullScope("number.human.storage_units.units." + SIZE_UNITS[iterations], options); unit = this.t(fullScope); 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(options.separator || 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(options.separator || 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.bind(I18n); I18n.l = I18n.localize.bind(I18n); I18n.p = I18n.pluralize.bind(I18n); return I18n; }));