You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
335 lines
10 KiB
335 lines
10 KiB
// (c) 2012-2016 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. |
|
// |
|
|
|
'use strict'; |
|
|
|
var forEach = require('for-each'); |
|
var warning = require('warning'); |
|
var has = require('has'); |
|
var trim = require('string.prototype.trim'); |
|
|
|
var warn = function warn(message) { |
|
warning(false, message); |
|
}; |
|
|
|
var replace = String.prototype.replace; |
|
var split = String.prototype.split; |
|
|
|
// #### Pluralization methods |
|
// The string that separates the different phrase possibilities. |
|
var delimeter = '||||'; |
|
|
|
// Mapping from pluralization group plural logic. |
|
var pluralTypes = { |
|
arabic: function (n) { |
|
// http://www.arabeyes.org/Plural_Forms |
|
if (n < 3) { return n; } |
|
if (n % 100 >= 3 && n % 100 <= 10) return 3; |
|
return n % 100 >= 11 ? 4 : 5; |
|
}, |
|
chinese: function () { return 0; }, |
|
german: function (n) { return n !== 1 ? 1 : 0; }, |
|
french: function (n) { return n > 1 ? 1 : 0; }, |
|
russian: function (n) { |
|
if (n % 10 === 1 && n % 100 !== 11) { return 0; } |
|
return n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 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; } |
|
return 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 = { |
|
arabic: ['ar'], |
|
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', 'lt'], |
|
czech: ['cs', 'sk'], |
|
polish: ['pl'], |
|
icelandic: ['is'] |
|
}; |
|
|
|
function langToTypeMap(mapping) { |
|
var ret = {}; |
|
forEach(mapping, function (langs, type) { |
|
forEach(langs, function (lang) { |
|
ret[lang] = type; |
|
}); |
|
}); |
|
return ret; |
|
} |
|
|
|
function pluralTypeName(locale) { |
|
var langToPluralType = langToTypeMap(pluralTypeToLanguages); |
|
return langToPluralType[locale] |
|
|| langToPluralType[split.call(locale, /-/, 1)[0]] |
|
|| langToPluralType.en; |
|
} |
|
|
|
function pluralTypeIndex(locale, count) { |
|
return pluralTypes[pluralTypeName(locale)](count); |
|
} |
|
|
|
var dollarRegex = /\$/g; |
|
var dollarBillsYall = '$$'; |
|
var tokenRegex = /%\{(.*?)\}/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) { |
|
if (typeof phrase !== 'string') { |
|
throw new TypeError('Polyglot.transformPhrase expects argument #1 to be string'); |
|
} |
|
|
|
if (substitutions == null) { |
|
return phrase; |
|
} |
|
|
|
var result = phrase; |
|
|
|
// 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 `delimeter`, 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 && result) { |
|
var texts = split.call(result, delimeter); |
|
result = trim(texts[pluralTypeIndex(locale || 'en', options.smart_count)] || texts[0]); |
|
} |
|
|
|
// Interpolate: Creates a `RegExp` object for each interpolation placeholder. |
|
result = replace.call(result, tokenRegex, function (expression, argument) { |
|
if (!has(options, argument) || options[argument] == null) { return expression; } |
|
// Ensure replacement value is escaped to prevent special $-prefixed regex replace tokens. |
|
return replace.call(options[argument], dollarRegex, dollarBillsYall); |
|
}); |
|
|
|
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; |
|
} |
|
|
|
// ### 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) { |
|
forEach(morePhrases, function (phrase, key) { |
|
var prefixedKey = prefix ? prefix + '.' + key : key; |
|
if (typeof phrase === 'object') { |
|
this.extend(phrase, prefixedKey); |
|
} else { |
|
this.phrases[prefixedKey] = phrase; |
|
} |
|
}, this); |
|
}; |
|
|
|
// ### 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 { |
|
forEach(morePhrases, function (phrase, key) { |
|
var prefixedKey = prefix ? prefix + '.' + key : key; |
|
if (typeof phrase === 'object') { |
|
this.unset(phrase, prefixedKey); |
|
} else { |
|
delete this.phrases[prefixedKey]; |
|
} |
|
}, this); |
|
} |
|
}; |
|
|
|
// ### 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); |
|
} else { |
|
this.warn('Missing translation for key: "' + key + '"'); |
|
result = key; |
|
} |
|
if (typeof phrase === 'string') { |
|
result = transformPhrase(phrase, opts, this.currentLocale); |
|
} |
|
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 = transformPhrase; |
|
|
|
module.exports = Polyglot;
|
|
|