@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,482 +1,243 @@
1
- import { memoize } from '@ultraq/function-utils';
2
-
3
- /*
4
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
5
- *
6
- * Licensed under the Apache License, Version 2.0 (the "License");
7
- * you may not use this file except in compliance with the License.
8
- * You may obtain a copy of the License at
9
- *
10
- * http://www.apache.org/licenses/LICENSE-2.0
11
- *
12
- * Unless required by applicable law or agreed to in writing, software
13
- * distributed under the License is distributed on an "AS IS" BASIS,
14
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
- * See the License for the specific language governing permissions and
16
- * limitations under the License.
17
- */
18
-
19
- /**
20
- * @typedef ParseCasesResult
21
- * @property {string[]} args
22
- * A list of prepended arguments.
23
- * @property {Record<string,string>} cases
24
- * A map of all cases.
25
- */
26
-
27
- /**
28
- * Most branch-based type handlers are based around "cases". For example,
29
- * `select` and `plural` compare compare a value to "case keys" to choose a
30
- * subtranslation.
31
- *
32
- * This util splits "matches" portions provided to the aforementioned handlers
33
- * into case strings, and extracts any prepended arguments (for example,
34
- * `plural` supports an `offset:n` argument used for populating the magic `#`
35
- * variable).
36
- *
37
- * @param {string} string
38
- * @return {ParseCasesResult}
39
- */
40
- function parseCases(string = '') {
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
- };
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { memoize } from "@ultraq/function-utils";
5
+ function parseCases(string = "") {
6
+ const isWhitespace = (ch) => /\s/.test(ch);
7
+ const args = [];
8
+ const cases = {};
9
+ let currTermStart = 0;
10
+ let latestTerm = null;
11
+ let inTerm = false;
12
+ let i = 0;
13
+ while (i < string.length) {
14
+ if (inTerm && (isWhitespace(string[i]) || string[i] === "{")) {
15
+ inTerm = false;
16
+ latestTerm = string.slice(currTermStart, i);
17
+ if (string[i] === "{") {
18
+ i--;
19
+ }
20
+ } else if (!inTerm && !isWhitespace(string[i])) {
21
+ const caseBody = string[i] === "{";
22
+ if (latestTerm && caseBody) {
23
+ const branchEndIndex = findClosingBracket(string, i);
24
+ if (branchEndIndex === -1) {
25
+ throw new Error(`Unbalanced curly braces in string: "${string}"`);
26
+ }
27
+ cases[latestTerm] = string.slice(i + 1, branchEndIndex);
28
+ i = branchEndIndex;
29
+ latestTerm = null;
30
+ } else {
31
+ if (latestTerm) {
32
+ args.push(latestTerm);
33
+ latestTerm = null;
34
+ }
35
+ inTerm = true;
36
+ currTermStart = i;
37
+ }
38
+ }
39
+ i++;
40
+ }
41
+ if (inTerm) {
42
+ latestTerm = string.slice(currTermStart);
43
+ }
44
+ if (latestTerm) {
45
+ args.push(latestTerm);
46
+ }
47
+ return {
48
+ args,
49
+ cases
50
+ };
106
51
  }
107
-
108
- /**
109
- * Finds the index of the matching closing curly bracket, including through
110
- * strings that could have nested brackets.
111
- *
112
- * @param {string} string
113
- * @param {number} fromIndex
114
- * @return {number}
115
- * The index of the matching closing bracket, or -1 if no closing bracket
116
- * could be found.
117
- */
118
52
  function findClosingBracket(string, fromIndex) {
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;
53
+ let depth = 0;
54
+ for (let i = fromIndex + 1; i < string.length; i++) {
55
+ let char = string.charAt(i);
56
+ if (char === "}") {
57
+ if (depth === 0) {
58
+ return i;
59
+ }
60
+ depth--;
61
+ } else if (char === "{") {
62
+ depth++;
63
+ }
64
+ }
65
+ return -1;
133
66
  }
