// (c) 2012-2018 Airbnb, Inc. // // polyglot.js may be freely distributed under the terms of the BSD // license. For all licensing information, details, and documentation: // 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. // 'use strict'; var entries = require('object.entries'); var warning = require('warning'); var has = require('hasown'); var warn = function warn(message) { warning(false, message); }; var defaultReplace = String.prototype.replace; var split = String.prototype.split; // #### Pluralization methods // The string that separates the different phrase possibilities. var delimiter = '||||'; var russianPluralGroups = function (n) { var lastTwo = n % 100; var end = lastTwo % 10; if (lastTwo !== 11 && end === 1) { return 0; } if (2 <= end && end <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 1; } return 2; }; var defaultPluralRules = { // Mapping from pluralization group plural logic. pluralTypes: { arabic: function (n) { // http://www.arabeyes.org/Plural_Forms if (n < 3) { return n; } var lastTwo = n % 100; if (lastTwo >= 3 && lastTwo <= 10) return 3; return lastTwo >= 11 ? 4 : 5; }, bosnian_serbian: russianPluralGroups, chinese: function () { return 0; }, croatian: russianPluralGroups, french: function (n) { return n >= 2 ? 1 : 0; }, german: function (n) { return n !== 1 ? 1 : 0; }, russian: russianPluralGroups, lithuanian: function (n) { if (n % 10 === 1 && n % 100 !== 11) { return 0; } return n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19) ? 1 : 2; }, czech: function (n) { if (n === 1) { return 0; } return (n >= 2 && n <= 4) ? 1 : 2; }, polish: function (n) { if (n === 1) { return 0; } var end = n % 10; return 2 <= end && end <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; }, icelandic: function (n) { return (n % 10 !== 1 || n % 100 === 11) ? 1 : 0; }, slovenian: function (n) { var lastTwo = n % 100; if (lastTwo === 1) { return 0; } if (lastTwo === 2) { return 1; } if (lastTwo === 3 || lastTwo === 4) { return 2; } return 3; }, romanian: function (n) { if (n === 1) { return 0; } var lastTwo = n % 100; if (n === 0 || (lastTwo >= 2 && lastTwo <= 19)) { return 1; } return 2; }, ukrainian: russianPluralGroups }, // Mapping from pluralization group to individual language codes/locales. // Will look up based on exact match, if not found and it's a locale will parse the locale // for language code, and if that does not exist will default to 'en' pluralTypeToLanguages: { arabic: ['ar'], bosnian_serbian: ['bs-Latn-BA', 'bs-Cyrl-BA', 'srl-RS', 'sr-RS'], chinese: ['id', 'id-ID', 'ja', 'ko', 'ko-KR', 'lo', 'ms', 'th', 'th-TH', 'zh'], croatian: ['hr', 'hr-HR'], german: ['fa', 'da', 'de', 'en', 'es', 'fi', 'el', 'he', 'hi-IN', 'hu', 'hu-HU', 'it', 'nl', 'no', 'pt', 'sv', 'tr'], french: ['fr', 'tl', 'pt-br'], russian: ['ru', 'ru-RU'], lithuanian: ['lt'], czech: ['cs', 'cs-CZ', 'sk'], polish: ['pl'], icelandic: ['is', 'mk'], slovenian: ['sl-SL'], romanian: ['ro'], ukrainian: ['uk', 'ua'] } }; function langToTypeMap(mapping) { var ret = {}; var mappingEntries = entries(mapping); for (var i = 0; i < mappingEntries.length; i += 1) { var type = mappingEntries[i][0]; var langs = mappingEntries[i][1]; for (var j = 0; j < langs.length; j += 1) { ret[langs[j]] = type; } } return ret; } function pluralTypeName(pluralRules, locale) { var langToPluralType = langToTypeMap(pluralRules.pluralTypeToLanguages); return langToPluralType[locale] || langToPluralType[split.call(locale, /-/, 1)[0]] || langToPluralType.en; } function pluralTypeIndex(pluralRules, pluralType, count) { return pluralRules.pluralTypes[pluralType](count); } function createMemoizedPluralTypeNameSelector() { var localePluralTypeStorage = {}; return function (pluralRules, locale) { var pluralType = localePluralTypeStorage[locale]; if (pluralType && !pluralRules.pluralTypes[pluralType]) { pluralType = null; localePluralTypeStorage[locale] = pluralType; } if (!pluralType) { pluralType = pluralTypeName(pluralRules, locale); if (pluralType) { localePluralTypeStorage[locale] = pluralType; } } return pluralType; }; } function escape(token) { return token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function constructTokenRegex(opts) { var prefix = (opts && opts.prefix) || '%{'; var suffix = (opts && opts.suffix) || '}'; if (prefix === delimiter || suffix === delimiter) { throw new RangeError('"' + delimiter + '" token is reserved for pluralization'); } return new RegExp(escape(prefix) + '(.*?)' + escape(suffix), 'g'); } var memoizedPluralTypeName = createMemoizedPluralTypeNameSelector(); var defaultTokenRegex = /%\{(.*?)\}/g; // ### transformPhrase(phrase, substitutions, locale) // // Takes a phrase string and transforms it by choosing the correct // plural form and interpolating it. // // transformPhrase('Hello, %{name}!', {name: 'Spike'}); // // "Hello, Spike!" // // The correct plural form is selected if substitutions.smart_count // is set. You can pass in a number instead of an Object as `substitutions` // as a shortcut for `smart_count`. // // transformPhrase('%{smart_count} new messages |||| 1 new message', {smart_count: 1}, 'en'); // // "1 new message" // // transformPhrase('%{smart_count} new messages |||| 1 new message', {smart_count: 2}, 'en'); // // "2 new messages" // // transformPhrase('%{smart_count} new messages |||| 1 new message', 5, 'en'); // // "5 new messages" // // You should pass in a third argument, the locale, to specify the correct plural type. // It defaults to `'en'` with 2 plural forms. function transformPhrase( phrase, substitutions, locale, tokenRegex, pluralRules, replaceImplementation ) { if (typeof phrase !== 'string') { throw new TypeError('Polyglot.transformPhrase expects argument #1 to be string'); } if (substitutions == null) { return phrase; } var result = phrase; var interpolationRegex = tokenRegex || defaultTokenRegex; var replace = replaceImplementation || defaultReplace; // allow number as a pluralization shortcut var options = typeof substitutions === 'number' ? { smart_count: substitutions } : substitutions; // Select plural form: based on a phrase text that contains `n` // plural forms separated by `delimiter`, a `locale`, and a `substitutions.smart_count`, // choose the correct plural form. This is only done if `count` is set. if (options.smart_count != null && phrase) { var pluralRulesOrDefault = pluralRules || defaultPluralRules; var texts = split.call(phrase, delimiter); var bestLocale = locale || 'en'; var pluralType = memoizedPluralTypeName(pluralRulesOrDefault, bestLocale); var pluralTypeWithCount = pluralTypeIndex( pluralRulesOrDefault, pluralType, options.smart_count ); result = defaultReplace.call(texts[pluralTypeWithCount] || texts[0], /^[^\S]*|[^\S]*$/g, ''); } // Interpolate: Creates a `RegExp` object for each interpolation placeholder. result = replace.call(result, interpolationRegex, function (expression, argument) { if (!has(options, argument) || options[argument] == null) { return expression; } return options[argument]; }); return result; } // ### Polyglot class constructor function Polyglot(options) { var opts = options || {}; this.phrases = {}; this.extend(opts.phrases || {}); this.currentLocale = opts.locale || 'en'; var allowMissing = opts.allowMissing ? transformPhrase : null; this.onMissingKey = typeof opts.onMissingKey === 'function' ? opts.onMissingKey : allowMissing; this.warn = opts.warn || warn; this.replaceImplementation = opts.replace || defaultReplace; this.tokenRegex = constructTokenRegex(opts.interpolation); this.pluralRules = opts.pluralRules || defaultPluralRules; } // ### 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 phraseEntries = entries(morePhrases || {}); for (var i = 0; i < phraseEntries.length; i += 1) { var key = phraseEntries[i][0]; var phrase = phraseEntries[i][1]; var prefixedKey = prefix ? prefix + '.' + key : key; if (typeof phrase === 'object') { this.extend(phrase, prefixedKey); } else { this.phrases[prefixedKey] = phrase; } } }; // ### polyglot.unset(phrases) // Use `unset` to selectively remove keys from a polyglot instance. // // polyglot.unset("some_key"); // polyglot.unset({ // "hello": "Hello", // "hello_name": "Hello, %{name}" // }); // // The unset method can take either a string (for the key), or an object hash with // the keys that you would like to unset. Polyglot.prototype.unset = function (morePhrases, prefix) { if (typeof morePhrases === 'string') { delete this.phrases[morePhrases]; } else { var phraseEntries = entries(morePhrases || {}); for (var i = 0; i < phraseEntries.length; i += 1) { var key = phraseEntries[i][0]; var phrase = phraseEntries[i][1]; var prefixedKey = prefix ? prefix + '.' + key : key; if (typeof phrase === 'object') { this.unset(phrase, prefixedKey); } else { delete this.phrases[prefixedKey]; } } } }; // ### 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; var opts = options == null ? {} : options; if (typeof this.phrases[key] === 'string') { phrase = this.phrases[key]; } else if (typeof opts._ === 'string') { phrase = opts._; } else if (this.onMissingKey) { var onMissingKey = this.onMissingKey; result = onMissingKey( key, opts, this.currentLocale, this.tokenRegex, this.pluralRules, this.replaceImplementation ); } else { this.warn('Missing translation for key: "' + key + '"'); result = key; } if (typeof phrase === 'string') { result = transformPhrase( phrase, opts, this.currentLocale, this.tokenRegex, this.pluralRules, this.replaceImplementation ); } return result; }; // ### polyglot.has(key) // // Check if polyglot has a translation for given key Polyglot.prototype.has = function (key) { return has(this.phrases, key); }; // export transformPhrase Polyglot.transformPhrase = function transform(phrase, substitutions, locale) { return transformPhrase(phrase, substitutions, locale); }; module.exports = Polyglot;