1093 lines
33 KiB
JavaScript
1093 lines
33 KiB
JavaScript
|
// 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 (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()
|
||
|
, 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()
|
||
|
, 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 (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('_',' ').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;
|
||
|
// 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) {
|
||
|
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("%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) {
|
||
|
var date = this.parseDate(dateString)
|
||
|
, format = this.lookup(scope)
|
||
|
;
|
||
|
|
||
|
// 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);
|
||
|
};
|
||
|
|
||
|
// 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.bind(I18n);
|
||
|
I18n.l = I18n.localize.bind(I18n);
|
||
|
I18n.p = I18n.pluralize.bind(I18n);
|
||
|
|
||
|
return I18n;
|
||
|
}));
|