134
-
135
- /**
136
- * Split a `{key, type, format}` block into those 3 parts, taking into account
137
- * nested message syntax that can exist in the `format` part.
138
- *
139
- * @param {string} block
140
- * @return {string[]}
141
- * An array with `key`, `type`, and `format` items in that order, if present
142
- * in the formatted argument block.
143
- */
144
67
  function splitFormattedArgument(block) {
145
- return split(block.slice(1, -1), ',', 3);
68
+ return split(block.slice(1, -1), ",", 3);
146
69
  }
147
-
148
- /**
149
- * Like `String.prototype.split()` but where the limit parameter causes the
150
- * remainder of the string to be grouped together in a final entry.
151
- *
152
- * @private
153
- * @param {string} string
154
- * @param {string} separator
155
- * @param {number} limit
156
- * @param {string[]} accumulator
157
- * @return {string[]}
158
- */
159
70
  function split(string, separator, limit, 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);
71
+ if (!string) {
72
+ return accumulator;
73
+ }
74
+ if (limit === 1) {
75
+ accumulator.push(string);
76
+ return accumulator;
77
+ }
78
+ let indexOfDelimiter = string.indexOf(separator);
79
+ if (indexOfDelimiter === -1) {
80
+ accumulator.push(string);
81
+ return accumulator;
82
+ }
83
+ let head = string.substring(0, indexOfDelimiter).trim();
84
+ let tail = string.substring(indexOfDelimiter + separator.length + 1).trim();
85
+ accumulator.push(head);
86
+ return split(tail, separator, limit - 1, accumulator);
176
87
  }
177
-
178
- /*
179
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
180
- *
181
- * Licensed under the Apache License, Version 2.0 (the "License");
182
- * you may not use this file except in compliance with the License.
183
- * You may obtain a copy of the License at
184
- *
185
- * http://www.apache.org/licenses/LICENSE-2.0
186
- *
187
- * Unless required by applicable law or agreed to in writing, software
188
- * distributed under the License is distributed on an "AS IS" BASIS,
189
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
- * See the License for the specific language governing permissions and
191
- * limitations under the License.
192
- */
193
-
194
-
195
- /**
196
- * @typedef {Record<string,any>} FormatValues
197
- */
198
-
199
- /**
200
- * @callback ProcessFunction
201
- * @param {string} message
202
- * @param {FormatValues} [values={}]
203
- * @return {any[]}
204
- */
205
-
206
- /**
207
- * @callback TypeHandler
208
- * @param {any} value
209
- * The object which matched the key of the block being processed.
210
- * @param {string} matches
211
- * Any format options associated with the block being processed.
212
- * @param {string} locale
213
- * The locale to use for formatting.
214
- * @param {FormatValues} values
215
- * The object of placeholder data given to the original `format`/`process`
216
- * call.
217
- * @param {ProcessFunction} process
218
- * The `process` function itself so that sub-messages can be processed by type
219
- * handlers.
220
- * @return {any | any[]}
221
- */
222
-
223
- /**
224
- * The main class for formatting messages.
225
- *
226
- * @author Emanuel Rabina
227
- */
228
88
  class MessageFormatter {
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
- }
89
+ /**
90
+ * Creates a new formatter that can work using any of the custom type handlers
91
+ * you register.
92
+ *
93
+ * @param {string} locale
94
+ * @param {Record<string,TypeHandler>} [typeHandlers]
95
+ * Optional object where the keys are the names of the types to register,
96
+ * their values being the functions that will return a nicely formatted
97
+ * string for the data and locale they are given.
98
+ */
99
+ constructor(locale, typeHandlers = {}) {
100
+ /**
101
+ * Formats an ICU message syntax string using `values` for placeholder data
102
+ * and any currently-registered type handlers.
103
+ *
104
+ * @type {(message: string, values?: FormatValues) => string}
105
+ */
106
+ __publicField(this, "format", memoize((message, values = {}) => {
107
+ return this.process(message, values).flat(Infinity).join("");
108
+ }));
109
+ this.locale = locale;
110
+ this.typeHandlers = typeHandlers;
111
+ }
112
+ /**
113
+ * Process an ICU message syntax string using `values` for placeholder data
114
+ * and any currently-registered type handlers. The result of this method is
115
+ * an array of the component parts after they have been processed in turn by
116
+ * their own type handlers. This raw output is useful for other renderers,
117
+ * eg: React where components can be used instead of being forced to return
118
+ * raw strings.
119
+ *
120
+ * This method is used by {@link MessageFormatter#format} where it acts as a
121
+ * string renderer.
122
+ *
123
+ * @param {string} message
124
+ * @param {FormatValues} [values]
125
+ * @return {any[]}
126
+ */
127
+ process(message, values = {}) {
128
+ if (!message) {
129
+ return [];
130
+ }
131
+ let blockStartIndex = message.indexOf("{");
132
+ if (blockStartIndex !== -1) {
133
+ let blockEndIndex = findClosingBracket(message, blockStartIndex);
134
+ if (blockEndIndex !== -1) {
135
+ let block = message.substring(blockStartIndex, blockEndIndex + 1);
136
+ if (block) {
137
+ let result = [];
138
+ let head = message.substring(0, blockStartIndex);
139
+ if (head) {
140
+ result.push(head);
141
+ }
142
+ let [key, type, format] = splitFormattedArgument(block);
143
+ let body = values[key];
144
+ if (body === null || body === void 0) {
145
+ body = "";
146
+ }
147
+ let typeHandler = type && this.typeHandlers[type];
148
+ result.push(typeHandler ? typeHandler(body, format, this.locale, values, this.process.bind(this)) : body);
149
+ let tail = message.substring(blockEndIndex + 1);
150
+ if (tail) {
151
+ result.push(this.process(tail, values));
152
+ }
153
+ return result;
154
+ }
155
+ } else {
156
+ throw new Error(`Unbalanced curly braces in string: "${message}"`);
157
+ }
158
+ }
159
+ return [message];
160
+ }
311
161
  }
