messages.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. "use strict";
  2. /**
  3. * A collection of runtime utility functions
  4. *
  5. * @remarks
  6. * This package should be marked as a dependency for any package that publishes the output of {@link @messageformat/core#compileModule},
  7. * as it may be included in its ES module source output as a dependency.
  8. *
  9. * For applications that bundle their output using e.g. Webpack this is not necessary.
  10. *
  11. * The `Messages` accessor class is a completely optional addition.
  12. * See also {@link @messageformat/react# | @messageformat/react} for a React-specific solution.
  13. *
  14. * @packageDocumentation
  15. */
  16. Object.defineProperty(exports, "__esModule", { value: true });
  17. /**
  18. * Accessor class for compiled message functions generated by
  19. * {@link @messageformat/core#compileModule}
  20. *
  21. * @public
  22. * @remarks
  23. * ```js
  24. * import Messages from '@messageformat/runtime/messages'
  25. * ```
  26. *
  27. * @example
  28. * ```js
  29. * // build.js
  30. * import { writeFileSync } from 'fs';
  31. * import MessageFormat from '@messageformat/core';
  32. * import compileModule from '@messageformat/core/compile-module'
  33. *
  34. * const mf = new MessageFormat(['en', 'fi']);
  35. * const msgSet = {
  36. * en: {
  37. * a: 'A {TYPE} example.',
  38. * b: 'This has {COUNT, plural, one{one user} other{# users}}.',
  39. * c: {
  40. * d: 'We have {P, number, percent} code coverage.'
  41. * }
  42. * },
  43. * fi: {
  44. * b: 'Tällä on {COUNT, plural, one{yksi käyttäjä} other{# käyttäjää}}.',
  45. * e: 'Minä puhun vain suomea.'
  46. * }
  47. * };
  48. * writeFileSync('messages.js', compileModule(mf, msgSet));
  49. * ```
  50. *
  51. * ```js
  52. * // runtime.js
  53. * import Messages from '@messageformat/runtime/messages';
  54. * import msgData from './messages';
  55. *
  56. * const messages = new Messages(msgData, 'en');
  57. *
  58. * messages.hasMessage('a') // true
  59. * messages.hasObject('c') // true
  60. * messages.get('b', { COUNT: 3 }) // 'This has 3 users.'
  61. * messages.get(['c', 'd'], { P: 0.314 }) // 'We have 31% code coverage.'
  62. *
  63. * messages.get('e') // 'e'
  64. * messages.setFallback('en', ['foo', 'fi'])
  65. * messages.get('e') // 'Minä puhun vain suomea.'
  66. *
  67. * messages.locale = 'fi'
  68. * messages.hasMessage('a') // false
  69. * messages.hasMessage('a', 'en') // true
  70. * messages.hasMessage('a', null, true) // true
  71. * messages.hasObject('c') // false
  72. * messages.get('b', { COUNT: 3 }) // 'Tällä on 3 käyttäjää.'
  73. * messages.get('c').d({ P: 0.628 }) // 'We have 63% code coverage.'
  74. * ```
  75. */
  76. var Messages = /** @class */ (function () {
  77. /**
  78. * @param msgData - A map of locale codes to their function objects
  79. * @param defaultLocale - If not defined, default and initial locale is the first key of `msgData`
  80. */
  81. function Messages(msgData, defaultLocale) {
  82. var _this = this;
  83. /** @internal */
  84. this._data = {};
  85. /** @internal */
  86. this._fallback = {};
  87. /** @internal */
  88. this._defaultLocale = null;
  89. /** @internal */
  90. this._locale = null;
  91. Object.keys(msgData).forEach(function (lc) {
  92. if (lc !== 'toString') {
  93. _this._data[lc] = msgData[lc];
  94. if (defaultLocale === undefined)
  95. defaultLocale = lc;
  96. }
  97. });
  98. this.locale = defaultLocale || null;
  99. this._defaultLocale = this.locale;
  100. }
  101. Object.defineProperty(Messages.prototype, "availableLocales", {
  102. /** Read-only list of available locales */
  103. get: function () {
  104. return Object.keys(this._data);
  105. },
  106. enumerable: false,
  107. configurable: true
  108. });
  109. Object.defineProperty(Messages.prototype, "locale", {
  110. /**
  111. * Current locale
  112. *
  113. * @remarks
  114. * One of {@link Messages.availableLocales} or `null`.
  115. * Partial matches of language tags are supported, so e.g. with an `en` locale defined, it will be selected by `messages.locale = 'en-US'` and vice versa.
  116. */
  117. get: function () {
  118. return this._locale;
  119. },
  120. set: function (locale) {
  121. this._locale = this.resolveLocale(locale);
  122. },
  123. enumerable: false,
  124. configurable: true
  125. });
  126. Object.defineProperty(Messages.prototype, "defaultLocale", {
  127. /**
  128. * Default fallback locale
  129. *
  130. * @remarks
  131. * One of {@link Messages.availableLocales} or `null`.
  132. * Partial matches of language tags are supported, so e.g. with an `en` locale defined, it will be selected by `messages.defaultLocale = 'en-US'` and vice versa.
  133. */
  134. get: function () {
  135. return this._defaultLocale;
  136. },
  137. set: function (locale) {
  138. this._defaultLocale = this.resolveLocale(locale);
  139. },
  140. enumerable: false,
  141. configurable: true
  142. });
  143. /**
  144. * Add new messages to the accessor; useful if loading data dynamically
  145. *
  146. * @remarks
  147. * The locale code `lc` should be an exact match for the locale being updated, or empty to default to the current locale.
  148. * Use {@link Messages.resolveLocale} for resolving partial locale strings.
  149. *
  150. * If `keypath` is empty, adds or sets the complete message object for the corresponding locale.
  151. * If any keys in `keypath` do not exist, a new object will be created at that key.
  152. *
  153. * @param data - Hierarchical map of keys to functions, or a single message function
  154. * @param locale - If empty or undefined, defaults to `this.locale`
  155. * @param keypath - The keypath being added
  156. */
  157. Messages.prototype.addMessages = function (data, locale, keypath) {
  158. var lc = locale || String(this.locale);
  159. if (typeof data !== 'function') {
  160. data = Object.keys(data).reduce(function (map, key) {
  161. if (key !== 'toString')
  162. map[key] = data[key];
  163. return map;
  164. }, {});
  165. }
  166. if (Array.isArray(keypath) && keypath.length > 0) {
  167. var parent_1 = this._data[lc];
  168. for (var i = 0; i < keypath.length - 1; ++i) {
  169. var key = keypath[i];
  170. if (!parent_1[key])
  171. parent_1[key] = {};
  172. parent_1 = parent_1[key];
  173. }
  174. parent_1[keypath[keypath.length - 1]] = data;
  175. }
  176. else {
  177. this._data[lc] = data;
  178. }
  179. return this;
  180. };
  181. /**
  182. * Resolve `lc` to the key of an available locale or `null`, allowing for partial matches.
  183. *
  184. * @remarks
  185. * For example, with an `en` locale defined, it will be selected by `messages.defaultLocale = 'en-US'` and vice versa.
  186. */
  187. Messages.prototype.resolveLocale = function (locale) {
  188. var lc = String(locale);
  189. if (this._data[lc])
  190. return locale;
  191. if (locale) {
  192. while ((lc = lc.replace(/[-_]?[^-_]*$/, ''))) {
  193. if (this._data[lc])
  194. return lc;
  195. }
  196. var ll = this.availableLocales;
  197. var re = new RegExp('^' + locale + '[-_]');
  198. for (var i = 0; i < ll.length; ++i) {
  199. if (re.test(ll[i]))
  200. return ll[i];
  201. }
  202. }
  203. return null;
  204. };
  205. /**
  206. * Get the list of fallback locales
  207. *
  208. * @param locale - If empty or undefined, defaults to `this.locale`
  209. */
  210. Messages.prototype.getFallback = function (locale) {
  211. var lc = locale || String(this.locale);
  212. return (this._fallback[lc] ||
  213. (lc === this.defaultLocale || !this.defaultLocale
  214. ? []
  215. : [this.defaultLocale]));
  216. };
  217. /**
  218. * Set the fallback locale or locales for `lc`
  219. *
  220. * @remarks
  221. * To disable fallback for the locale, use `setFallback(lc, [])`.
  222. * To use the default fallback, use `setFallback(lc, null)`.
  223. */
  224. Messages.prototype.setFallback = function (lc, fallback) {
  225. this._fallback[lc] = Array.isArray(fallback) ? fallback : null;
  226. return this;
  227. };
  228. /**
  229. * Check if `key` is a message function for the locale
  230. *
  231. * @remarks
  232. * `key` may be a `string` for functions at the root level, or `string[]` for
  233. * accessing hierarchical objects. If an exact match is not found and
  234. * `fallback` is true, the fallback locales are checked for the first match.
  235. *
  236. * @param key - The key or keypath being sought
  237. * @param locale - If empty or undefined, defaults to `this.locale`
  238. * @param fallback - If true, also checks fallback locales
  239. */
  240. Messages.prototype.hasMessage = function (key, locale, fallback) {
  241. var lc = locale || String(this.locale);
  242. var fb = fallback ? this.getFallback(lc) : null;
  243. return _has(this._data, lc, key, fb, 'function');
  244. };
  245. /**
  246. * Check if `key` is a message object for the locale
  247. *
  248. * @remarks
  249. * `key` may be a `string` for functions at the root level, or `string[]` for
  250. * accessing hierarchical objects. If an exact match is not found and
  251. * `fallback` is true, the fallback locales are checked for the first match.
  252. *
  253. * @param key - The key or keypath being sought
  254. * @param locale - If empty or undefined, defaults to `this.locale`
  255. * @param fallback - If true, also checks fallback locales
  256. */
  257. Messages.prototype.hasObject = function (key, locale, fallback) {
  258. var lc = locale || String(this.locale);
  259. var fb = fallback ? this.getFallback(lc) : null;
  260. return _has(this._data, lc, key, fb, 'object');
  261. };
  262. /**
  263. * Get the message or object corresponding to `key`
  264. *
  265. * @remarks
  266. * `key` may be a `string` for functions at the root level, or `string[]` for accessing hierarchical objects.
  267. * If an exact match is not found, the fallback locales are checked for the first match.
  268. *
  269. * If `key` maps to a message function, the returned value will be the result of calling it with `props`.
  270. * If it maps to an object, the object is returned directly.
  271. * If nothing is found, `key` is returned.
  272. *
  273. * @param key - The key or keypath being sought
  274. * @param props - Optional properties passed to the function
  275. * @param lc - If empty or undefined, defaults to `this.locale`
  276. */
  277. Messages.prototype.get = function (key, props, locale) {
  278. var lc = locale || String(this.locale);
  279. var msg = _get(this._data[lc], key);
  280. if (msg)
  281. return typeof msg == 'function' ? msg(props) : msg;
  282. var fb = this.getFallback(lc);
  283. for (var i = 0; i < fb.length; ++i) {
  284. msg = _get(this._data[fb[i]], key);
  285. if (msg)
  286. return typeof msg == 'function' ? msg(props) : msg;
  287. }
  288. return key;
  289. };
  290. return Messages;
  291. }());
  292. exports.default = Messages;
  293. function _get(obj, key) {
  294. if (!obj)
  295. return null;
  296. var res = obj;
  297. if (Array.isArray(key)) {
  298. for (var i = 0; i < key.length; ++i) {
  299. if (typeof res !== 'object')
  300. return null;
  301. res = res[key[i]];
  302. if (!res)
  303. return null;
  304. }
  305. return res;
  306. }
  307. return typeof res === 'object' ? res[key] : null;
  308. }
  309. function _has(data, lc, key, fallback, type) {
  310. var msg = _get(data[lc], key);
  311. if (msg)
  312. return typeof msg === type;
  313. if (fallback) {
  314. for (var i = 0; i < fallback.length; ++i) {
  315. msg = _get(data[fallback[i]], key);
  316. if (msg)
  317. return typeof msg === type;
  318. }
  319. }
  320. return false;
  321. }