intl-messageformat 7.8.4 → 8.2.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.
- package/CHANGELOG.md +65 -0
- package/README.md +19 -16
- package/dist/core.d.ts +3 -4
- package/dist/core.js +20 -4
- package/dist/error.d.ts +16 -0
- package/dist/error.js +44 -0
- package/dist/formatters.d.ts +7 -9
- package/dist/formatters.js +27 -169
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/umd/intl-messageformat.js +767 -416
- package/dist/umd/intl-messageformat.js.map +1 -1
- package/dist/umd/intl-messageformat.min.js +1 -1
- package/dist/umd/intl-messageformat.min.js.map +1 -1
- package/lib/core.d.ts +3 -4
- package/lib/core.js +21 -5
- package/lib/error.d.ts +16 -0
- package/lib/error.js +42 -0
- package/lib/formatters.d.ts +7 -9
- package/lib/formatters.js +28 -168
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/intl-messageformat.d.ts +30 -15
- package/package.json +3 -3
- package/src/core.ts +29 -24
- package/src/error.ts +39 -0
- package/src/formatters.ts +69 -245
- package/src/index.ts +1 -0
package/src/formatters.ts
CHANGED
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
isTimeElement,
|
|
13
13
|
MessageFormatElement,
|
|
14
14
|
parseDateTimeSkeleton,
|
|
15
|
+
isTagElement,
|
|
15
16
|
} from 'intl-messageformat-parser';
|
|
17
|
+
import {
|
|
18
|
+
MissingValueError,
|
|
19
|
+
InvalidValueError,
|
|
20
|
+
ErrorCode,
|
|
21
|
+
FormatError,
|
|
22
|
+
} from './error';
|
|
16
23
|
|
|
17
24
|
export interface Formats {
|
|
18
25
|
number: Record<string, Intl.NumberFormatOptions>;
|
|
@@ -40,7 +47,7 @@ export interface Formatters {
|
|
|
40
47
|
|
|
41
48
|
export const enum PART_TYPE {
|
|
42
49
|
literal,
|
|
43
|
-
|
|
50
|
+
object,
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export interface LiteralPart {
|
|
@@ -48,24 +55,18 @@ export interface LiteralPart {
|
|
|
48
55
|
value: string;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
export interface
|
|
52
|
-
type: PART_TYPE.
|
|
53
|
-
value:
|
|
58
|
+
export interface ObjectPart<T = any> {
|
|
59
|
+
type: PART_TYPE.object;
|
|
60
|
+
value: T;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
export type MessageFormatPart = LiteralPart |
|
|
63
|
+
export type MessageFormatPart<T> = LiteralPart | ObjectPart<T>;
|
|
57
64
|
|
|
58
65
|
export type PrimitiveType = string | number | boolean | null | undefined | Date;
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
super(msg);
|
|
64
|
-
this.variableId = variableId;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function mergeLiteral(parts: MessageFormatPart[]): MessageFormatPart[] {
|
|
67
|
+
function mergeLiteral<T>(
|
|
68
|
+
parts: MessageFormatPart<T>[]
|
|
69
|
+
): MessageFormatPart<T>[] {
|
|
69
70
|
if (parts.length < 2) {
|
|
70
71
|
return parts;
|
|
71
72
|
}
|
|
@@ -81,20 +82,26 @@ function mergeLiteral(parts: MessageFormatPart[]): MessageFormatPart[] {
|
|
|
81
82
|
lastPart.value += part.value;
|
|
82
83
|
}
|
|
83
84
|
return all;
|
|
84
|
-
}, [] as MessageFormatPart[]);
|
|
85
|
+
}, [] as MessageFormatPart<T>[]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isFormatXMLElementFn<T>(
|
|
89
|
+
el: PrimitiveType | T | FormatXMLElementFn<T>
|
|
90
|
+
): el is FormatXMLElementFn<T> {
|
|
91
|
+
return typeof el === 'function';
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
// TODO(skeleton): add skeleton support
|
|
88
|
-
export function formatToParts(
|
|
95
|
+
export function formatToParts<T>(
|
|
89
96
|
els: MessageFormatElement[],
|
|
90
97
|
locales: string | string[],
|
|
91
98
|
formatters: Formatters,
|
|
92
99
|
formats: Formats,
|
|
93
|
-
values?: Record<string,
|
|
100
|
+
values?: Record<string, PrimitiveType | T | FormatXMLElementFn<T>>,
|
|
94
101
|
currentPluralValue?: number,
|
|
95
102
|
// For debugging
|
|
96
103
|
originalMessage?: string
|
|
97
|
-
): MessageFormatPart[] {
|
|
104
|
+
): MessageFormatPart<T>[] {
|
|
98
105
|
// Hot path for straight simple msg translations
|
|
99
106
|
if (els.length === 1 && isLiteralElement(els[0])) {
|
|
100
107
|
return [
|
|
@@ -104,7 +111,7 @@ export function formatToParts(
|
|
|
104
111
|
},
|
|
105
112
|
];
|
|
106
113
|
}
|
|
107
|
-
const result: MessageFormatPart[] = [];
|
|
114
|
+
const result: MessageFormatPart<T>[] = [];
|
|
108
115
|
for (const el of els) {
|
|
109
116
|
// Exit early for string parts.
|
|
110
117
|
if (isLiteralElement(el)) {
|
|
@@ -130,9 +137,7 @@ export function formatToParts(
|
|
|
130
137
|
|
|
131
138
|
// Enforce that all required values are provided by the caller.
|
|
132
139
|
if (!(values && varName in values)) {
|
|
133
|
-
throw new
|
|
134
|
-
`The intl string context variable "${varName}" was not provided to the string "${originalMessage}"`
|
|
135
|
-
);
|
|
140
|
+
throw new MissingValueError(varName, originalMessage);
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
let value = values[varName];
|
|
@@ -144,9 +149,9 @@ export function formatToParts(
|
|
|
144
149
|
: '';
|
|
145
150
|
}
|
|
146
151
|
result.push({
|
|
147
|
-
type: PART_TYPE.
|
|
152
|
+
type: typeof value === 'string' ? PART_TYPE.literal : PART_TYPE.object,
|
|
148
153
|
value,
|
|
149
|
-
});
|
|
154
|
+
} as ObjectPart<T>);
|
|
150
155
|
continue;
|
|
151
156
|
}
|
|
152
157
|
|
|
@@ -194,14 +199,39 @@ export function formatToParts(
|
|
|
194
199
|
});
|
|
195
200
|
continue;
|
|
196
201
|
}
|
|
202
|
+
if (isTagElement(el)) {
|
|
203
|
+
const {children, value} = el;
|
|
204
|
+
const formatFn = values[value];
|
|
205
|
+
if (!isFormatXMLElementFn<T>(formatFn)) {
|
|
206
|
+
throw new TypeError(`Value for "${value}" must be a function`);
|
|
207
|
+
}
|
|
208
|
+
const parts = formatToParts<T>(
|
|
209
|
+
children,
|
|
210
|
+
locales,
|
|
211
|
+
formatters,
|
|
212
|
+
formats,
|
|
213
|
+
values
|
|
214
|
+
);
|
|
215
|
+
let chunks = formatFn(...parts.map(p => p.value));
|
|
216
|
+
if (!Array.isArray(chunks)) {
|
|
217
|
+
chunks = [chunks];
|
|
218
|
+
}
|
|
219
|
+
result.push(
|
|
220
|
+
...chunks.map(
|
|
221
|
+
(c): MessageFormatPart<T> => {
|
|
222
|
+
return {
|
|
223
|
+
type:
|
|
224
|
+
typeof c === 'string' ? PART_TYPE.literal : PART_TYPE.object,
|
|
225
|
+
value: c,
|
|
226
|
+
} as MessageFormatPart<T>;
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
197
231
|
if (isSelectElement(el)) {
|
|
198
232
|
const opt = el.options[value as string] || el.options.other;
|
|
199
233
|
if (!opt) {
|
|
200
|
-
throw new
|
|
201
|
-
`Invalid values for "${
|
|
202
|
-
el.value
|
|
203
|
-
}": "${value}". Options are "${Object.keys(el.options).join('", "')}"`
|
|
204
|
-
);
|
|
234
|
+
throw new InvalidValueError(el.value, value, Object.keys(el.options));
|
|
205
235
|
}
|
|
206
236
|
result.push(
|
|
207
237
|
...formatToParts(opt.value, locales, formatters, formats, values)
|
|
@@ -212,9 +242,12 @@ export function formatToParts(
|
|
|
212
242
|
let opt = el.options[`=${value}`];
|
|
213
243
|
if (!opt) {
|
|
214
244
|
if (!Intl.PluralRules) {
|
|
215
|
-
throw new FormatError(
|
|
245
|
+
throw new FormatError(
|
|
246
|
+
`Intl.PluralRules is not available in this environment.
|
|
216
247
|
Try polyfilling it using "@formatjs/intl-pluralrules"
|
|
217
|
-
|
|
248
|
+
`,
|
|
249
|
+
ErrorCode.MISSING_INTL_API
|
|
250
|
+
);
|
|
218
251
|
}
|
|
219
252
|
const rule = formatters
|
|
220
253
|
.getPluralRules(locales, {type: el.pluralType})
|
|
@@ -222,11 +255,7 @@ Try polyfilling it using "@formatjs/intl-pluralrules"
|
|
|
222
255
|
opt = el.options[rule] || el.options.other;
|
|
223
256
|
}
|
|
224
257
|
if (!opt) {
|
|
225
|
-
throw new
|
|
226
|
-
`Invalid values for "${
|
|
227
|
-
el.value
|
|
228
|
-
}": "${value}". Options are "${Object.keys(el.options).join('", "')}"`
|
|
229
|
-
);
|
|
258
|
+
throw new InvalidValueError(el.value, value, Object.keys(el.options));
|
|
230
259
|
}
|
|
231
260
|
result.push(
|
|
232
261
|
...formatToParts(
|
|
@@ -235,7 +264,7 @@ Try polyfilling it using "@formatjs/intl-pluralrules"
|
|
|
235
264
|
formatters,
|
|
236
265
|
formats,
|
|
237
266
|
values,
|
|
238
|
-
value - (el.offset || 0)
|
|
267
|
+
(value as number) - (el.offset || 0)
|
|
239
268
|
)
|
|
240
269
|
);
|
|
241
270
|
continue;
|
|
@@ -244,211 +273,6 @@ Try polyfilling it using "@formatjs/intl-pluralrules"
|
|
|
244
273
|
return mergeLiteral(result);
|
|
245
274
|
}
|
|
246
275
|
|
|
247
|
-
export
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
formatters: Formatters,
|
|
251
|
-
formats: Formats,
|
|
252
|
-
values?: Record<string, PrimitiveType>,
|
|
253
|
-
// For debugging
|
|
254
|
-
originalMessage?: string
|
|
255
|
-
): string {
|
|
256
|
-
const parts = formatToParts(
|
|
257
|
-
els,
|
|
258
|
-
locales,
|
|
259
|
-
formatters,
|
|
260
|
-
formats,
|
|
261
|
-
values,
|
|
262
|
-
undefined,
|
|
263
|
-
originalMessage
|
|
264
|
-
);
|
|
265
|
-
// Hot path for straight simple msg translations
|
|
266
|
-
if (parts.length === 1) {
|
|
267
|
-
return parts[0].value;
|
|
268
|
-
}
|
|
269
|
-
return parts.reduce((all, part) => (all += part.value), '');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export type FormatXMLElementFn = (...args: any[]) => string | object;
|
|
273
|
-
|
|
274
|
-
// Singleton
|
|
275
|
-
let domParser: DOMParser;
|
|
276
|
-
const TOKEN_DELIMITER = '@@';
|
|
277
|
-
const TOKEN_REGEX = /@@(\d+_\d+)@@/g;
|
|
278
|
-
let counter = 0;
|
|
279
|
-
function generateId() {
|
|
280
|
-
return `${Date.now()}_${++counter}`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function restoreRichPlaceholderMessage(
|
|
284
|
-
text: string,
|
|
285
|
-
objectParts: Record<string, any>
|
|
286
|
-
): Array<string | object> {
|
|
287
|
-
return text
|
|
288
|
-
.split(TOKEN_REGEX)
|
|
289
|
-
.filter(Boolean)
|
|
290
|
-
.map(c => (objectParts[c] != null ? objectParts[c] : c))
|
|
291
|
-
.reduce((all, c) => {
|
|
292
|
-
if (!all.length) {
|
|
293
|
-
all.push(c);
|
|
294
|
-
} else if (
|
|
295
|
-
typeof c === 'string' &&
|
|
296
|
-
typeof all[all.length - 1] === 'string'
|
|
297
|
-
) {
|
|
298
|
-
all[all.length - 1] += c;
|
|
299
|
-
} else {
|
|
300
|
-
all.push(c);
|
|
301
|
-
}
|
|
302
|
-
return all;
|
|
303
|
-
}, []);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Not exhaustive, just for sanity check
|
|
308
|
-
*/
|
|
309
|
-
const SIMPLE_XML_REGEX = /(<([0-9a-zA-Z-_]*?)>(.*?)<\/([0-9a-zA-Z-_]*?)>)|(<[0-9a-zA-Z-_]*?\/>)/;
|
|
310
|
-
|
|
311
|
-
const TEMPLATE_ID = Date.now() + '@@';
|
|
312
|
-
|
|
313
|
-
const VOID_ELEMENTS = [
|
|
314
|
-
'area',
|
|
315
|
-
'base',
|
|
316
|
-
'br',
|
|
317
|
-
'col',
|
|
318
|
-
'embed',
|
|
319
|
-
'hr',
|
|
320
|
-
'img',
|
|
321
|
-
'input',
|
|
322
|
-
'link',
|
|
323
|
-
'meta',
|
|
324
|
-
'param',
|
|
325
|
-
'source',
|
|
326
|
-
'track',
|
|
327
|
-
'wbr',
|
|
328
|
-
];
|
|
329
|
-
|
|
330
|
-
function formatHTMLElement(
|
|
331
|
-
el: Element,
|
|
332
|
-
objectParts: Record<string, any>,
|
|
333
|
-
values: Record<string, PrimitiveType | object | FormatXMLElementFn>
|
|
334
|
-
): Array<PrimitiveType | object> {
|
|
335
|
-
let {tagName} = el;
|
|
336
|
-
const {outerHTML, textContent, childNodes} = el;
|
|
337
|
-
// Regular text
|
|
338
|
-
if (!tagName) {
|
|
339
|
-
return restoreRichPlaceholderMessage(textContent || '', objectParts);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
tagName = tagName.toLowerCase();
|
|
343
|
-
const isVoidElement = ~VOID_ELEMENTS.indexOf(tagName);
|
|
344
|
-
const formatFnOrValue = values[tagName];
|
|
345
|
-
|
|
346
|
-
if (formatFnOrValue && isVoidElement) {
|
|
347
|
-
throw new FormatError(
|
|
348
|
-
`${tagName} is a self-closing tag and can not be used, please use another tag name.`
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!childNodes.length) {
|
|
353
|
-
return [outerHTML];
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const chunks: any[] = (Array.prototype.slice.call(
|
|
357
|
-
childNodes
|
|
358
|
-
) as ChildNode[]).reduce(
|
|
359
|
-
(all: any[], child) =>
|
|
360
|
-
all.concat(formatHTMLElement(child as HTMLElement, objectParts, values)),
|
|
361
|
-
[]
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
// Legacy HTML
|
|
365
|
-
if (!formatFnOrValue) {
|
|
366
|
-
return [`<${tagName}>`, ...chunks, `</${tagName}>`];
|
|
367
|
-
}
|
|
368
|
-
// HTML Tag replacement
|
|
369
|
-
if (typeof formatFnOrValue === 'function') {
|
|
370
|
-
return [formatFnOrValue(...chunks)];
|
|
371
|
-
}
|
|
372
|
-
return [formatFnOrValue];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function formatHTMLMessage(
|
|
376
|
-
els: MessageFormatElement[],
|
|
377
|
-
locales: string | string[],
|
|
378
|
-
formatters: Formatters,
|
|
379
|
-
formats: Formats,
|
|
380
|
-
values?: Record<string, PrimitiveType | object | FormatXMLElementFn>,
|
|
381
|
-
// For debugging
|
|
382
|
-
originalMessage?: string
|
|
383
|
-
): Array<string | object> {
|
|
384
|
-
const parts = formatToParts(
|
|
385
|
-
els,
|
|
386
|
-
locales,
|
|
387
|
-
formatters,
|
|
388
|
-
formats,
|
|
389
|
-
values,
|
|
390
|
-
undefined,
|
|
391
|
-
originalMessage
|
|
392
|
-
);
|
|
393
|
-
const objectParts: Record<string, ArgumentPart['value']> = {};
|
|
394
|
-
const formattedMessage = parts.reduce((all, part) => {
|
|
395
|
-
if (part.type === PART_TYPE.literal) {
|
|
396
|
-
return (all += part.value);
|
|
397
|
-
}
|
|
398
|
-
const id = generateId();
|
|
399
|
-
objectParts[id] = part.value;
|
|
400
|
-
return (all += `${TOKEN_DELIMITER}${id}${TOKEN_DELIMITER}`);
|
|
401
|
-
}, '');
|
|
402
|
-
|
|
403
|
-
// Not designed to filter out aggressively
|
|
404
|
-
if (!SIMPLE_XML_REGEX.test(formattedMessage)) {
|
|
405
|
-
return restoreRichPlaceholderMessage(formattedMessage, objectParts);
|
|
406
|
-
}
|
|
407
|
-
if (!values) {
|
|
408
|
-
throw new FormatError('Message has placeholders but no values was given');
|
|
409
|
-
}
|
|
410
|
-
if (typeof DOMParser === 'undefined') {
|
|
411
|
-
throw new FormatError('Cannot format XML message without DOMParser');
|
|
412
|
-
}
|
|
413
|
-
if (!domParser) {
|
|
414
|
-
domParser = new DOMParser();
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const content = domParser
|
|
418
|
-
.parseFromString(
|
|
419
|
-
`<formatted-message id="${TEMPLATE_ID}">${formattedMessage}</formatted-message>`,
|
|
420
|
-
'text/html'
|
|
421
|
-
)
|
|
422
|
-
.getElementById(TEMPLATE_ID);
|
|
423
|
-
|
|
424
|
-
if (!content) {
|
|
425
|
-
throw new FormatError(`Malformed HTML message ${formattedMessage}`);
|
|
426
|
-
}
|
|
427
|
-
const tagsToFormat = Object.keys(values).filter(
|
|
428
|
-
varName => !!content.getElementsByTagName(varName).length
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
// No tags to format
|
|
432
|
-
if (!tagsToFormat.length) {
|
|
433
|
-
return restoreRichPlaceholderMessage(formattedMessage, objectParts);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const caseSensitiveTags = tagsToFormat.filter(
|
|
437
|
-
tagName => tagName !== tagName.toLowerCase()
|
|
438
|
-
);
|
|
439
|
-
if (caseSensitiveTags.length) {
|
|
440
|
-
throw new FormatError(
|
|
441
|
-
`HTML tag must be lowercased but the following tags are not: ${caseSensitiveTags.join(
|
|
442
|
-
', '
|
|
443
|
-
)}`
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// We're doing this since top node is `<formatted-message/>` which does not have a formatter
|
|
448
|
-
return Array.prototype.slice
|
|
449
|
-
.call(content.childNodes)
|
|
450
|
-
.reduce(
|
|
451
|
-
(all, child) => all.concat(formatHTMLElement(child, objectParts, values)),
|
|
452
|
-
[]
|
|
453
|
-
);
|
|
454
|
-
}
|
|
276
|
+
export type FormatXMLElementFn<T> = (
|
|
277
|
+
...args: Array<string | T>
|
|
278
|
+
) => string | Array<string | T>;
|