UNPKG

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