@ultraq/icu-message-formatter 0.14.2 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,61 +38,71 @@ import { memoize } from '@ultraq/function-utils';
38
38
  * @return {ParseCasesResult}
39
39
  */
40
40
  function parseCases(string = '') {
41
- const isWhitespace = ch => /\s/.test(ch);
42
- const args = [];
43
- const cases = {};
44
- let currTermStart = 0;
45
- let latestTerm = null;
46
- let inTerm = false;
47
- let i = 0;
48
- while (i < string.length) {
49
- // Term ended
50
- if (inTerm && (isWhitespace(string[i]) || string[i] === '{')) {
51
- inTerm = false;
52
- latestTerm = string.slice(currTermStart, i);
53
-
54
- // We want to process the opening char again so the case will be properly registered.
55
- if (string[i] === '{') {
56
- i--;
57
- }
58
- }
59
-
60
- // New term
61
- else if (!inTerm && !isWhitespace(string[i])) {
62
- const caseBody = string[i] === '{';
63
-
64
- // If there's a previous term, we can either handle a whole
65
- // case, or add that as an argument.
66
- if (latestTerm && caseBody) {
67
- const branchEndIndex = findClosingBracket(string, i);
68
- if (branchEndIndex === -1) {
69
- throw new Error(`Unbalanced curly braces in string: "${string}"`);
70
- }
71
- cases[latestTerm] = string.slice(i + 1, branchEndIndex); // Don't include the braces
72
-
73
- i = branchEndIndex; // Will be moved up where needed at end of loop.
74
- latestTerm = null;
75
- } else {
76
- if (latestTerm) {
77
- args.push(latestTerm);
78
- latestTerm = null;
79
- }
80
- inTerm = true;
81
- currTermStart = i;
82
- }
83
- }
84
- i++;
85
- }
86
- if (inTerm) {
87
- latestTerm = string.slice(currTermStart);
88
- }
89
- if (latestTerm) {
90
- args.push(latestTerm);
91
- }
92
- return {
93
- args,
94
- cases
95
- };
41
+ const isWhitespace = ch => /\s/.test(ch);
42
+
43
+ const args = [];
44
+ const cases = {};
45
+
46
+ let currTermStart = 0;
47
+ let latestTerm = null;
48
+ let inTerm = false;
49
+
50
+ let i = 0;
51
+ while (i < string.length) {
52
+ // Term ended
53
+ if (inTerm && (isWhitespace(string[i]) || string[i] === '{')) {
54
+ inTerm = false;
55
+ latestTerm = string.slice(currTermStart, i);
56
+
57
+ // We want to process the opening char again so the case will be properly registered.
58
+ if (string[i] === '{') {
59
+ i--;
60
+ }
61
+ }
62
+
63
+ // New term
64
+ else if (!inTerm && !isWhitespace(string[i])) {
65
+ const caseBody = string[i] === '{';
66
+
67
+ // If there's a previous term, we can either handle a whole
68
+ // case, or add that as an argument.
69
+ if (latestTerm && caseBody) {
70
+ const branchEndIndex = findClosingBracket(string, i);
71
+
72
+ if (branchEndIndex === -1) {
73
+ throw new Error(`Unbalanced curly braces in string: "${string}"`);
74
+ }
75
+
76
+ cases[latestTerm] = string.slice(i + 1, branchEndIndex); // Don't include the braces
77
+
78
+ i = branchEndIndex; // Will be moved up where needed at end of loop.
79
+ latestTerm = null;
80
+ }
81
+ else {
82
+ if (latestTerm) {
83
+ args.push(latestTerm);
84
+ latestTerm = null;
85
+ }
86
+
87
+ inTerm = true;
88
+ currTermStart = i;
89
+ }
90
+ }
91
+ i++;
92
+ }
93
+
94
+ if (inTerm) {
95
+ latestTerm = string.slice(currTermStart);
96
+ }
97
+
98
+ if (latestTerm) {
99
+ args.push(latestTerm);
100
+ }
101
+
102
+ return {
103
+ args,
104
+ cases
105
+ };
96
106
  }
