// (c) 2012 Airbnb, Inc. // // polyglot.js may be freely distributed under the terms of the BSD // license. For all licensing information, details, and documention: // http://airbnb.github.com/polyglot.js // // // Polyglot.js is an I18n helper library written in JavaScript, made to // work both in the browser and in Node. It provides a simple solution for // interpolation and pluralization, based off of Airbnb's // experience adding I18n functionality to its Backbone.js and Node apps. // // Polylglot is agnostic to your translation backend. It doesn't perform any // translation; it simply gives you a way to manage translated phrases from // your client- or server-side JavaScript application. // (function(root, factory) { if (typeof define === 'function' && define.amd) { define([], function() { return factory(root); }); } else if (typeof exports === 'object') { module.exports = factory(root); } else { root.Polyglot = factory(root); } }(this, function(root) { 'use strict'; // ### Polyglot class constructor function Polyglot(options) { options = options || {}; this.phrases = {}; this.extend(options.phrases || {}); this.currentLocale = options.locale || 'en'; this.allowMissing = !!options.allowMissing; this.warn = options.warn || warn; } // ### Version Polyglot.VERSION = '0.4.3'; // ### polyglot.locale([locale]) // // Get or set locale. Internally, Polyglot only uses locale for pluralization. Polyglot.prototype.locale = function(newLocale) { if (newLocale) this.currentLocale = newLocale; return this.currentLocale; }; // ### polyglot.extend(phrases) // // Use `extend` to tell Polyglot how to translate a given key. // // polyglot.extend({ // "hello": "Hello", // "hello_name": "Hello, %{name}" // }); // // The key can be any string. Feel free to call `extend` multiple times; // it will override any phrases with the same key, but leave existing phrases // untouched. // // It is also possible to pass nested phrase objects, which get flattened // into an object with the nested keys concatenated using dot notation. // // polyglot.extend({ // "nav": { // "hello": "Hello", // "hello_name": "Hello, %{name}", // "sidebar": { // "welcome": "Welcome" // } // } // }); // // console.log(polyglot.phrases); // // { // // 'nav.hello': 'Hello', // // 'nav.hello_name': 'Hello, %{name}', // // 'nav.sidebar.welcome': 'Welcome' // // } // // `extend` accepts an optional second argument, `prefix`, which can be used // to prefix every key in the phrases object with some string, using dot // notation. // // polyglot.extend({ // "hello": "Hello", // "hello_name": "Hello, %{name}" // }, "nav"); // // console.log(polyglot.phrases); // // { // // 'nav.hello': 'Hello', // // 'nav.hello_name': 'Hello, %{name}' // // } // // This feature is used internally to support nested phrase objects. Polyglot.prototype.extend = function(morePhrases, prefix) { var phrase; for (var key in morePhrases) { if (morePhrases.hasOwnProperty(key)) { phrase = morePhrases[key]; if (prefix) key = prefix + '.' + key; if (typeof phrase === 'object') { this.extend(phrase, key); } else { this.phrases[key] = phrase; } } } }; // ### polyglot.clear() // // Clears all phrases. Useful for special cases, such as freeing // up memory if you have lots of phrases but no longer need to // perform any translation. Also used internally by `replace`. Polyglot.prototype.clear = function() { this.phrases = {}; }; // ### polyglot.replace(phrases) // // Completely replace the existing phrases with a new set of phrases. // Normally, just use `extend` to add more phrases, but under certain // circumstances, you may want to make sure no old phrases are lying around. Polyglot.prototype.replace = function(newPhrases) { this.clear(); this.extend(newPhrases); }; // ### polyglot.t(key, options) // // The most-used method. Provide a key, and `t` will return the // phrase. // // polyglot.t("hello"); // => "Hello" // // The phrase value is provided first by a call to `polyglot.extend()` or // `polyglot.replace()`. // // Pass in an object as the second argument to perform interpolation. // // polyglot.t("hello_name", {name: "Spike"}); // => "Hello, Spike" // // If you like, you can provide a default value in case the phrase is missing. // Use the special option key "_" to specify a default. // // polyglot.t("i_like_to_write_in_language", { // _: "I like to write in %{language}.", // language: "JavaScript" // }); // => "I like to write in JavaScript." // Polyglot.prototype.t = function(key, options) { var phrase, result; options = options == null ? {} : options; // allow number as a pluralization shortcut if (typeof options === 'number') { options = {smart_count: options}; } if (typeof this.phrases[key] === 'string') { phrase = this.phrases[key]; } else if (typeof options._ === 'string') { phrase = options._; } else if (this.allowMissing) { phrase = key; } else { this.warn('Missing translation for key: "'+key+'"'); result = key; } if (typeof phrase === 'string') { options = clone(options); result = choosePluralForm(phrase, this.currentLocale, options.smart_count); result = interpolate(result, options); } return result; }; // ### polyglot.has(key) // // Check if polyglot has a translation for given key Polyglot.prototype.has = function(key) { return key in this.phrases; }; // #### Pluralization methods // The string that separates the different phrase possibilities. var delimeter = '||||'; // Mapping from pluralization group plural logic. var pluralTypes = { chinese: function(n) { return 0; }, german: function(n) { return n !== 1 ? 1 : 0; }, french: function(n) { return n > 1 ? 1 : 0; }, russian: function(n) { return n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; }, czech: function(n) { return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2; }, polish: function(n) { return (n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); }, icelandic: function(n) { return (n % 10 !== 1 || n % 100 === 11) ? 1 : 0; } }; // Mapping from pluralization group to individual locales. var pluralTypeToLanguages = { chinese: ['fa', 'id', 'ja', 'ko', 'lo', 'ms', 'th', 'tr', 'zh'], german: ['da', 'de', 'en', 'es', 'fi', 'el', 'he', 'hu', 'it', 'nl', 'no', 'pt', 'sv'], french: ['fr', 'tl', 'pt-br'], russian: ['hr', 'ru'], czech: ['cs'], polish: ['pl'], icelandic: ['is'] }; function langToTypeMap(mapping) { var type, langs, l, ret = {}; for (type in mapping) { if (mapping.hasOwnProperty(type)) { langs = mapping[type]; for (l in langs) { ret[langs[l]] = type; } } } return ret; } // Trim a string. function trim(str){ var trimRe = /^\s+|\s+$/g; return str.replace(trimRe, ''); } // Based on a phrase text that contains `n` plural forms separated // by `delimeter`, a `locale`, and a `count`, choose the correct // plural form, or none if `count` is `null`. function choosePluralForm(text, locale, count){ var ret, texts, chosenText; if (count != null && text) { texts = text.split(delimeter); chosenText = texts[pluralTypeIndex(locale, count)] || texts[0]; ret = trim(chosenText); } else { ret = text; } return ret; } function pluralTypeName(locale) { var langToPluralType = langToTypeMap(pluralTypeToLanguages); return langToPluralType[locale] || langToPluralType.en; } function pluralTypeIndex(locale, count) { return pluralTypes[pluralTypeName(locale)](count); } // ### interpolate // // Does the dirty work. Creates a `RegExp` object for each // interpolation placeholder. function interpolate(phrase, options) { for (var arg in options) { if (arg !== '_' && options.hasOwnProperty(arg)) { // We create a new `RegExp` each time instead of using a more-efficient // string replace so that the same argument can be replaced multiple times // in the same phrase. phrase = phrase.replace(new RegExp('%\\{'+arg+'\\}', 'g'), options[arg]); } } return phrase; } // ### warn // // Provides a warning in the console if a phrase key is missing. function warn(message) { root.console && root.console.warn && root.console.warn('WARNING: ' + message); } // ### clone // // Clone an object. function clone(source) { var ret = {}; for (var prop in source) { ret[prop] = source[prop]; } return ret; } return Polyglot; }));