intl-messageformat 10.0.1 → 10.1.2

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/LICENSE.md CHANGED
File without changes
package/README.md CHANGED
File without changes
package/index.ts ADDED
@@ -0,0 +1,11 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intl-messageformat",
3
- "version": "10.0.1",
3
+ "version": "10.1.2",
4
4
  "description": "Formats ICU Message strings with number, date, plural, and select placeholders to create localized messages.",
5
5
  "keywords": [
6
6
  "i18n",
@@ -31,13 +31,13 @@
31
31
  "module": "lib/index.js",
32
32
  "types": "index.d.ts",
33
33
  "dependencies": {
34
- "@formatjs/ecma402-abstract": "1.11.6",
35
- "@formatjs/fast-memoize": "1.2.3",
36
- "@formatjs/icu-messageformat-parser": "2.1.2",
34
+ "@formatjs/ecma402-abstract": "1.11.9",
35
+ "@formatjs/fast-memoize": "1.2.5",
36
+ "@formatjs/icu-messageformat-parser": "2.1.5",
37
37
  "tslib": "2.4.0"
38
38
  },
39
39
  "sideEffects": false,
40
40
  "homepage": "https://github.com/formatjs/formatjs",
41
41
  "license": "BSD-3-Clause",
42
42
  "gitHead": "a7842673d8ad205171ad7c8cb8bb2f318b427c0c"
43
- }
43
+ }
package/src/core.ts ADDED
@@ -0,0 +1,294 @@
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 ADDED
@@ -0,0 +1,65 @@
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
+ }
@@ -0,0 +1,311 @@
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