97
107
 
98
108
  /**
@@ -106,19 +116,20 @@ function parseCases(string = '') {
106
116
  * could be found.
107
117
  */
108
118
  function findClosingBracket(string, fromIndex) {
109
- let depth = 0;
110
- for (let i = fromIndex + 1; i < string.length; i++) {
111
- let char = string.charAt(i);
112
- if (char === '}') {
113
- if (depth === 0) {
114
- return i;
115
- }
116
- depth--;
117
- } else if (char === '{') {
118
- depth++;
119
- }
120
- }
121
- return -1;
119
+ let depth = 0;
120
+ for (let i = fromIndex + 1; i < string.length; i++) {
121
+ let char = string.charAt(i);
122
+ if (char === '}') {
123
+ if (depth === 0) {
124
+ return i;
125
+ }
126
+ depth--;
127
+ }
128
+ else if (char === '{') {
129
+ depth++;
130
+ }
131
+ }
132
+ return -1;
122
133
  }
123
134
 
124
135
  /**
@@ -131,7 +142,7 @@ function findClosingBracket(string, fromIndex) {
131
142
  * in the formatted argument block.
132
143
  */
133
144
  function splitFormattedArgument(block) {
134
- return split(block.slice(1, -1), ',', 3);
145
+ return split(block.slice(1, -1), ',', 3);
135
146
  }
136
147
 
137
148
  /**
@@ -146,22 +157,22 @@ function splitFormattedArgument(block) {
146
157
  * @return {string[]}
147
158
  */
148
159
  function split(string, separator, limit, accumulator = []) {
149
- if (!string) {
150
- return accumulator;
151
- }
152
- if (limit === 1) {
153
- accumulator.push(string);
154
- return accumulator;
155
- }
156
- let indexOfDelimiter = string.indexOf(separator);
157
- if (indexOfDelimiter === -1) {
158
- accumulator.push(string);
159
- return accumulator;
160
- }
161
- let head = string.substring(0, indexOfDelimiter).trim();
162
- let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
163
- accumulator.push(head);
164
- return split(tail, separator, limit - 1, accumulator);
160
+ if (!string) {
161
+ return accumulator;
162
+ }
163
+ if (limit === 1) {
164
+ accumulator.push(string);
165
+ return accumulator;
166
+ }
167
+ let indexOfDelimiter = string.indexOf(separator);
168
+ if (indexOfDelimiter === -1) {
169
+ accumulator.push(string);
170
+ return accumulator;
171
+ }
172
+ let head = string.substring(0, indexOfDelimiter).trim();
173
+ let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
174
+ accumulator.push(head);
175
+ return split(tail, separator, limit - 1, accumulator);
165
176
  }
166
177
 
167
178
  /*
@@ -215,80 +226,88 @@ function split(string, separator, limit, accumulator = []) {
215
226
  * @author Emanuel Rabina
216
227
  */
