polyglot.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. // (c) 2012 Airbnb, Inc.
  2. //
  3. // polyglot.js may be freely distributed under the terms of the BSD
  4. // license. For all licensing information, details, and documention:
  5. // http://airbnb.github.com/polyglot.js
  6. //
  7. //
  8. // Polyglot.js is an I18n helper library written in JavaScript, made to
  9. // work both in the browser and in Node. It provides a simple solution for
  10. // interpolation and pluralization, based off of Airbnb's
  11. // experience adding I18n functionality to its Backbone.js and Node apps.
  12. //
  13. // Polylglot is agnostic to your translation backend. It doesn't perform any
  14. // translation; it simply gives you a way to manage translated phrases from
  15. // your client- or server-side JavaScript application.
  16. //
  17. (function(root, factory) {
  18. if (typeof define === 'function' && define.amd) {
  19. define([], function() {
  20. return factory(root);
  21. });
  22. } else if (typeof exports === 'object') {
  23. module.exports = factory(root);
  24. } else {
  25. root.Polyglot = factory(root);
  26. }
  27. }(this, function(root) {
  28. 'use strict';
  29. // ### Polyglot class constructor
  30. function Polyglot(options) {
  31. options = options || {};
  32. this.phrases = {};
  33. this.extend(options.phrases || {});
  34. this.currentLocale = options.locale || 'en';
  35. this.allowMissing = !!options.allowMissing;
  36. this.warn = options.warn || warn;
  37. }
  38. // ### Version
  39. Polyglot.VERSION = '0.4.3';
  40. // ### polyglot.locale([locale])
  41. //
  42. // Get or set locale. Internally, Polyglot only uses locale for pluralization.
  43. Polyglot.prototype.locale = function(newLocale) {
  44. if (newLocale) this.currentLocale = newLocale;
  45. return this.currentLocale;
  46. };
  47. // ### polyglot.extend(phrases)
  48. //
  49. // Use `extend` to tell Polyglot how to translate a given key.
  50. //
  51. // polyglot.extend({
  52. // "hello": "Hello",
  53. // "hello_name": "Hello, %{name}"
  54. // });
  55. //
  56. // The key can be any string. Feel free to call `extend` multiple times;
  57. // it will override any phrases with the same key, but leave existing phrases
  58. // untouched.
  59. //
  60. // It is also possible to pass nested phrase objects, which get flattened
  61. // into an object with the nested keys concatenated using dot notation.
  62. //
  63. // polyglot.extend({
  64. // "nav": {
  65. // "hello": "Hello",
  66. // "hello_name": "Hello, %{name}",
  67. // "sidebar": {
  68. // "welcome": "Welcome"
  69. // }
  70. // }
  71. // });
  72. //
  73. // console.log(polyglot.phrases);
  74. // // {
  75. // // 'nav.hello': 'Hello',
  76. // // 'nav.hello_name': 'Hello, %{name}',
  77. // // 'nav.sidebar.welcome': 'Welcome'
  78. // // }
  79. //
  80. // `extend` accepts an optional second argument, `prefix`, which can be used
  81. // to prefix every key in the phrases object with some string, using dot
  82. // notation.
  83. //
  84. // polyglot.extend({
  85. // "hello": "Hello",
  86. // "hello_name": "Hello, %{name}"
  87. // }, "nav");
  88. //
  89. // console.log(polyglot.phrases);
  90. // // {
  91. // // 'nav.hello': 'Hello',
  92. // // 'nav.hello_name': 'Hello, %{name}'
  93. // // }
  94. //
  95. // This feature is used internally to support nested phrase objects.
  96. Polyglot.prototype.extend = function(morePhrases, prefix) {
  97. var phrase;
  98. for (var key in morePhrases) {
  99. if (morePhrases.hasOwnProperty(key)) {
  100. phrase = morePhrases[key];
  101. if (prefix) key = prefix + '.' + key;
  102. if (typeof phrase === 'object') {
  103. this.extend(phrase, key);
  104. } else {
  105. this.phrases[key] = phrase;
  106. }
  107. }
  108. }
  109. };
  110. // ### polyglot.clear()
  111. //
  112. // Clears all phrases. Useful for special cases, such as freeing
  113. // up memory if you have lots of phrases but no longer need to
  114. // perform any translation. Also used internally by `replace`.
  115. Polyglot.prototype.clear = function() {
  116. this.phrases = {};
  117. };
  118. // ### polyglot.replace(phrases)
  119. //
  120. // Completely replace the existing phrases with a new set of phrases.
  121. // Normally, just use `extend` to add more phrases, but under certain
  122. // circumstances, you may want to make sure no old phrases are lying around.
  123. Polyglot.prototype.replace = function(newPhrases) {
  124. this.clear();
  125. this.extend(newPhrases);
  126. };
  127. // ### polyglot.t(key, options)
  128. //
  129. // The most-used method. Provide a key, and `t` will return the
  130. // phrase.
  131. //
  132. // polyglot.t("hello");
  133. // => "Hello"
  134. //
  135. // The phrase value is provided first by a call to `polyglot.extend()` or
  136. // `polyglot.replace()`.
  137. //
  138. // Pass in an object as the second argument to perform interpolation.
  139. //
  140. // polyglot.t("hello_name", {name: "Spike"});
  141. // => "Hello, Spike"
  142. //
  143. // If you like, you can provide a default value in case the phrase is missing.
  144. // Use the special option key "_" to specify a default.
  145. //
  146. // polyglot.t("i_like_to_write_in_language", {
  147. // _: "I like to write in %{language}.",
  148. // language: "JavaScript"
  149. // });
  150. // => "I like to write in JavaScript."
  151. //
  152. Polyglot.prototype.t = function(key, options) {
  153. var phrase, result;
  154. options = options == null ? {} : options;
  155. // allow number as a pluralization shortcut
  156. if (typeof options === 'number') {
  157. options = {smart_count: options};
  158. }
  159. if (typeof this.phrases[key] === 'string') {
  160. phrase = this.phrases[key];
  161. } else if (typeof options._ === 'string') {
  162. phrase = options._;
  163. } else if (this.allowMissing) {
  164. phrase = key;
  165. } else {
  166. this.warn('Missing translation for key: "'+key+'"');
  167. result = key;
  168. }
  169. if (typeof phrase === 'string') {
  170. options = clone(options);
  171. result = choosePluralForm(phrase, this.currentLocale, options.smart_count);
  172. result = interpolate(result, options);
  173. }
  174. return result;
  175. };
  176. // ### polyglot.has(key)
  177. //
  178. // Check if polyglot has a translation for given key
  179. Polyglot.prototype.has = function(key) {
  180. return key in this.phrases;
  181. };
  182. // #### Pluralization methods
  183. // The string that separates the different phrase possibilities.
  184. var delimeter = '||||';
  185. // Mapping from pluralization group plural logic.
  186. var pluralTypes = {
  187. chinese: function(n) { return 0; },
  188. german: function(n) { return n !== 1 ? 1 : 0; },
  189. french: function(n) { return n > 1 ? 1 : 0; },
  190. 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; },
  191. czech: function(n) { return (n === 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2; },
  192. polish: function(n) { return (n === 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); },
  193. icelandic: function(n) { return (n % 10 !== 1 || n % 100 === 11) ? 1 : 0; }
  194. };
  195. // Mapping from pluralization group to individual locales.
  196. var pluralTypeToLanguages = {
  197. chinese: ['fa', 'id', 'ja', 'ko', 'lo', 'ms', 'th', 'tr', 'zh'],
  198. german: ['da', 'de', 'en', 'es', 'fi', 'el', 'he', 'hu', 'it', 'nl', 'no', 'pt', 'sv'],
  199. french: ['fr', 'tl', 'pt-br'],
  200. russian: ['hr', 'ru'],
  201. czech: ['cs'],
  202. polish: ['pl'],
  203. icelandic: ['is']
  204. };
  205. function langToTypeMap(mapping) {
  206. var type, langs, l, ret = {};
  207. for (type in mapping) {
  208. if (mapping.hasOwnProperty(type)) {
  209. langs = mapping[type];
  210. for (l in langs) {
  211. ret[langs[l]] = type;
  212. }
  213. }
  214. }
  215. return ret;
  216. }
  217. // Trim a string.
  218. function trim(str){
  219. var trimRe = /^\s+|\s+$/g;
  220. return str.replace(trimRe, '');
  221. }
  222. // Based on a phrase text that contains `n` plural forms separated
  223. // by `delimeter`, a `locale`, and a `count`, choose the correct
  224. // plural form, or none if `count` is `null`.
  225. function choosePluralForm(text, locale, count){
  226. var ret, texts, chosenText;
  227. if (count != null && text) {
  228. texts = text.split(delimeter);
  229. chosenText = texts[pluralTypeIndex(locale, count)] || texts[0];
  230. ret = trim(chosenText);
  231. } else {
  232. ret = text;
  233. }
  234. return ret;
  235. }
  236. function pluralTypeName(locale) {
  237. var langToPluralType = langToTypeMap(pluralTypeToLanguages);
  238. return langToPluralType[locale] || langToPluralType.en;
  239. }
  240. function pluralTypeIndex(locale, count) {
  241. return pluralTypes[pluralTypeName(locale)](count);
  242. }
  243. // ### interpolate
  244. //
  245. // Does the dirty work. Creates a `RegExp` object for each
  246. // interpolation placeholder.
  247. function interpolate(phrase, options) {
  248. for (var arg in options) {
  249. if (arg !== '_' && options.hasOwnProperty(arg)) {
  250. // We create a new `RegExp` each time instead of using a more-efficient
  251. // string replace so that the same argument can be replaced multiple times
  252. // in the same phrase.
  253. phrase = phrase.replace(new RegExp('%\\{'+arg+'\\}', 'g'), options[arg]);
  254. }
  255. }
  256. return phrase;
  257. }
  258. // ### warn
  259. //
  260. // Provides a warning in the console if a phrase key is missing.
  261. function warn(message) {
  262. root.console && root.console.warn && root.console.warn('WARNING: ' + message);
  263. }
  264. // ### clone
  265. //
  266. // Clone an object.
  267. function clone(source) {
  268. var ret = {};
  269. for (var prop in source) {
  270. ret[prop] = source[prop];
  271. }
  272. return ret;
  273. }
  274. return Polyglot;
  275. }));