intl-messageformat 10.1.2 → 10.1.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.
- package/index.d.ts +6 -0
- package/index.d.ts.map +1 -0
- package/index.js +13 -0
- package/intl-messageformat.esm.js +3131 -0
- package/intl-messageformat.iife.js +3212 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +10 -0
- package/lib/src/core.d.ts +34 -0
- package/lib/src/core.d.ts.map +1 -0
- package/lib/src/core.js +240 -0
- package/lib/src/error.d.ts +28 -0
- package/lib/src/error.d.ts.map +1 -0
- package/lib/src/error.js +48 -0
- package/lib/src/formatters.d.ts +47 -0
- package/lib/src/formatters.d.ts.map +1 -0
- package/lib/src/formatters.js +177 -0
- package/package.json +4 -4
- package/src/core.d.ts +34 -0
- package/src/core.d.ts.map +1 -0
- package/src/core.js +243 -0
- package/src/error.d.ts +28 -0
- package/src/error.d.ts.map +1 -0
- package/src/error.js +51 -0
- package/src/formatters.d.ts +47 -0
- package/src/formatters.d.ts.map +1 -0
- package/src/formatters.js +182 -0
- package/BUILD +0 -126
- package/CHANGELOG.md +0 -1141
- package/index.ts +0 -11
- package/src/core.ts +0 -294
- package/src/error.ts +0 -65
- package/src/formatters.ts +0 -311
- package/tests/benchmark.ts +0 -103
- package/tests/index.test.ts +0 -968
- package/tsconfig.json +0 -5
package/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
|
|
3
|
-
Copyrights licensed under the New BSD License.
|
|
4
|
-
See the accompanying LICENSE file for terms.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {IntlMessageFormat} from './src/core'
|
|
8
|
-
export * from './src/formatters'
|
|
9
|
-
export * from './src/core'
|
|
10
|
-
export * from './src/error'
|
|
11
|
-
export default IntlMessageFormat
|
package/src/core.ts
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
|
|
3
|
-
Copyrights licensed under the New BSD License.
|
|
4
|
-
See the accompanying LICENSE file for terms.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {parse, MessageFormatElement} from '@formatjs/icu-messageformat-parser'
|
|
8
|
-
import memoize, {Cache, strategies} from '@formatjs/fast-memoize'
|
|
9
|
-
import {
|
|
10
|
-
FormatterCache,
|
|
11
|
-
Formatters,
|
|
12
|
-
Formats,
|
|
13
|
-
formatToParts,
|
|
14
|
-
FormatXMLElementFn,
|
|
15
|
-
PrimitiveType,
|
|
16
|
-
MessageFormatPart,
|
|
17
|
-
PART_TYPE,
|
|
18
|
-
} from './formatters'
|
|
19
|
-
|
|
20
|
-
// -- MessageFormat --------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
function mergeConfig(c1: Record<string, object>, c2?: Record<string, object>) {
|
|
23
|
-
if (!c2) {
|
|
24
|
-
return c1
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
...(c1 || {}),
|
|
28
|
-
...(c2 || {}),
|
|
29
|
-
...Object.keys(c1).reduce((all: Record<string, object>, k) => {
|
|
30
|
-
all[k] = {
|
|
31
|
-
...c1[k],
|
|
32
|
-
...(c2[k] || {}),
|
|
33
|
-
}
|
|
34
|
-
return all
|
|
35
|
-
}, {}),
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function mergeConfigs(
|
|
40
|
-
defaultConfig: Formats,
|
|
41
|
-
configs?: Partial<Formats>
|
|
42
|
-
): Formats {
|
|
43
|
-
if (!configs) {
|
|
44
|
-
return defaultConfig
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return (Object.keys(defaultConfig) as Array<keyof Formats>).reduce(
|
|
48
|
-
(all: Formats, k: keyof Formats) => {
|
|
49
|
-
all[k] = mergeConfig(defaultConfig[k], configs[k])
|
|
50
|
-
return all
|
|
51
|
-
},
|
|
52
|
-
{...defaultConfig}
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface Options {
|
|
57
|
-
formatters?: Formatters
|
|
58
|
-
/**
|
|
59
|
-
* Whether to treat HTML/XML tags as string literal
|
|
60
|
-
* instead of parsing them as tag token.
|
|
61
|
-
* When this is false we only allow simple tags without
|
|
62
|
-
* any attributes
|
|
63
|
-
*/
|
|
64
|
-
ignoreTag?: boolean
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function createFastMemoizeCache<V>(store: Record<string, V>): Cache<string, V> {
|
|
68
|
-
return {
|
|
69
|
-
create() {
|
|
70
|
-
return {
|
|
71
|
-
get(key) {
|
|
72
|
-
return store[key]
|
|
73
|
-
},
|
|
74
|
-
set(key, value) {
|
|
75
|
-
store[key] = value
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function createDefaultFormatters(
|
|
83
|
-
cache: FormatterCache = {
|
|
84
|
-
number: {},
|
|
85
|
-
dateTime: {},
|
|
86
|
-
pluralRules: {},
|
|
87
|
-
}
|
|
88
|
-
): Formatters {
|
|
89
|
-
return {
|
|
90
|
-
getNumberFormat: memoize((...args) => new Intl.NumberFormat(...args), {
|
|
91
|
-
cache: createFastMemoizeCache(cache.number),
|
|
92
|
-
strategy: strategies.variadic,
|
|
93
|
-
}),
|
|
94
|
-
getDateTimeFormat: memoize((...args) => new Intl.DateTimeFormat(...args), {
|
|
95
|
-
cache: createFastMemoizeCache(cache.dateTime),
|
|
96
|
-
strategy: strategies.variadic,
|
|
97
|
-
}),
|
|
98
|
-
getPluralRules: memoize((...args) => new Intl.PluralRules(...args), {
|
|
99
|
-
cache: createFastMemoizeCache(cache.pluralRules),
|
|
100
|
-
strategy: strategies.variadic,
|
|
101
|
-
}),
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export class IntlMessageFormat {
|
|
106
|
-
private readonly ast: MessageFormatElement[]
|
|
107
|
-
private readonly locales: string | string[]
|
|
108
|
-
private readonly resolvedLocale?: Intl.Locale
|
|
109
|
-
private readonly formatters: Formatters
|
|
110
|
-
private readonly formats: Formats
|
|
111
|
-
private readonly message: string | undefined
|
|
112
|
-
private readonly formatterCache: FormatterCache = {
|
|
113
|
-
number: {},
|
|
114
|
-
dateTime: {},
|
|
115
|
-
pluralRules: {},
|
|
116
|
-
}
|
|
117
|
-
constructor(
|
|
118
|
-
message: string | MessageFormatElement[],
|
|
119
|
-
locales: string | string[] = IntlMessageFormat.defaultLocale,
|
|
120
|
-
overrideFormats?: Partial<Formats>,
|
|
121
|
-
opts?: Options
|
|
122
|
-
) {
|
|
123
|
-
// Defined first because it's used to build the format pattern.
|
|
124
|
-
this.locales = locales
|
|
125
|
-
this.resolvedLocale = IntlMessageFormat.resolveLocale(locales)
|
|
126
|
-
|
|
127
|
-
if (typeof message === 'string') {
|
|
128
|
-
this.message = message
|
|
129
|
-
if (!IntlMessageFormat.__parse) {
|
|
130
|
-
throw new TypeError(
|
|
131
|
-
'IntlMessageFormat.__parse must be set to process `message` of type `string`'
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
// Parse string messages into an AST.
|
|
135
|
-
this.ast = IntlMessageFormat.__parse(message, {
|
|
136
|
-
ignoreTag: opts?.ignoreTag,
|
|
137
|
-
locale: this.resolvedLocale,
|
|
138
|
-
})
|
|
139
|
-
} else {
|
|
140
|
-
this.ast = message
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!Array.isArray(this.ast)) {
|
|
144
|
-
throw new TypeError('A message must be provided as a String or AST.')
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Creates a new object with the specified `formats` merged with the default
|
|
148
|
-
// formats.
|
|
149
|
-
this.formats = mergeConfigs(IntlMessageFormat.formats, overrideFormats)
|
|
150
|
-
|
|
151
|
-
this.formatters =
|
|
152
|
-
(opts && opts.formatters) || createDefaultFormatters(this.formatterCache)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
format = <T = void>(
|
|
156
|
-
values?: Record<string, PrimitiveType | T | FormatXMLElementFn<T>>
|
|
157
|
-
) => {
|
|
158
|
-
const parts = this.formatToParts(values)
|
|
159
|
-
// Hot path for straight simple msg translations
|
|
160
|
-
if (parts.length === 1) {
|
|
161
|
-
return parts[0].value
|
|
162
|
-
}
|
|
163
|
-
const result = parts.reduce((all, part) => {
|
|
164
|
-
if (
|
|
165
|
-
!all.length ||
|
|
166
|
-
part.type !== PART_TYPE.literal ||
|
|
167
|
-
typeof all[all.length - 1] !== 'string'
|
|
168
|
-
) {
|
|
169
|
-
all.push(part.value)
|
|
170
|
-
} else {
|
|
171
|
-
all[all.length - 1] += part.value
|
|
172
|
-
}
|
|
173
|
-
return all
|
|
174
|
-
}, [] as Array<string | T>)
|
|
175
|
-
|
|
176
|
-
if (result.length <= 1) {
|
|
177
|
-
return result[0] || ''
|
|
178
|
-
}
|
|
179
|
-
return result
|
|
180
|
-
}
|
|
181
|
-
formatToParts = <T>(
|
|
182
|
-
values?: Record<string, PrimitiveType | T | FormatXMLElementFn<T>>
|
|
183
|
-
): MessageFormatPart<T>[] =>
|
|
184
|
-
formatToParts(
|
|
185
|
-
this.ast,
|
|
186
|
-
this.locales,
|
|
187
|
-
this.formatters,
|
|
188
|
-
this.formats,
|
|
189
|
-
values,
|
|
190
|
-
undefined,
|
|
191
|
-
this.message
|
|
192
|
-
)
|
|
193
|
-
resolvedOptions = () => ({
|
|
194
|
-
locale:
|
|
195
|
-
this.resolvedLocale?.toString() ||
|
|
196
|
-
Intl.NumberFormat.supportedLocalesOf(this.locales)[0],
|
|
197
|
-
})
|
|
198
|
-
getAst = () => this.ast
|
|
199
|
-
private static memoizedDefaultLocale: string | null = null
|
|
200
|
-
|
|
201
|
-
static get defaultLocale() {
|
|
202
|
-
if (!IntlMessageFormat.memoizedDefaultLocale) {
|
|
203
|
-
IntlMessageFormat.memoizedDefaultLocale =
|
|
204
|
-
new Intl.NumberFormat().resolvedOptions().locale
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return IntlMessageFormat.memoizedDefaultLocale
|
|
208
|
-
}
|
|
209
|
-
static resolveLocale = (
|
|
210
|
-
locales: string | string[]
|
|
211
|
-
): Intl.Locale | undefined => {
|
|
212
|
-
if (typeof Intl.Locale === 'undefined') {
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
const supportedLocales = Intl.NumberFormat.supportedLocalesOf(locales)
|
|
216
|
-
if (supportedLocales.length > 0) {
|
|
217
|
-
return new Intl.Locale(supportedLocales[0])
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return new Intl.Locale(typeof locales === 'string' ? locales : locales[0])
|
|
221
|
-
}
|
|
222
|
-
static __parse: typeof parse | undefined = parse
|
|
223
|
-
// Default format options used as the prototype of the `formats` provided to the
|
|
224
|
-
// constructor. These are used when constructing the internal Intl.NumberFormat
|
|
225
|
-
// and Intl.DateTimeFormat instances.
|
|
226
|
-
static formats: Formats = {
|
|
227
|
-
number: {
|
|
228
|
-
integer: {
|
|
229
|
-
maximumFractionDigits: 0,
|
|
230
|
-
},
|
|
231
|
-
currency: {
|
|
232
|
-
style: 'currency',
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
percent: {
|
|
236
|
-
style: 'percent',
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
date: {
|
|
241
|
-
short: {
|
|
242
|
-
month: 'numeric',
|
|
243
|
-
day: 'numeric',
|
|
244
|
-
year: '2-digit',
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
medium: {
|
|
248
|
-
month: 'short',
|
|
249
|
-
day: 'numeric',
|
|
250
|
-
year: 'numeric',
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
long: {
|
|
254
|
-
month: 'long',
|
|
255
|
-
day: 'numeric',
|
|
256
|
-
year: 'numeric',
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
full: {
|
|
260
|
-
weekday: 'long',
|
|
261
|
-
month: 'long',
|
|
262
|
-
day: 'numeric',
|
|
263
|
-
year: 'numeric',
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
|
|
267
|
-
time: {
|
|
268
|
-
short: {
|
|
269
|
-
hour: 'numeric',
|
|
270
|
-
minute: 'numeric',
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
medium: {
|
|
274
|
-
hour: 'numeric',
|
|
275
|
-
minute: 'numeric',
|
|
276
|
-
second: 'numeric',
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
long: {
|
|
280
|
-
hour: 'numeric',
|
|
281
|
-
minute: 'numeric',
|
|
282
|
-
second: 'numeric',
|
|
283
|
-
timeZoneName: 'short',
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
full: {
|
|
287
|
-
hour: 'numeric',
|
|
288
|
-
minute: 'numeric',
|
|
289
|
-
second: 'numeric',
|
|
290
|
-
timeZoneName: 'short',
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
}
|
|
294
|
-
}
|
package/src/error.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
export enum ErrorCode {
|
|
2
|
-
// When we have a placeholder but no value to format
|
|
3
|
-
MISSING_VALUE = 'MISSING_VALUE',
|
|
4
|
-
// When value supplied is invalid
|
|
5
|
-
INVALID_VALUE = 'INVALID_VALUE',
|
|
6
|
-
// When we need specific Intl API but it's not available
|
|
7
|
-
MISSING_INTL_API = 'MISSING_INTL_API',
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class FormatError extends Error {
|
|
11
|
-
public readonly code: ErrorCode
|
|
12
|
-
/**
|
|
13
|
-
* Original message we're trying to format
|
|
14
|
-
* `undefined` if we're only dealing w/ AST
|
|
15
|
-
*
|
|
16
|
-
* @type {(string | undefined)}
|
|
17
|
-
* @memberof FormatError
|
|
18
|
-
*/
|
|
19
|
-
public readonly originalMessage: string | undefined
|
|
20
|
-
constructor(msg: string, code: ErrorCode, originalMessage?: string) {
|
|
21
|
-
super(msg)
|
|
22
|
-
this.code = code
|
|
23
|
-
this.originalMessage = originalMessage
|
|
24
|
-
}
|
|
25
|
-
public toString() {
|
|
26
|
-
return `[formatjs Error: ${this.code}] ${this.message}`
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class InvalidValueError extends FormatError {
|
|
31
|
-
constructor(
|
|
32
|
-
variableId: string,
|
|
33
|
-
value: any,
|
|
34
|
-
options: string[],
|
|
35
|
-
originalMessage?: string
|
|
36
|
-
) {
|
|
37
|
-
super(
|
|
38
|
-
`Invalid values for "${variableId}": "${value}". Options are "${Object.keys(
|
|
39
|
-
options
|
|
40
|
-
).join('", "')}"`,
|
|
41
|
-
ErrorCode.INVALID_VALUE,
|
|
42
|
-
originalMessage
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export class InvalidValueTypeError extends FormatError {
|
|
48
|
-
constructor(value: any, type: string, originalMessage?: string) {
|
|
49
|
-
super(
|
|
50
|
-
`Value for "${value}" must be of type ${type}`,
|
|
51
|
-
ErrorCode.INVALID_VALUE,
|
|
52
|
-
originalMessage
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export class MissingValueError extends FormatError {
|
|
58
|
-
constructor(variableId: string, originalMessage?: string) {
|
|
59
|
-
super(
|
|
60
|
-
`The intl string context variable "${variableId}" was not provided to the string "${originalMessage}"`,
|
|
61
|
-
ErrorCode.MISSING_VALUE,
|
|
62
|
-
originalMessage
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
}
|
package/src/formatters.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import {NumberFormatOptions} from '@formatjs/ecma402-abstract'
|
|
2
|
-
import {
|
|
3
|
-
isArgumentElement,
|
|
4
|
-
isDateElement,
|
|
5
|
-
isDateTimeSkeleton,
|
|
6
|
-
isLiteralElement,
|
|
7
|
-
isNumberElement,
|
|
8
|
-
isNumberSkeleton,
|
|
9
|
-
isPluralElement,
|
|
10
|
-
isPoundElement,
|
|
11
|
-
isSelectElement,
|
|
12
|
-
isTimeElement,
|
|
13
|
-
MessageFormatElement,
|
|
14
|
-
isTagElement,
|
|
15
|
-
ExtendedNumberFormatOptions,
|
|
16
|
-
} from '@formatjs/icu-messageformat-parser'
|
|
17
|
-
import {
|
|
18
|
-
MissingValueError,
|
|
19
|
-
InvalidValueError,
|
|
20
|
-
ErrorCode,
|
|
21
|
-
FormatError,
|
|
22
|
-
InvalidValueTypeError,
|
|
23
|
-
} from './error'
|
|
24
|
-
|
|
25
|
-
declare global {
|
|
26
|
-
namespace FormatjsIntl {
|
|
27
|
-
interface Message {}
|
|
28
|
-
interface IntlConfig {}
|
|
29
|
-
interface Formats {}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type Format<Source = string> = Source extends keyof FormatjsIntl.Formats
|
|
34
|
-
? FormatjsIntl.Formats[Source]
|
|
35
|
-
: string
|
|
36
|
-
|
|
37
|
-
export interface Formats {
|
|
38
|
-
number: Record<Format<'number'>, NumberFormatOptions>
|
|
39
|
-
date: Record<Format<'date'>, Intl.DateTimeFormatOptions>
|
|
40
|
-
time: Record<Format<'time'>, Intl.DateTimeFormatOptions>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface FormatterCache {
|
|
44
|
-
number: Record<string, NumberFormatOptions>
|
|
45
|
-
dateTime: Record<string, Intl.DateTimeFormat>
|
|
46
|
-
pluralRules: Record<string, Intl.PluralRules>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface Formatters {
|
|
50
|
-
getNumberFormat(
|
|
51
|
-
locals?: string | string[],
|
|
52
|
-
opts?: NumberFormatOptions
|
|
53
|
-
): Intl.NumberFormat
|
|
54
|
-
getDateTimeFormat(
|
|
55
|
-
...args: ConstructorParameters<typeof Intl.DateTimeFormat>
|
|
56
|
-
): Intl.DateTimeFormat
|
|
57
|
-
getPluralRules(
|
|
58
|
-
...args: ConstructorParameters<typeof Intl.PluralRules>
|
|
59
|
-
): Intl.PluralRules
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export enum PART_TYPE {
|
|
63
|
-
literal,
|
|
64
|
-
object,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface LiteralPart {
|
|
68
|
-
type: PART_TYPE.literal
|
|
69
|
-
value: string
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface ObjectPart<T = any> {
|
|
73
|
-
type: PART_TYPE.object
|
|
74
|
-
value: T
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export type MessageFormatPart<T> = LiteralPart | ObjectPart<T>
|
|
78
|
-
|
|
79
|
-
export type PrimitiveType = string | number | boolean | null | undefined | Date
|
|
80
|
-
|
|
81
|
-
function mergeLiteral<T>(
|
|
82
|
-
parts: MessageFormatPart<T>[]
|
|
83
|
-
): MessageFormatPart<T>[] {
|
|
84
|
-
if (parts.length < 2) {
|
|
85
|
-
return parts
|
|
86
|
-
}
|
|
87
|
-
return parts.reduce((all, part) => {
|
|
88
|
-
const lastPart = all[all.length - 1]
|
|
89
|
-
if (
|
|
90
|
-
!lastPart ||
|
|
91
|
-
lastPart.type !== PART_TYPE.literal ||
|
|
92
|
-
part.type !== PART_TYPE.literal
|
|
93
|
-
) {
|
|
94
|
-
all.push(part)
|
|
95
|
-
} else {
|
|
96
|
-
lastPart.value += part.value
|
|
97
|
-
}
|
|
98
|
-
return all
|
|
99
|
-
}, [] as MessageFormatPart<T>[])
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function isFormatXMLElementFn<T>(
|
|
103
|
-
el: PrimitiveType | T | FormatXMLElementFn<T>
|
|
104
|
-
): el is FormatXMLElementFn<T> {
|
|
105
|
-
return typeof el === 'function'
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// TODO(skeleton): add skeleton support
|
|
109
|
-
export function formatToParts<T>(
|
|
110
|
-
els: MessageFormatElement[],
|
|
111
|
-
locales: string | string[],
|
|
112
|
-
formatters: Formatters,
|
|
113
|
-
formats: Formats,
|
|
114
|
-
values?: Record<string, PrimitiveType | T | FormatXMLElementFn<T>>,
|
|
115
|
-
currentPluralValue?: number,
|
|
116
|
-
// For debugging
|
|
117
|
-
originalMessage?: string
|
|
118
|
-
): MessageFormatPart<T>[] {
|
|
119
|
-
// Hot path for straight simple msg translations
|
|
120
|
-
if (els.length === 1 && isLiteralElement(els[0])) {
|
|
121
|
-
return [
|
|
122
|
-
{
|
|
123
|
-
type: PART_TYPE.literal,
|
|
124
|
-
value: els[0].value,
|
|
125
|
-
},
|
|
126
|
-
]
|
|
127
|
-
}
|
|
128
|
-
const result: MessageFormatPart<T>[] = []
|
|
129
|
-
for (const el of els) {
|
|
130
|
-
// Exit early for string parts.
|
|
131
|
-
if (isLiteralElement(el)) {
|
|
132
|
-
result.push({
|
|
133
|
-
type: PART_TYPE.literal,
|
|
134
|
-
value: el.value,
|
|
135
|
-
})
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
// TODO: should this part be literal type?
|
|
139
|
-
// Replace `#` in plural rules with the actual numeric value.
|
|
140
|
-
if (isPoundElement(el)) {
|
|
141
|
-
if (typeof currentPluralValue === 'number') {
|
|
142
|
-
result.push({
|
|
143
|
-
type: PART_TYPE.literal,
|
|
144
|
-
value: formatters.getNumberFormat(locales).format(currentPluralValue),
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const {value: varName} = el
|
|
151
|
-
|
|
152
|
-
// Enforce that all required values are provided by the caller.
|
|
153
|
-
if (!(values && varName in values)) {
|
|
154
|
-
throw new MissingValueError(varName, originalMessage)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let value = values[varName]
|
|
158
|
-
if (isArgumentElement(el)) {
|
|
159
|
-
if (!value || typeof value === 'string' || typeof value === 'number') {
|
|
160
|
-
value =
|
|
161
|
-
typeof value === 'string' || typeof value === 'number'
|
|
162
|
-
? String(value)
|
|
163
|
-
: ''
|
|
164
|
-
}
|
|
165
|
-
result.push({
|
|
166
|
-
type: typeof value === 'string' ? PART_TYPE.literal : PART_TYPE.object,
|
|
167
|
-
value,
|
|
168
|
-
} as ObjectPart<T>)
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Recursively format plural and select parts' option — which can be a
|
|
173
|
-
// nested pattern structure. The choosing of the option to use is
|
|
174
|
-
// abstracted-by and delegated-to the part helper object.
|
|
175
|
-
if (isDateElement(el)) {
|
|
176
|
-
const style =
|
|
177
|
-
typeof el.style === 'string'
|
|
178
|
-
? formats.date[el.style]
|
|
179
|
-
: isDateTimeSkeleton(el.style)
|
|
180
|
-
? el.style.parsedOptions
|
|
181
|
-
: undefined
|
|
182
|
-
result.push({
|
|
183
|
-
type: PART_TYPE.literal,
|
|
184
|
-
value: formatters
|
|
185
|
-
.getDateTimeFormat(locales, style)
|
|
186
|
-
.format(value as number),
|
|
187
|
-
})
|
|
188
|
-
continue
|
|
189
|
-
}
|
|
190
|
-
if (isTimeElement(el)) {
|
|
191
|
-
const style =
|
|
192
|
-
typeof el.style === 'string'
|
|
193
|
-
? formats.time[el.style]
|
|
194
|
-
: isDateTimeSkeleton(el.style)
|
|
195
|
-
? el.style.parsedOptions
|
|
196
|
-
: formats.time.medium
|
|
197
|
-
result.push({
|
|
198
|
-
type: PART_TYPE.literal,
|
|
199
|
-
value: formatters
|
|
200
|
-
.getDateTimeFormat(locales, style)
|
|
201
|
-
.format(value as number),
|
|
202
|
-
})
|
|
203
|
-
continue
|
|
204
|
-
}
|
|
205
|
-
if (isNumberElement(el)) {
|
|
206
|
-
const style =
|
|
207
|
-
typeof el.style === 'string'
|
|
208
|
-
? formats.number[el.style]
|
|
209
|
-
: isNumberSkeleton(el.style)
|
|
210
|
-
? el.style.parsedOptions
|
|
211
|
-
: undefined
|
|
212
|
-
|
|
213
|
-
if (style && (style as ExtendedNumberFormatOptions).scale) {
|
|
214
|
-
value =
|
|
215
|
-
(value as number) *
|
|
216
|
-
((style as ExtendedNumberFormatOptions).scale || 1)
|
|
217
|
-
}
|
|
218
|
-
result.push({
|
|
219
|
-
type: PART_TYPE.literal,
|
|
220
|
-
value: formatters
|
|
221
|
-
.getNumberFormat(locales, style)
|
|
222
|
-
.format(value as number),
|
|
223
|
-
})
|
|
224
|
-
continue
|
|
225
|
-
}
|
|
226
|
-
if (isTagElement(el)) {
|
|
227
|
-
const {children, value} = el
|
|
228
|
-
const formatFn = values[value]
|
|
229
|
-
if (!isFormatXMLElementFn<T>(formatFn)) {
|
|
230
|
-
throw new InvalidValueTypeError(value, 'function', originalMessage)
|
|
231
|
-
}
|
|
232
|
-
const parts = formatToParts<T>(
|
|
233
|
-
children,
|
|
234
|
-
locales,
|
|
235
|
-
formatters,
|
|
236
|
-
formats,
|
|
237
|
-
values,
|
|
238
|
-
currentPluralValue
|
|
239
|
-
)
|
|
240
|
-
let chunks = formatFn(parts.map(p => p.value))
|
|
241
|
-
if (!Array.isArray(chunks)) {
|
|
242
|
-
chunks = [chunks]
|
|
243
|
-
}
|
|
244
|
-
result.push(
|
|
245
|
-
...chunks.map((c): MessageFormatPart<T> => {
|
|
246
|
-
return {
|
|
247
|
-
type: typeof c === 'string' ? PART_TYPE.literal : PART_TYPE.object,
|
|
248
|
-
value: c,
|
|
249
|
-
} as MessageFormatPart<T>
|
|
250
|
-
})
|
|
251
|
-
)
|
|
252
|
-
}
|
|
253
|
-
if (isSelectElement(el)) {
|
|
254
|
-
const opt = el.options[value as string] || el.options.other
|
|
255
|
-
if (!opt) {
|
|
256
|
-
throw new InvalidValueError(
|
|
257
|
-
el.value,
|
|
258
|
-
value,
|
|
259
|
-
Object.keys(el.options),
|
|
260
|
-
originalMessage
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
result.push(
|
|
264
|
-
...formatToParts(opt.value, locales, formatters, formats, values)
|
|
265
|
-
)
|
|
266
|
-
continue
|
|
267
|
-
}
|
|
268
|
-
if (isPluralElement(el)) {
|
|
269
|
-
let opt = el.options[`=${value}`]
|
|
270
|
-
if (!opt) {
|
|
271
|
-
if (!Intl.PluralRules) {
|
|
272
|
-
throw new FormatError(
|
|
273
|
-
`Intl.PluralRules is not available in this environment.
|
|
274
|
-
Try polyfilling it using "@formatjs/intl-pluralrules"
|
|
275
|
-
`,
|
|
276
|
-
ErrorCode.MISSING_INTL_API,
|
|
277
|
-
originalMessage
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
const rule = formatters
|
|
281
|
-
.getPluralRules(locales, {type: el.pluralType})
|
|
282
|
-
.select((value as number) - (el.offset || 0))
|
|
283
|
-
opt = el.options[rule] || el.options.other
|
|
284
|
-
}
|
|
285
|
-
if (!opt) {
|
|
286
|
-
throw new InvalidValueError(
|
|
287
|
-
el.value,
|
|
288
|
-
value,
|
|
289
|
-
Object.keys(el.options),
|
|
290
|
-
originalMessage
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
result.push(
|
|
294
|
-
...formatToParts(
|
|
295
|
-
opt.value,
|
|
296
|
-
locales,
|
|
297
|
-
formatters,
|
|
298
|
-
formats,
|
|
299
|
-
values,
|
|
300
|
-
(value as number) - (el.offset || 0)
|
|
301
|
-
)
|
|
302
|
-
)
|
|
303
|
-
continue
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return mergeLiteral(result)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export type FormatXMLElementFn<T, R = string | T | Array<string | T>> = (
|
|
310
|
-
parts: Array<string | T>
|
|
311
|
-
) => R
|