217
228
  class MessageFormatter {
218
- /**
219
- * Creates a new formatter that can work using any of the custom type handlers
220
- * you register.
221
- *
222
- * @param {string} locale
223
- * @param {Record<string,TypeHandler>} [typeHandlers]
224
- * Optional object where the keys are the names of the types to register,
225
- * their values being the functions that will return a nicely formatted
226
- * string for the data and locale they are given.
227
- */
228
- constructor(locale, typeHandlers = {}) {
229
- this.locale = locale;
230
- this.typeHandlers = typeHandlers;
231
- }
232
-
233
- /**
234
- * Formats an ICU message syntax string using `values` for placeholder data
235
- * and any currently-registered type handlers.
236
- *
237
- * @type {(message: string, values?: FormatValues) => string}
238
- */
239
- format = memoize((message, values = {}) => {
240
- return this.process(message, values).flat(Infinity).join('');
241
- });
242
-
243
- /**
244
- * Process an ICU message syntax string using `values` for placeholder data
245
- * and any currently-registered type handlers. The result of this method is
246
- * an array of the component parts after they have been processed in turn by
247
- * their own type handlers. This raw output is useful for other renderers,
248
- * eg: React where components can be used instead of being forced to return
249
- * raw strings.
250
- *
251
- * This method is used by {@link MessageFormatter#format} where it acts as a
252
- * string renderer.
253
- *
254
- * @param {string} message
255
- * @param {FormatValues} [values]
256
- * @return {any[]}
257
- */
258
- process(message, values = {}) {
259
- if (!message) {
260
- return [];
261
- }
262
- let blockStartIndex = message.indexOf('{');
263
- if (blockStartIndex !== -1) {
264
- let blockEndIndex = findClosingBracket(message, blockStartIndex);
265
- if (blockEndIndex !== -1) {
266
- let block = message.substring(blockStartIndex, blockEndIndex + 1);
267
- if (block) {
268
- let result = [];
269
- let head = message.substring(0, blockStartIndex);
270
- if (head) {
271
- result.push(head);
272
- }
273
- let [key, type, format] = splitFormattedArgument(block);
274
- let body = values[key];
275
- if (body === null || body === undefined) {
276
- body = '';
277
- }
278
- let typeHandler = type && this.typeHandlers[type];
279
- result.push(typeHandler ? typeHandler(body, format, this.locale, values, this.process.bind(this)) : body);
280
- let tail = message.substring(blockEndIndex + 1);
281
- if (tail) {
282
- result.push(this.process(tail, values));
283
- }
284
- return result;
285
- }
286
- } else {
287
- throw new Error(`Unbalanced curly braces in string: "${message}"`);
288
- }
289
- }
290
- return [message];
291
- }
229
+
230
+ /**
231
+ * Creates a new formatter that can work using any of the custom type handlers
232
+ * you register.
233
+ *
234
+ * @param {string} locale
235
+ * @param {Record<string,TypeHandler>} [typeHandlers]
236
+ * Optional object where the keys are the names of the types to register,
237
+ * their values being the functions that will return a nicely formatted
238
+ * string for the data and locale they are given.
239
+ */
240
+ constructor(locale, typeHandlers = {}) {
241
+
242
+ this.locale = locale;
243
+ this.typeHandlers = typeHandlers;
244
+ }
245
+
246
+ /**
247
+ * Formats an ICU message syntax string using `values` for placeholder data
248
+ * and any currently-registered type handlers.
249
+ *
250
+ * @type {(message: string, values?: FormatValues) => string}
251
+ */
252
+ format = memoize((message, values = {}) => {
253
+
254
+ return this.process(message, values).flat(Infinity).join('');
255
+ });
256
+
257
+ /**
258
+ * Process an ICU message syntax string using `values` for placeholder data
259
+ * and any currently-registered type handlers. The result of this method is
260
+ * an array of the component parts after they have been processed in turn by
261
+ * their own type handlers. This raw output is useful for other renderers,
262
+ * eg: React where components can be used instead of being forced to return
263
+ * raw strings.
264
+ *
265
+ * This method is used by {@link MessageFormatter#format} where it acts as a
266
+ * string renderer.
267
+ *
268
+ * @param {string} message
269
+ * @param {FormatValues} [values]
270
+ * @return {any[]}
271
+ */
272
+ process(message, values = {}) {
273
+
274
+ if (!message) {
275
+ return [];
276
+ }
277
+
278
+ let blockStartIndex = message.indexOf('{');
279
+ if (blockStartIndex !== -1) {
280
+ let blockEndIndex = findClosingBracket(message, blockStartIndex);
281
+ if (blockEndIndex !== -1) {
282
+ let block = message.substring(blockStartIndex, blockEndIndex + 1);
283
+ if (block) {
284
+ let result = [];
285
+ let head = message.substring(0, blockStartIndex);
286
+ if (head) {
287
+ result.push(head);
288
+ }
289
+ let [key, type, format] = splitFormattedArgument(block);
290
+ let body = values[key];
291
+ if (body === null || body === undefined) {
292
+ body = '';
293
+ }
294
+ let typeHandler = type && this.typeHandlers[type];
295
+ result.push(typeHandler ?
296
+ typeHandler(body, format, this.locale, values, this.process.bind(this)) :
297
+ body);
298
+ let tail = message.substring(blockEndIndex + 1);
299
+ if (tail) {
300
+ result.push(this.process(tail, values));
301
+ }
302
+ return result;
303
+ }
304
+ }
305
+ else {
306
+ throw new Error(`Unbalanced curly braces in string: "${message}"`);
307
+ }
308
+ }
309
+ return [message];
310
+ }
292
311
  }
