/* eslint-disable no-tabs */ /*! * jQuery Internationalization library * * Copyright (C) 2012 Santhosh Thottingal * * jquery.i18n is dual licensed GPLv2 or later and MIT. You don't have to do * anything special to choose one license or the other and you don't have to * notify anyone which license you are using. You are free to use * UniversalLanguageSelector in commercial projects as long as the copyright * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details. * * @licence GNU General Public Licence 2.0 or later * @licence MIT License */ (function ($) { "use strict"; var I18N, slice = Array.prototype.slice; /** * @constructor * @param {Object} options */ I18N = function (options) { // Load defaults this.options = $.extend({}, I18N.defaults, options); this.parser = this.options.parser; this.locale = this.options.locale; this.messageStore = this.options.messageStore; this.languages = {}; }; I18N.prototype = { /** * Localize a given messageKey to a locale. * @param {string} messageKey * @return {string} Localized message */ localize: function (messageKey) { var localeParts, localePartIndex, locale, fallbackIndex, tryingLocale, message; locale = this.locale; fallbackIndex = 0; while (locale) { // Iterate through locales starting at most-specific until // localization is found. As in fi-Latn-FI, fi-Latn and fi. localeParts = locale.split("-"); localePartIndex = localeParts.length; do { tryingLocale = localeParts.slice(0, localePartIndex).join("-"); message = this.messageStore.get(tryingLocale, messageKey); if (message) { return message; } localePartIndex--; } while (localePartIndex); if (locale === this.options.fallbackLocale) { break; } locale = ($.i18n.fallbacks[this.locale] && $.i18n.fallbacks[this.locale][fallbackIndex]) || this.options.fallbackLocale; $.i18n.log("Trying fallback locale for " + this.locale + ": " + locale + " (" + messageKey + ")"); fallbackIndex++; } // key not found return ""; }, /* * Destroy the i18n instance. */ destroy: function () { $.removeData(document, "i18n"); }, /** * General message loading API This can take a URL string for * the json formatted messages. Example: * load('path/to/all_localizations.json'); * * To load a localization file for a locale: * * load('path/to/de-messages.json', 'de' ); * * * To load a localization file from a directory: * * load('path/to/i18n/directory', 'de' ); * * The above method has the advantage of fallback resolution. * ie, it will automatically load the fallback locales for de. * For most usecases, this is the recommended method. * It is optional to have trailing slash at end. * * A data object containing message key- message translation mappings * can also be passed. Example: * * load( { 'hello' : 'Hello' }, optionalLocale ); * * * A source map containing key-value pair of languagename and locations * can also be passed. Example: * * load( { * bn: 'i18n/bn.json', * he: 'i18n/he.json', * en: 'i18n/en.json' * } ) * * * If the data argument is null/undefined/false, * all cached messages for the i18n instance will get reset. * * @param {string|Object} source * @param {string} locale Language tag * @return {jQuery.Promise} */ load: function (source, locale) { var fallbackLocales, locIndex, fallbackLocale, sourceMap = {}; if (!source && !locale) { source = "i18n/" + $.i18n().locale + ".json"; locale = $.i18n().locale; } if (typeof source === "string" && // source extension should be json, but can have query params after that. source.split("?")[0].split(".").pop() !== "json" ) { // Load specified locale then check for fallbacks when directory is // specified in load() sourceMap[locale] = source + "/" + locale + ".json"; fallbackLocales = ($.i18n.fallbacks[locale] || []) .concat(this.options.fallbackLocale); for (locIndex = 0; locIndex < fallbackLocales.length; locIndex++) { fallbackLocale = fallbackLocales[locIndex]; sourceMap[fallbackLocale] = source + "/" + fallbackLocale + ".json"; } return this.load(sourceMap); } else { return this.messageStore.load(source, locale); } }, /** * Does parameter and magic word substitution. * * @param {string} key Message key * @param {Array} parameters Message parameters * @return {string} */ parse: function (key, parameters) { var message = this.localize(key); // FIXME: This changes the state of the I18N object, // should probably not change the 'this.parser' but just // pass it to the parser. this.parser.language = $.i18n.languages[$.i18n().locale] || $.i18n.languages.default; if (message === "") { message = key; } return this.parser.parse(message, parameters); } }; /** * Process a message from the $.I18N instance * for the current document, stored in jQuery.data(document). * * @param {string} key Key of the message. * @param {string} param1 [param...] Variadic list of parameters for {key}. * @return {string|$.I18N} Parsed message, or if no key was given * the instance of $.I18N is returned. */ $.i18n = function (key, param1) { var parameters, i18n = $.data(document, "i18n"), options = typeof key === "object" && key; // If the locale option for this call is different then the setup so far, // update it automatically. This doesn't just change the context for this // call but for all future call as well. // If there is no i18n setup yet, don't do this. It will be taken care of // by the `new I18N` construction below. // NOTE: It should only change language for this one call. // Then cache instances of I18N somewhere. if (options && options.locale && i18n && i18n.locale !== options.locale) { i18n.locale = options.locale; } if (!i18n) { i18n = new I18N(options); $.data(document, "i18n", i18n); } if (typeof key === "string") { if (param1 !== undefined) { parameters = slice.call(arguments, 1); } else { parameters = []; } return i18n.parse(key, parameters); } else { // FIXME: remove this feature/bug. return i18n; } }; $.fn.i18n = function () { var i18n = $.data(document, "i18n"); if (!i18n) { i18n = new I18N(); $.data(document, "i18n", i18n); } return this.each(function () { var $this = $(this), messageKey = $this.data("i18n"), lBracket, rBracket, type, key; if (messageKey) { lBracket = messageKey.indexOf("["); rBracket = messageKey.indexOf("]"); if (lBracket !== -1 && rBracket !== -1 && lBracket < rBracket) { type = messageKey.slice(lBracket + 1, rBracket); key = messageKey.slice(rBracket + 1); if (type === "html") { $this.html(i18n.parse(key)); } else { $this.attr(type, i18n.parse(key)); } } else { $this.text(i18n.parse(messageKey)); } } else { $this.find("[data-i18n]").i18n(); } }); }; function getDefaultLocale() { var locale = $("html").attr("lang"); if (!locale) { locale = navigator.language || navigator.userLanguage || ""; } return locale; } $.i18n.languages = {}; $.i18n.messageStore = $.i18n.messageStore || {}; $.i18n.parser = { // The default parser only handles variable substitution parse: function (message, parameters) { return message.replace(/\$(\d+)/g, function (str, match) { var index = parseInt(match, 10) - 1; return parameters[index] !== undefined ? parameters[index] : "$" + match; }); }, emitter: {} }; $.i18n.fallbacks = {}; $.i18n.debug = false; $.i18n.log = function (/* arguments */) { if (window.console && $.i18n.debug) { window.console.log.apply(window.console, arguments); } }; /* Static members */ I18N.defaults = { locale: getDefaultLocale(), fallbackLocale: "en", parser: $.i18n.parser, messageStore: $.i18n.messageStore }; // Expose constructor $.i18n.constructor = I18N; }(jQuery));