312
-
313
- /*
314
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
315
- *
316
- * Licensed under the Apache License, Version 2.0 (the "License");
317
- * you may not use this file except in compliance with the License.
318
- * You may obtain a copy of the License at
319
- *
320
- * http://www.apache.org/licenses/LICENSE-2.0
321
- *
322
- * Unless required by applicable law or agreed to in writing, software
323
- * distributed under the License is distributed on an "AS IS" BASIS,
324
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
325
- * See the License for the specific language governing permissions and
326
- * limitations under the License.
327
- */
328
-
329
-
330
162
  let pluralFormatter;
331
-
332
163
  let keyCounter = 0;
333
-
334
- // All the special keywords that can be used in `plural` blocks for the various branches
335
- const ONE = 'one';
336
- const OTHER$1 = 'other';
337
-
338
- /**
339
- * @private
340
- * @param {string} caseBody
341
- * @param {number} value
342
- * @return {{caseBody: string, numberValues: object}}
343
- */
164
+ const ONE = "one";
165
+ const OTHER$1 = "other";
344
166
  function replaceNumberSign(caseBody, value) {
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
- };
167
+ let i = 0;
168
+ let output = "";
169
+ let numBraces = 0;
170
+ const numberValues = {};
171
+ while (i < caseBody.length) {
172
+ if (caseBody[i] === "#" && !numBraces) {
173
+ let keyParam = `__hashToken${keyCounter++}`;
174
+ output += `{${keyParam}, number}`;
175
+ numberValues[keyParam] = value;
176
+ } else {
177
+ output += caseBody[i];
178
+ }
179
+ if (caseBody[i] === "{") {
180
+ numBraces++;
181
+ } else if (caseBody[i] === "}") {
182
+ numBraces--;
183
+ }
184
+ i++;
185
+ }
186
+ return {
187
+ caseBody: output,
188
+ numberValues
189
+ };
374
190
  }