293
312
 
294
313
  /*
@@ -307,11 +326,13 @@ class MessageFormatter {
307
326
  * limitations under the License.
308
327
  */
309
328
 
329
+
310
330
  let pluralFormatter;
331
+
311
332
  let keyCounter = 0;
312
333
 
313
334
  // All the special keywords that can be used in `plural` blocks for the various branches
314
- const ONE = 'one';
335
+ const ONE = 'one';
315
336
  const OTHER$1 = 'other';
316
337
 
317
338
  /**
@@ -321,29 +342,35 @@ const OTHER$1 = 'other';
321
342
  * @return {{caseBody: string, numberValues: object}}
322
343
  */
323
344
  function replaceNumberSign(caseBody, value) {
324
- let i = 0;
325
- let output = '';
326
- let numBraces = 0;
327
- const numberValues = {};
328
- while (i < caseBody.length) {
329
- if (caseBody[i] === '#' && !numBraces) {
330
- let keyParam = `__hashToken${keyCounter++}`;
331
- output += `{${keyParam}, number}`;
332
- numberValues[keyParam] = value;
333
- } else {
334
- output += caseBody[i];
335
- }
336
- if (caseBody[i] === '{') {
337
- numBraces++;
338
- } else if (caseBody[i] === '}') {
339
- numBraces--;
340
- }
341
- i++;
342
- }
343
- return {
344
- caseBody: output,
345
- numberValues
346
- };
345
+ let i = 0;
346
+ let output = '';
347
+ let numBraces = 0;
348
+ const numberValues = {};
349
+
350
+ while (i < caseBody.length) {
351
+ if (caseBody[i] === '#' && !numBraces) {
352
+ let keyParam = `__hashToken${keyCounter++}`;
353
+ output += `{${keyParam}, number}`;
354
+ numberValues[keyParam] = value;
355
+ }
356
+ else {
357
+ output += caseBody[i];
358
+ }
359
+
360
+ if (caseBody[i] === '{') {
361
+ numBraces++;
362
+ }
363
+ else if (caseBody[i] === '}') {
364
+ numBraces--;
365
+ }
366
+
367
+ i++;
368
+ }
369
+
370
+ return {
371
+ caseBody: output,
372
+ numberValues
373
+ };
347
374
  }
348
375
 
349
376
  /**
@@ -361,47 +388,48 @@ function replaceNumberSign(caseBody, value) {
361
388
  * @return {any | any[]}
362
389
  */
