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 | */
|
55 | function 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 |
|
118 | module.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 | */
|
137 | Messages.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 | */
|
167 | Messages.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 | */
|
188 | Messages.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 | */
|
208 | Messages.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 | */
|
225 | Messages.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 | */
|
243 | Messages.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 | */
|
264 | Messages.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 */
|
277 | function _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 */
|
290 | function _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 | }
|