@ultraq/icu-message-formatter 0.14.3 → 0.15.0-beta.1

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.
@@ -1,485 +1,239 @@
1
- 'use strict';
2
-
3
- var functionUtils = require('@ultraq/function-utils');
4
-
5
- /*
6
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
7
- *
8
- * Licensed under the Apache License, Version 2.0 (the "License");
9
- * you may not use this file except in compliance with the License.
10
- * You may obtain a copy of the License at
11
- *
12
- * http://www.apache.org/licenses/LICENSE-2.0
13
- *
14
- * Unless required by applicable law or agreed to in writing, software
15
- * distributed under the License is distributed on an "AS IS" BASIS,
16
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- * See the License for the specific language governing permissions and
18
- * limitations under the License.
19
- */
20
-
21
- /**
22
- * @typedef ParseCasesResult
23
- * @property {string[]} args
24
- * A list of prepended arguments.
25
- * @property {Record<string,string>} cases
26
- * A map of all cases.
27
- */
28
-
29
- /**
30
- * Most branch-based type handlers are based around "cases". For example,
31
- * `select` and `plural` compare compare a value to "case keys" to choose a
32
- * subtranslation.
33
- *
34
- * This util splits "matches" portions provided to the aforementioned handlers
35
- * into case strings, and extracts any prepended arguments (for example,
36
- * `plural` supports an `offset:n` argument used for populating the magic `#`
37
- * variable).
38
- *
39
- * @param {string} string
40
- * @return {ParseCasesResult}
41
- */
42
- function parseCases(string = '') {
43
- const isWhitespace = ch => /\s/.test(ch);
44
-
45
- const args = [];
46
- const cases = {};
47
-
48
- let currTermStart = 0;
49
- let latestTerm = null;
50
- let inTerm = false;
51
-
52
- let i = 0;
53
- while (i < string.length) {
54
- // Term ended
55
- if (inTerm && (isWhitespace(string[i]) || string[i] === '{')) {
56
- inTerm = false;
57
- latestTerm = string.slice(currTermStart, i);
58
-
59
- // We want to process the opening char again so the case will be properly registered.
60
- if (string[i] === '{') {
61
- i--;
62
- }
63
- }
64
-
65
- // New term
66
- else if (!inTerm && !isWhitespace(string[i])) {
67
- const caseBody = string[i] === '{';
68
-
69
- // If there's a previous term, we can either handle a whole
70
- // case, or add that as an argument.
71
- if (latestTerm && caseBody) {
72
- const branchEndIndex = findClosingBracket(string, i);
73
-
74
- if (branchEndIndex === -1) {
75
- throw new Error(`Unbalanced curly braces in string: "${string}"`);
76
- }
77
-
78
- cases[latestTerm] = string.slice(i + 1, branchEndIndex); // Don't include the braces
79
-
80
- i = branchEndIndex; // Will be moved up where needed at end of loop.
81
- latestTerm = null;
82
- }
83
- else {
84
- if (latestTerm) {
85
- args.push(latestTerm);
86
- latestTerm = null;
87
- }
88
-
89
- inTerm = true;
90
- currTermStart = i;
91
- }
92
- }
93
- i++;
94
- }
95
-
96
- if (inTerm) {
97
- latestTerm = string.slice(currTermStart);
98
- }
99
-
100
- if (latestTerm) {
101
- args.push(latestTerm);
102
- }
103
-
104
- return {
105
- args,
106
- cases
107
- };
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
+ const functionUtils = require("@ultraq/function-utils");
7
+ function parseCases(string = "") {
8
+ const isWhitespace = (ch) => /\s/.test(ch);
9
+ const args = [];
10
+ const cases = {};
11
+ let currTermStart = 0;
12
+ let latestTerm = null;
13
+ let inTerm = false;
14
+ let i = 0;
15
+ while (i < string.length) {
16
+ if (inTerm && (isWhitespace(string[i]) || string[i] === "{")) {
17
+ inTerm = false;
18
+ latestTerm = string.slice(currTermStart, i);
19
+ if (string[i] === "{") {
20
+ i--;
21
+ }
22
+ } else if (!inTerm && !isWhitespace(string[i])) {
23
+ const caseBody = string[i] === "{";
24
+ if (latestTerm && caseBody) {
25
+ const branchEndIndex = findClosingBracket(string, i);
26
+ if (branchEndIndex === -1) {
27
+ throw new Error(`Unbalanced curly braces in string: "${string}"`);
28
+ }
29
+ cases[latestTerm] = string.slice(i + 1, branchEndIndex);
30
+ i = branchEndIndex;
31
+ latestTerm = null;
32
+ } else {
33
+ if (latestTerm) {
34
+ args.push(latestTerm);
35
+ latestTerm = null;
36
+ }
37
+ inTerm = true;
38
+ currTermStart = i;
39
+ }
40
+ }
41
+ i++;
42
+ }
43
+ if (inTerm) {
44
+ latestTerm = string.slice(currTermStart);
45
+ }
46
+ if (latestTerm) {
47
+ args.push(latestTerm);
48
+ }
49
+ return {
50
+ args,
51
+ cases
52
+ };
108
53
  }
109
-
110
- /**
111
- * Finds the index of the matching closing curly bracket, including through
112
- * strings that could have nested brackets.
113
- *
114
- * @param {string} string
115
- * @param {number} fromIndex
116
- * @return {number}
117
- * The index of the matching closing bracket, or -1 if no closing bracket
118
- * could be found.
119
- */
120
54
  function findClosingBracket(string, fromIndex) {
121
- let depth = 0;
122
- for (let i = fromIndex + 1; i < string.length; i++) {
123
- let char = string.charAt(i);
124
- if (char === '}') {
125
- if (depth === 0) {
126
- return i;
127
- }
128
- depth--;
129
- }
130
- else if (char === '{') {
131
- depth++;
132
- }
133
- }
134
- return -1;
55
+ let depth = 0;
56
+ for (let i = fromIndex + 1; i < string.length; i++) {
57
+ let char = string.charAt(i);
58
+ if (char === "}") {
59
+ if (depth === 0) {
60
+ return i;
61
+ }
62
+ depth--;
63
+ } else if (char === "{") {
64
+ depth++;
65
+ }
66
+ }
67
+ return -1;
135
68
  }
136
-
137
- /**
138
- * Split a `{key, type, format}` block into those 3 parts, taking into account
139
- * nested message syntax that can exist in the `format` part.
140
- *
141
- * @param {string} block
142
- * @return {string[]}
143
- * An array with `key`, `type`, and `format` items in that order, if present
144
- * in the formatted argument block.
145
- */
146
69
  function splitFormattedArgument(block) {
147
- return split(block.slice(1, -1), ',', 3);
70
+ return split(block.slice(1, -1), ",", 3);
148
71
  }
149
-
150
- /**
151
- * Like `String.prototype.split()` but where the limit parameter causes the
152
- * remainder of the string to be grouped together in a final entry.
153
- *
154
- * @private
155
- * @param {string} string
156
- * @param {string} separator
157
- * @param {number} limit
158
- * @param {string[]} accumulator
159
- * @return {string[]}
160
- */
161
72
  function split(string, separator, limit, accumulator = []) {
162
- if (!string) {
163
- return accumulator;
164
- }
165
- if (limit === 1) {
166
- accumulator.push(string);
167
- return accumulator;
168
- }
169
- let indexOfDelimiter = string.indexOf(separator);
170
- if (indexOfDelimiter === -1) {
171
- accumulator.push(string);
172
- return accumulator;
173
- }
174
- let head = string.substring(0, indexOfDelimiter).trim();
175
- let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
176
- accumulator.push(head);
177
- return split(tail, separator, limit - 1, accumulator);
73
+ if (!string) {
74
+ return accumulator;
75
+ }
76
+ if (limit === 1) {
77
+ accumulator.push(string);
78
+ return accumulator;
79
+ }
80
+ let indexOfDelimiter = string.indexOf(separator);
81
+ if (indexOfDelimiter === -1) {
82
+ accumulator.push(string);
83
+ return accumulator;
84
+ }
85
+ let head = string.substring(0, indexOfDelimiter).trim();
86
+ let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
87
+ accumulator.push(head);
88
+ return split(tail, separator, limit - 1, accumulator);
178
89
  }
179
-
180
- /*
181
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
182
- *
183
- * Licensed under the Apache License, Version 2.0 (the "License");
184
- * you may not use this file except in compliance with the License.
185
- * You may obtain a copy of the License at
186
- *
187
- * http://www.apache.org/licenses/LICENSE-2.0
188
- *
189
- * Unless required by applicable law or agreed to in writing, software
190
- * distributed under the License is distributed on an "AS IS" BASIS,
191
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
192
- * See the License for the specific language governing permissions and
193
- * limitations under the License.
194
- */
195
-
196
-
197
- /**
198
- * @typedef {Record<string,any>} FormatValues
199
- */
200
-
201
- /**
202
- * @callback ProcessFunction
203
- * @param {string} message
204
- * @param {FormatValues} [values={}]
205
- * @return {any[]}
206
- */
207
-
208
- /**
209
- * @callback TypeHandler
210
- * @param {any} value
211
- * The object which matched the key of the block being processed.
212
- * @param {string} matches
213
- * Any format options associated with the block being processed.
214
- * @param {string} locale
215
- * The locale to use for formatting.
216
- * @param {FormatValues} values
217
- * The object of placeholder data given to the original `format`/`process`
218
- * call.
219
- * @param {ProcessFunction} process
220
- * The `process` function itself so that sub-messages can be processed by type
221
- * handlers.
222
- * @return {any | any[]}
223
- */
224
-
225
- /**
226
- * The main class for formatting messages.
227
- *
228
- * @author Emanuel Rabina
229
- */
230
90
  class MessageFormatter {
231
-
232
- /**
233
- * Creates a new formatter that can work using any of the custom type handlers
234
- * you register.
235
- *
236
- * @param {string} locale
237
- * @param {Record<string,TypeHandler>} [typeHandlers]
238
- * Optional object where the keys are the names of the types to register,
239
- * their values being the functions that will return a nicely formatted
240
- * string for the data and locale they are given.
241
- */
242
- constructor(locale, typeHandlers = {}) {
243
-
244
- this.locale = locale;
245
- this.typeHandlers = typeHandlers;
246
- }
247
-
248
- /**
249
- * Formats an ICU message syntax string using `values` for placeholder data
250
- * and any currently-registered type handlers.
251
- *
252
- * @type {(message: string, values?: FormatValues) => string}
253
- */
254
- format = functionUtils.memoize((message, values = {}) => {
255
-
256
- return this.process(message, values).flat(Infinity).join('');
257
- });
258
-
259
- /**
260
- * Process an ICU message syntax string using `values` for placeholder data
261
- * and any currently-registered type handlers. The result of this method is
262
- * an array of the component parts after they have been processed in turn by
263
- * their own type handlers. This raw output is useful for other renderers,
264
- * eg: React where components can be used instead of being forced to return
265
- * raw strings.
266
- *
267
- * This method is used by {@link MessageFormatter#format} where it acts as a
268
- * string renderer.
269
- *
270
- * @param {string} message
271
- * @param {FormatValues} [values]
272
- * @return {any[]}
273
- */
274
- process(message, values = {}) {
275
-
276
- if (!message) {
277
- return [];
278
- }
279
-
280
- let blockStartIndex = message.indexOf('{');
281
- if (blockStartIndex !== -1) {
282
- let blockEndIndex = findClosingBracket(message, blockStartIndex);
283
- if (blockEndIndex !== -1) {
284
- let block = message.substring(blockStartIndex, blockEndIndex + 1);
285
- if (block) {
286
- let result = [];
287
- let head = message.substring(0, blockStartIndex);
288
- if (head) {
289
- result.push(head);
290
- }
291
- let [key, type, format] = splitFormattedArgument(block);
292
- let body = values[key];
293
- if (body === null || body === undefined) {
294
- body = '';
295
- }
296
- let typeHandler = type && this.typeHandlers[type];
297
- result.push(typeHandler ?
298
- typeHandler(body, format, this.locale, values, this.process.bind(this)) :
299
- body);
300
- let tail = message.substring(blockEndIndex + 1);
301
- if (tail) {
302
- result.push(this.process(tail, values));
303
- }
304
- return result;
305
- }
306
- }
307
- else {
308
- throw new Error(`Unbalanced curly braces in string: "${message}"`);
309
- }
310
- }
311
- return [message];
312
- }
91
+ /**
92
+ * Creates a new formatter that can work using any of the custom type handlers
93
+ * you register.
94
+ *
95
+ * @param {string} locale
96
+ * @param {Record<string,TypeHandler>} [typeHandlers]
97
+ * Optional object where the keys are the names of the types to register,
98
+ * their values being the functions that will return a nicely formatted
99
+ * string for the data and locale they are given.
100
+ */
101
+ constructor(locale, typeHandlers = {}) {
102
+ /**
103
+ * Formats an ICU message syntax string using `values` for placeholder data
104
+ * and any currently-registered type handlers.
105
+ *
106
+ * @type {(message: string, values?: FormatValues) => string}
107
+ */
108
+ __publicField(this, "format", functionUtils.memoize((message, values = {}) => {
109
+ return this.process(message, values).flat(Infinity).join("");
110
+ }));
111
+ this.locale = locale;
112
+ this.typeHandlers = typeHandlers;
113
+ }
114
+ /**
115
+ * Process an ICU message syntax string using `values` for placeholder data
116
+ * and any currently-registered type handlers. The result of this method is
117
+ * an array of the component parts after they have been processed in turn by
118
+ * their own type handlers. This raw output is useful for other renderers,
119
+ * eg: React where components can be used instead of being forced to return
120
+ * raw strings.
121
+ *
122
+ * This method is used by {@link MessageFormatter#format} where it acts as a
123
+ * string renderer.
124
+ *
125
+ * @param {string} message
126
+ * @param {FormatValues} [values]
127
+ * @return {any[]}
128
+ */
129
+ process(message, values = {}) {
130
+ if (!message) {
131
+ return [];
132
+ }
133
+ let blockStartIndex = message.indexOf("{");
134
+ if (blockStartIndex !== -1) {
135
+ let blockEndIndex = findClosingBracket(message, blockStartIndex);
136
+ if (blockEndIndex !== -1) {
137
+ let block = message.substring(blockStartIndex, blockEndIndex + 1);
138
+ if (block) {
139
+ let result = [];
140
+ let head = message.substring(0, blockStartIndex);
141
+ if (head) {
142
+ result.push(head);
143
+ }
144
+ let [key, type, format] = splitFormattedArgument(block);
145
+ let body = values[key];
146
+ if (body === null || body === void 0) {
147
+ body = "";
148
+ }
149
+ let typeHandler = type && this.typeHandlers[type];
150
+ result.push(typeHandler ? typeHandler(body, format, this.locale, values, this.process.bind(this)) : body);
151
+ let tail = message.substring(blockEndIndex + 1);
152
+ if (tail) {
153
+ result.push(this.process(tail, values));
154
+ }
155
+ return result;
156
+ }
157
+ } else {
158
+ throw new Error(`Unbalanced curly braces in string: "${message}"`);
159
+ }
160
+ }
161
+ return [message];
162
+ }
313
163
  }
314
-
315
- /*
316
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
317
- *
318
- * Licensed under the Apache License, Version 2.0 (the "License");
319
- * you may not use this file except in compliance with the License.
320
- * You may obtain a copy of the License at
321
- *
322
- * http://www.apache.org/licenses/LICENSE-2.0
323
- *
324
- * Unless required by applicable law or agreed to in writing, software
325
- * distributed under the License is distributed on an "AS IS" BASIS,
326
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
327
- * See the License for the specific language governing permissions and
328
- * limitations under the License.
329
- */
330
-
331
-
332
164
  let pluralFormatter;
333
-
334
165
  let keyCounter = 0;
335
-
336
- // All the special keywords that can be used in `plural` blocks for the various branches
337
- const ONE = 'one';
338
- const OTHER$1 = 'other';
339
-
340
- /**
341
- * @private
342
- * @param {string} caseBody
343
- * @param {number} value
344
- * @return {{caseBody: string, numberValues: object}}
345
- */
166
+ const ONE = "one";
167
+ const OTHER$1 = "other";
346
168
  function replaceNumberSign(caseBody, value) {
347
- let i = 0;
348
- let output = '';
349
- let numBraces = 0;
350
- const numberValues = {};
351
-
352
- while (i < caseBody.length) {
353
- if (caseBody[i] === '#' && !numBraces) {
354
- let keyParam = `__hashToken${keyCounter++}`;
355
- output += `{${keyParam}, number}`;
356
- numberValues[keyParam] = value;
357
- }
358
- else {
359
- output += caseBody[i];
360
- }
361
-
362
- if (caseBody[i] === '{') {
363
- numBraces++;
364
- }
365
- else if (caseBody[i] === '}') {
366
- numBraces--;
367
- }
368
-
369
- i++;
370
- }
371
-
372
- return {
373
- caseBody: output,
374
- numberValues
375
- };
169
+ let i = 0;
170
+ let output = "";
171
+ let numBraces = 0;
172
+ const numberValues = {};
173
+ while (i < caseBody.length) {
174
+ if (caseBody[i] === "#" && !numBraces) {
175
+ let keyParam = `__hashToken${keyCounter++}`;
176
+ output += `{${keyParam}, number}`;
177
+ numberValues[keyParam] = value;
178
+ } else {
179
+ output += caseBody[i];
180
+ }
181
+ if (caseBody[i] === "{") {
182
+ numBraces++;
183
+ } else if (caseBody[i] === "}") {
184
+ numBraces--;
185
+ }
186
+ i++;
187
+ }
188
+ return {
189
+ caseBody: output,
190
+ numberValues
191
+ };
376
192
  }
377
-
378
- /**
379
- * Handler for `plural` statements within ICU message syntax strings. Returns
380
- * a formatted string for the branch that closely matches the current value.
381
- *
382
- * See https://formatjs.io/docs/core-concepts/icu-syntax#plural-format for more
383
- * details on how the `plural` statement works.
384
- *
385
- * @param {string} value
386
- * @param {string} matches
387
- * @param {string} locale
388
- * @param {import('./MessageFormatter.js').FormatValues} values
389
- * @param {import('./MessageFormatter.js').ProcessFunction} process
390
- * @return {any | any[]}
391
- */
392
193
  function pluralTypeHandler(value, matches, locale, values, process) {
393
- const {args, cases} = parseCases(matches);
394
-
395
- let intValue = parseInt(value);
396
-
397
- args.forEach((arg) => {
398
- if (arg.startsWith('offset:')) {
399
- intValue -= parseInt(arg.slice('offset:'.length));
400
- }
401
- });
402
-
403
- const keywordPossibilities = [];
404
-
405
- if ('PluralRules' in Intl) {
406
- // Effectively memoize because instantiation of `Int.*` objects is expensive.
407
- if (pluralFormatter === undefined || pluralFormatter.resolvedOptions().locale !== locale) {
408
- pluralFormatter = new Intl.PluralRules(locale);
409
- }
410
-
411
- const pluralKeyword = pluralFormatter.select(intValue);
412
-
413
- // Other is always added last with least priority, so we don't want to add it here.
414
- if (pluralKeyword !== OTHER$1) {
415
- keywordPossibilities.push(pluralKeyword);
416
- }
417
- }
418
- if (intValue === 1) {
419
- keywordPossibilities.push(ONE);
420
- }
421
- keywordPossibilities.push(`=${intValue}`, OTHER$1);
422
-
423
- for (let i = 0; i < keywordPossibilities.length; i++) {
424
- const keyword = keywordPossibilities[i];
425
- if (keyword in cases) {
426
- const {caseBody, numberValues} = replaceNumberSign(cases[keyword], intValue);
427
- return process(caseBody, {
428
- ...values,
429
- ...numberValues
430
- });
431
- }
432
- }
433
-
434
- return value;
194
+ const { args, cases } = parseCases(matches);
195
+ let intValue = parseInt(value);
196
+ args.forEach((arg) => {
197
+ if (arg.startsWith("offset:")) {
198
+ intValue -= parseInt(arg.slice("offset:".length));
199
+ }
200
+ });
201
+ const keywordPossibilities = [];
202
+ if ("PluralRules" in Intl) {
203
+ if (pluralFormatter === void 0 || pluralFormatter.resolvedOptions().locale !== locale) {
204
+ pluralFormatter = new Intl.PluralRules(locale);
205
+ }
206
+ const pluralKeyword = pluralFormatter.select(intValue);
207
+ if (pluralKeyword !== OTHER$1) {
208
+ keywordPossibilities.push(pluralKeyword);
209
+ }
210
+ }
211
+ if (intValue === 1) {
212
+ keywordPossibilities.push(ONE);
213
+ }
214
+ keywordPossibilities.push(`=${intValue}`, OTHER$1);
215
+ for (let i = 0; i < keywordPossibilities.length; i++) {
216
+ const keyword = keywordPossibilities[i];
217
+ if (keyword in cases) {
218
+ const { caseBody, numberValues } = replaceNumberSign(cases[keyword], intValue);
219
+ return process(caseBody, {
220
+ ...values,
221
+ ...numberValues
222
+ });
223
+ }
224
+ }
225
+ return value;
435
226
  }
436
-
437
- /*
438
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
439
- *
440
- * Licensed under the Apache License, Version 2.0 (the "License");
441
- * you may not use this file except in compliance with the License.
442
- * You may obtain a copy of the License at
443
- *
444
- * http://www.apache.org/licenses/LICENSE-2.0
445
- *
446
- * Unless required by applicable law or agreed to in writing, software
447
- * distributed under the License is distributed on an "AS IS" BASIS,
448
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
449
- * See the License for the specific language governing permissions and
450
- * limitations under the License.
451
- */
452
-
453
-
454
- const OTHER = 'other';
455
-
456
- /**
457
- * Handler for `select` statements within ICU message syntax strings. Returns
458
- * a formatted string for the branch that closely matches the current value.
459
- *
460
- * See https://formatjs.io/docs/core-concepts/icu-syntax#select-format for more
461
- * details on how the `select` statement works.
462
- *
463
- * @param {string} value
464
- * @param {string} matches
465
- * @param {string} locale
466
- * @param {import('./MessageFormatter.js').FormatValues} values
467
- * @param {import('./MessageFormatter.js').ProcessFunction} process
468
- * @return {any | any[]}
469
- */
227
+ const OTHER = "other";
470
228
  function selectTypeHandler(value, matches, locale, values, process) {
471
- const {cases} = parseCases(matches);
472
-
473
- if (value in cases) {
474
- return process(cases[value], values);
475
- }
476
- else if (OTHER in cases) {
477
- return process(cases[OTHER], values);
478
- }
479
-
480
- return value;
229
+ const { cases } = parseCases(matches);
230
+ if (value in cases) {
231
+ return process(cases[value], values);
232
+ } else if (OTHER in cases) {
233
+ return process(cases[OTHER], values);
234
+ }
235
+ return value;
481
236
  }
482
-
483
237
  exports.MessageFormatter = MessageFormatter;
484
238
  exports.findClosingBracket = findClosingBracket;
485
239
  exports.parseCases = parseCases;