363
390
  function pluralTypeHandler(value, matches, locale, values, process) {
364
- const {
365
- args,
366
- cases
367
- } = parseCases(matches);
368
- let intValue = parseInt(value);
369
- args.forEach(arg => {
370
- if (arg.startsWith('offset:')) {
371
- intValue -= parseInt(arg.slice('offset:'.length));
372
- }
373
- });
374
- const keywordPossibilities = [];
375
- if ('PluralRules' in Intl) {
376
- // Effectively memoize because instantiation of `Int.*` objects is expensive.
377
- if (pluralFormatter === undefined || pluralFormatter.resolvedOptions().locale !== locale) {
378
- pluralFormatter = new Intl.PluralRules(locale);
379
- }
380
- const pluralKeyword = pluralFormatter.select(intValue);
381
-
382
- // Other is always added last with least priority, so we don't want to add it here.
383
- if (pluralKeyword !== OTHER$1) {
384
- keywordPossibilities.push(pluralKeyword);
385
- }
386
- }
387
- if (intValue === 1) {
388
- keywordPossibilities.push(ONE);
389
- }
390
- keywordPossibilities.push(`=${intValue}`, OTHER$1);
391
- for (let i = 0; i < keywordPossibilities.length; i++) {
392
- const keyword = keywordPossibilities[i];
393
- if (keyword in cases) {
394
- const {
395
- caseBody,
396
- numberValues
397
- } = replaceNumberSign(cases[keyword], intValue);
398
- return process(caseBody, {
399
- ...values,
400
- ...numberValues
401
- });
402
- }
403
- }
404
- return value;
391
+ const {args, cases} = parseCases(matches);
392
+
393
+ let intValue = parseInt(value);
394
+
395
+ args.forEach((arg) => {
396
+ if (arg.startsWith('offset:')) {
397
+ intValue -= parseInt(arg.slice('offset:'.length));
398
+ }
399
+ });
400
+
401
+ const keywordPossibilities = [];
402
+
403
+ if ('PluralRules' in Intl) {
404
+ // Effectively memoize because instantiation of `Int.*` objects is expensive.
405
+ if (pluralFormatter === undefined || pluralFormatter.resolvedOptions().locale !== locale) {
406
+ pluralFormatter = new Intl.PluralRules(locale);
407
+ }
408
+
409
+ const pluralKeyword = pluralFormatter.select(intValue);
410
+
411
+ // Other is always added last with least priority, so we don't want to add it here.
412
+ if (pluralKeyword !== OTHER$1) {
413
+ keywordPossibilities.push(pluralKeyword);
414
+ }
415
+ }
416
+ if (intValue === 1) {
417
+ keywordPossibilities.push(ONE);
418
+ }
419
+ keywordPossibilities.push(`=${intValue}`, OTHER$1);
420
+
421
+ for (let i = 0; i < keywordPossibilities.length; i++) {
422
+ const keyword = keywordPossibilities[i];
423
+ if (keyword in cases) {
424
+ const {caseBody, numberValues} = replaceNumberSign(cases[keyword], intValue);
425
+ return process(caseBody, {
426
+ ...values,
427
+ ...numberValues
428
+ });
429
+ }
430
+ }
431
+
432
+ return value;
405
433
  }
406
434
 
407
435
  /*
@@ -420,6 +448,7 @@ function pluralTypeHandler(value, matches, locale, values, process) {
420
448
  * limitations under the License.
421
449
  */
422
450
 
451
+
423
452
  const OTHER = 'other';
424
453
 
425
454
  /**
@@ -437,15 +466,16 @@ const OTHER = 'other';
437
466
  * @return {any | any[]}
438
467
  */
439
468
  function selectTypeHandler(value, matches, locale, values, process) {
440
- const {
441
- cases
442
- } = parseCases(matches);
443
- if (value in cases) {
444
- return process(cases[value], values);
445
- } else if (OTHER in cases) {
446
- return process(cases[OTHER], values);
447
- }
448
- return value;
469
+ const {cases} = parseCases(matches);
470
+
471
+ if (value in cases) {
472
+ return process(cases[value], values);
473
+ }
474
+ else if (OTHER in cases) {
475
+ return process(cases[OTHER], values);
476
+ }
477
+
478
+ return value;
449
479
  }
450
480
 
451
481
  export { MessageFormatter, findClosingBracket, parseCases, pluralTypeHandler, selectTypeHandler, splitFormattedArgument };