// Instantiate the object var I18n = I18n || {}; // Set default locale to english I18n.defaultLocale = "en"; // Set default pluralization rule I18n.pluralizationRules = { en: function(n) { return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other"; } }; // Set current locale to null I18n.locale = null; I18n.fallbackLocale = null; // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; I18n.SEPARATOR = "."; I18n.noFallbacks = false; I18n.isValidNode = function(obj, node, undefined) { return obj[node] !== null && obj[node] !== undefined; }; I18n.lookup = function(scope, options) { options = options || {}; var translations = this.prepareOptions(I18n.translations), locale = options.locale || I18n.currentLocale(), messages = translations[locale] || {}, currentScope; options = this.prepareOptions(options); if (typeof scope === "object") { scope = scope.join(this.SEPARATOR); } if (options.scope) { scope = options.scope.toString() + this.SEPARATOR + scope; } var originalScope = scope; scope = scope.split(this.SEPARATOR); if (scope.length > 0 && scope[0] !== "js") { scope.unshift("js"); } while (messages && scope.length > 0) { currentScope = scope.shift(); messages = messages[currentScope]; } if (messages === undefined && this.extras && this.extras[locale]) { messages = this.extras[locale]; scope = originalScope.split(this.SEPARATOR); while (messages && scope.length > 0) { currentScope = scope.shift(); messages = messages[currentScope]; } } if (messages === undefined) { messages = options.defaultValue; } return messages; }; // 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 options = {}, opts, count = arguments.length; for (var i = 0; i < count; i++) { opts = arguments[i]; if (!opts) { continue; } for (var key in opts) { if (!this.isValidNode(options, key)) { options[key] = opts[key]; } } } return options; }; I18n.interpolate = function(message, options) { options = this.prepareOptions(options); var matches = message.match(this.PLACEHOLDER), placeholder, value, name; if (!matches) { return message; } for (var i = 0; (placeholder = matches[i]); i++) { name = placeholder.replace(this.PLACEHOLDER, "$1"); if (typeof options[name] === "string") { // The dollar sign (`$`) is a special replace pattern, and `$&` inserts // the matched string. Thus dollars signs need to be escaped with the // special pattern `$$`, which inserts a single `$`. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter value = options[name].replace(/\$/g, "$$$$"); } else { value = options[name]; } if (!this.isValidNode(options, name)) { value = "[missing " + placeholder + " value]"; } var regex = new RegExp( placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}") ); message = message.replace(regex, value); } return message; }; I18n.translate = function(scope, options) { options = this.prepareOptions(options); options.needsPluralization = typeof options.count === "number"; options.ignoreMissing = !this.noFallbacks; var translation = this.findTranslation(scope, options); if (!this.noFallbacks) { if (!translation && this.fallbackLocale) { options.locale = this.fallbackLocale; translation = this.findTranslation(scope, options); } options.ignoreMissing = false; if (!translation && this.currentLocale() !== this.defaultLocale) { options.locale = this.defaultLocale; translation = this.findTranslation(scope, options); } if (!translation && this.currentLocale() !== "en") { options.locale = "en"; translation = this.findTranslation(scope, options); } } try { return this.interpolate(translation, options); } catch (error) { return this.missingTranslation(scope); } }; I18n.findTranslation = function(scope, options) { var translation = this.lookup(scope, options); if (translation && options.needsPluralization) { translation = this.pluralize(translation, scope, options); } return translation; }; I18n.toNumber = function(number, options) { options = this.prepareOptions(options, this.lookup("number.format"), { precision: 3, separator: this.SEPARATOR, delimiter: ",", strip_insignificant_zeros: false }); var negative = number < 0, string = Math.abs(number) .toFixed(options.precision) .toString(), parts = string.split(this.SEPARATOR), buffer = [], formattedNumber; number = parts[0]; 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.precision > 0) { formattedNumber += options.separator + parts[1]; } if (negative) { formattedNumber = "-" + formattedNumber; } if (options.strip_insignificant_zeros) { var regex = { separator: new RegExp(options.separator.replace(/\./, "\\.") + "$"), zeros: /0+$/ }; formattedNumber = formattedNumber .replace(regex.zeros, "") .replace(regex.separator, ""); } return formattedNumber; }; 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." + [null, "kb", "mb", "gb", "tb"][iterations] ); precision = size - Math.floor(size) === 0 ? 0 : 1; } options = this.prepareOptions(options, { precision: precision, format: this.t("number.human.storage_units.format"), delimiter: "" }); number = this.toNumber(size, options); number = options.format.replace("%u", unit).replace("%n", number); return number; }; I18n.pluralizer = function(locale) { var pluralizer = this.pluralizationRules[locale]; if (pluralizer !== undefined) return pluralizer; return this.pluralizationRules["en"]; }; I18n.findAndTranslateValidNode = function(keys, translation) { for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (this.isValidNode(translation, key)) return translation[key]; } return null; }; I18n.pluralize = function(translation, scope, options) { if (typeof translation !== "object") return translation; options = this.prepareOptions(options); var count = options.count.toString(); var pluralizer = this.pluralizer(options.locale || this.currentLocale()); var key = pluralizer(Math.abs(count)); var keys = typeof key === "object" && key instanceof Array ? key : [key]; var message = this.findAndTranslateValidNode(keys, translation); if (message !== null || options.ignoreMissing) { return message; } return this.missingTranslation(scope, keys[0]); }; I18n.missingTranslation = function(scope, key) { var message = "[" + this.currentLocale() + this.SEPARATOR + scope; if (key) { message += this.SEPARATOR + key; } return message + "]"; }; I18n.currentLocale = function() { return I18n.locale || I18n.defaultLocale; }; I18n.enableVerboseLocalization = function() { var counter = 0; var keys = {}; var t = I18n.t; I18n.noFallbacks = true; I18n.t = I18n.translate = function(scope, value) { var current = keys[scope]; if (!current) { current = keys[scope] = ++counter; var message = "Translation #" + current + ": " + scope; if (value && Object.keys(value).length > 0) { message += ", parameters: " + JSON.stringify(value); } // eslint-disable-next-line no-console console.info(message); } return t.apply(I18n, [scope, value]) + " (#" + current + ")"; }; }; I18n.enableVerboseLocalizationSession = function() { sessionStorage.setItem("verbose_localization", "true"); I18n.enableVerboseLocalization(); return "Verbose localization is enabled. Close the browser tab to turn it off. Reload the page to see the translation keys."; }; // shortcuts I18n.t = I18n.translate;