375
-
376
- /**
377
- * Handler for `plural` statements within ICU message syntax strings. Returns
378
- * a formatted string for the branch that closely matches the current value.
379
- *
380
- * See https://formatjs.io/docs/core-concepts/icu-syntax#plural-format for more
381
- * details on how the `plural` statement works.
382
- *
383
- * @param {string} value
384
- * @param {string} matches
385
- * @param {string} locale
386
- * @param {import('./MessageFormatter.js').FormatValues} values
387
- * @param {import('./MessageFormatter.js').ProcessFunction} process
388
- * @return {any | any[]}
389
- */
390
191
  function pluralTypeHandler(value, matches, locale, values, process) {
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;
192
+ const { args, cases } = parseCases(matches);
193
+ let intValue = parseInt(value);
194
+ args.forEach((arg) => {
195
+ if (arg.startsWith("offset:")) {
196
+ intValue -= parseInt(arg.slice("offset:".length));
197
+ }
198
+ });
199
+ const keywordPossibilities = [];
200
+ if ("PluralRules" in Intl) {
201
+ if (pluralFormatter === void 0 || pluralFormatter.resolvedOptions().locale !== locale) {
202
+ pluralFormatter = new Intl.PluralRules(locale);
203
+ }
204
+ const pluralKeyword = pluralFormatter.select(intValue);
205
+ if (pluralKeyword !== OTHER$1) {
206
+ keywordPossibilities.push(pluralKeyword);
207
+ }
208
+ }
209
+ if (intValue === 1) {
210
+ keywordPossibilities.push(ONE);
211
+ }
212
+ keywordPossibilities.push(`=${intValue}`, OTHER$1);
213
+ for (let i = 0; i < keywordPossibilities.length; i++) {
214
+ const keyword = keywordPossibilities[i];
215
+ if (keyword in cases) {
216
+ const { caseBody, numberValues } = replaceNumberSign(cases[keyword], intValue);
217
+ return process(caseBody, {
218
+ ...values,
219
+ ...numberValues
220
+ });
221
+ }
222
+ }
223
+ return value;
433
224
  }
434
-
435
- /*
436
- * Copyright 2019, Emanuel Rabina (http://www.ultraq.net.nz/)
437
- *
438
- * Licensed under the Apache License, Version 2.0 (the "License");
439
- * you may not use this file except in compliance with the License.
440
- * You may obtain a copy of the License at
441
- *
442
- * http://www.apache.org/licenses/LICENSE-2.0
443
- *
444
- * Unless required by applicable law or agreed to in writing, software
445
- * distributed under the License is distributed on an "AS IS" BASIS,
446
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
447
- * See the License for the specific language governing permissions and
448
- * limitations under the License.
449
- */
450
-
451
-
452
- const OTHER = 'other';
453
-
454
- /**
455
- * Handler for `select` statements within ICU message syntax strings. Returns
456
- * a formatted string for the branch that closely matches the current value.
457
- *
458
- * See https://formatjs.io/docs/core-concepts/icu-syntax#select-format for more
459
- * details on how the `select` statement works.
460
- *
461
- * @param {string} value
462
- * @param {string} matches
463
- * @param {string} locale
464
- * @param {import('./MessageFormatter.js').FormatValues} values
465
- * @param {import('./MessageFormatter.js').ProcessFunction} process
466
- * @return {any | any[]}
467
- */
225
+ const OTHER = "other";
468
226
  function selectTypeHandler(value, matches, locale, values, process) {
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;
227
+ const { cases } = parseCases(matches);
228
+ if (value in cases) {
229
+ return process(cases[value], values);
230
+ } else if (OTHER in cases) {
231
+ return process(cases[OTHER], values);
232
+ }
233
+ return value;
479
234
  }
480
-
481
- export { MessageFormatter, findClosingBracket, parseCases, pluralTypeHandler, selectTypeHandler, splitFormattedArgument };
235
+ export {
236
+ MessageFormatter,
237
+ findClosingBracket,
238
+ parseCases,
239
+ pluralTypeHandler,
240
+ selectTypeHandler,
241
+ splitFormattedArgument
242
+ };
482
243
  //# sourceMappingURL=icu-message-formatter.js.map