fluent-transpiler 0.3.2 → 0.4.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/index.js CHANGED
@@ -1,5 +1,77 @@
1
- import { parse } from '@fluent/syntax'
2
- import { camelCase, pascalCase, constantCase, snakeCase } from 'change-case'
1
+ // Copyright 2026 will Farrell, and fluent-transpiler contributors.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { parse } from "@fluent/syntax";
5
+ import { camelCase, constantCase, pascalCase, snakeCase } from "change-case";
6
+
7
+ const reservedWords = new Set([
8
+ "abstract",
9
+ "arguments",
10
+ "await",
11
+ "boolean",
12
+ "break",
13
+ "byte",
14
+ "case",
15
+ "catch",
16
+ "char",
17
+ "class",
18
+ "const",
19
+ "continue",
20
+ "debugger",
21
+ "default",
22
+ "delete",
23
+ "do",
24
+ "double",
25
+ "else",
26
+ "enum",
27
+ "eval",
28
+ "export",
29
+ "extends",
30
+ "false",
31
+ "final",
32
+ "finally",
33
+ "float",
34
+ "for",
35
+ "function",
36
+ "goto",
37
+ "if",
38
+ "implements",
39
+ "import",
40
+ "in",
41
+ "instanceof",
42
+ "int",
43
+ "interface",
44
+ "let",
45
+ "long",
46
+ "native",
47
+ "new",
48
+ "null",
49
+ "of",
50
+ "package",
51
+ "private",
52
+ "protected",
53
+ "public",
54
+ "return",
55
+ "short",
56
+ "static",
57
+ "super",
58
+ "switch",
59
+ "synchronized",
60
+ "this",
61
+ "throw",
62
+ "throws",
63
+ "transient",
64
+ "true",
65
+ "try",
66
+ "typeof",
67
+ "undefined",
68
+ "var",
69
+ "void",
70
+ "volatile",
71
+ "while",
72
+ "with",
73
+ "yield",
74
+ ]);
3
75
 
4
76
  const exportDefault = `(id, params) => {
5
77
  const source = __exports[id] ?? __exports['_'+id]
@@ -7,377 +79,342 @@ const exportDefault = `(id, params) => {
7
79
  if (typeof source === 'function') return source(params)
8
80
  return source
9
81
  }
10
- `
82
+ `;
11
83
  export const compile = (src, opts) => {
12
- const options = {
13
- comments: true,
14
- errorOnJunk: true,
15
- includeKey: [],
16
- excludeKey: [],
17
- excludeValue: undefined,
18
- //treeShaking: false,
19
- variableNotation: 'camelCase',
20
- disableMinify: false, // TODO needs better name strictInterface?
21
- useIsolating: false,
22
- params: 'params',
23
- exportDefault,
24
- ...opts
25
- }
26
- if (!Array.isArray(options.locale)) options.locale = [options.locale]
27
- if (!Array.isArray(options.includeKey))
28
- options.includeKey = [options.includeKey]
29
- if (!Array.isArray(options.excludeKey))
30
- options.excludeKey = [options.excludeKey]
31
- if (options.excludeValue) {
32
- // cast to template literal
33
- options.excludeValue = '`' + options.excludeValue + '`'
34
- }
84
+ const options = {
85
+ comments: true,
86
+ errorOnJunk: true,
87
+ includeKey: [],
88
+ excludeKey: [],
89
+ excludeValue: undefined,
90
+ variableNotation: "camelCase",
91
+ disableMinify: false,
92
+ useIsolating: false,
93
+ params: "params",
94
+ exportDefault,
95
+ ...opts,
96
+ };
97
+ if (!Array.isArray(options.locale)) options.locale = [options.locale];
98
+ if (!Array.isArray(options.includeKey))
99
+ options.includeKey = [options.includeKey];
100
+ if (!Array.isArray(options.excludeKey))
101
+ options.excludeKey = [options.excludeKey];
102
+ if (options.excludeValue) {
103
+ // cast to template literal
104
+ options.excludeValue = `\`${options.excludeValue}\``;
105
+ }
35
106
 
36
- const metadata = {}
37
- const exports = []
38
- const functions = {} // global functions
39
- let variable
107
+ const metadata = {};
108
+ const exports = [];
109
+ const functions = {}; // global functions
110
+ let variable;
40
111
 
41
- const regexpValidVariable = /^[a-zA-Z]+[a-zA-Z0-9]*$/
42
- const compileAssignment = (data) => {
43
- variable = compileType(data)
44
- metadata[variable] = {
45
- id: data.name,
46
- term: false,
47
- params: false
48
- }
49
- return variable
50
- }
112
+ const compileAssignment = (data) => {
113
+ variable = compileType(data);
114
+ metadata[variable] = {
115
+ id: data.name,
116
+ term: false,
117
+ params: false,
118
+ };
119
+ return variable;
120
+ };
51
121
 
52
- const compileFunctionArguments = (data) => {
53
- const positional = data.arguments?.positional.map((data) => {
54
- return types[data.type](data)
55
- })
56
- const named = data.arguments?.named.reduce((obj, data) => {
57
- // NamedArgument
58
- const key = data.name.name
59
- const value = compileType(data.value, data.type)
60
- obj[key] = value
61
- return obj
62
- }, {})
63
- return { positional, named }
64
- }
122
+ const compileFunctionArguments = (data) => {
123
+ const positional = data.arguments?.positional.map((data) => {
124
+ return types[data.type](data);
125
+ });
126
+ const named = data.arguments?.named.reduce((obj, data) => {
127
+ const entry = compileType(data);
128
+ const [key, value] = entry.split(": ");
129
+ obj[key] = value;
130
+ return obj;
131
+ }, {});
132
+ return { positional, named };
133
+ };
65
134
 
66
- const compileType = (data, parent) => {
67
- try {
68
- return types[data.type](data, parent)
69
- } catch (e) {
70
- console.error('Error:', e.message, data, e.stack)
71
- throw new Error(e.message, { cause: data, stack: e.stack })
72
- }
73
- }
135
+ const compileType = (data, parent) => {
136
+ try {
137
+ return types[data.type](data, parent);
138
+ } catch (e) {
139
+ throw new Error(e.message, { cause: { error: e, data } });
140
+ }
141
+ };
74
142
 
75
- const types = {
76
- Identifier: (data, parent) => {
77
- const value =
78
- parent === 'Attribute'
79
- ? data.name
80
- : variableNotation[options.variableNotation](data.name)
143
+ const types = {
144
+ Identifier: (data, parent) => {
145
+ const value =
146
+ parent === "Attribute"
147
+ ? data.name
148
+ : variableNotation[options.variableNotation](data.name);
81
149
 
82
- if (value.includes('-')) {
83
- return `'${value}'`
84
- }
85
- // Check for reserved words - TODO add in rest
86
- if (['const', 'default', 'enum', 'if'].includes(value)) {
87
- return '_' + value
88
- }
89
- return value
90
- },
91
- Attribute: (data) => {
92
- const key = compileType(data.id, data.type)
93
- const value = compileType(data.value, data.type)
94
- return ` ${key}: ${value}`
95
- },
96
- Pattern: (data, parent) => {
97
- return (
98
- '`' +
99
- data.elements
100
- .map((data) => {
101
- return compileType(data, parent)
102
- })
103
- .join('') +
104
- '`'
105
- )
106
- },
107
- // resources
108
- Term: (data) => {
109
- const assignment = compileAssignment(data.id)
110
- const templateStringLiteral = compileType(data.value)
111
- metadata[assignment].term = true
112
- if (metadata[assignment].params) {
113
- return `const ${assignment} = (${options.params}) => ${templateStringLiteral}\n`
114
- }
115
- return `const ${assignment} = ${templateStringLiteral}\n`
116
- },
117
- Message: (data) => {
118
- const assignment = compileAssignment(data.id)
150
+ if (value.includes("-")) {
151
+ return `'${value}'`;
152
+ }
153
+ // Check for reserved words
154
+ if (reservedWords.has(value)) {
155
+ return `_${value}`;
156
+ }
157
+ return value;
158
+ },
159
+ Attribute: (data) => {
160
+ const key = compileType(data.id, data.type);
161
+ const value = compileType(data.value, data.type);
162
+ return ` ${key}: ${value}`;
163
+ },
164
+ Pattern: (data, parent) => {
165
+ return (
166
+ "`" +
167
+ data.elements
168
+ .map((data) => {
169
+ return compileType(data, parent);
170
+ })
171
+ .join("") +
172
+ "`"
173
+ );
174
+ },
175
+ // resources
176
+ Term: (data) => {
177
+ const assignment = compileAssignment(data.id);
178
+ const templateStringLiteral = compileType(data.value);
179
+ metadata[assignment].term = true;
180
+ if (metadata[assignment].params) {
181
+ return `const ${assignment} = (${options.params}) => ${templateStringLiteral}\n`;
182
+ }
183
+ return `const ${assignment} = ${templateStringLiteral}\n`;
184
+ },
185
+ Message: (data) => {
186
+ const assignment = compileAssignment(data.id);
119
187
 
120
- if (
121
- options.includeKey.length &&
122
- !options.includeKey.includes(assignment)
123
- ) {
124
- return ''
125
- }
188
+ if (
189
+ options.includeKey.length &&
190
+ !options.includeKey.includes(assignment)
191
+ ) {
192
+ return "";
193
+ }
126
194
 
127
- if (
128
- options.excludeKey.length &&
129
- options.excludeKey.includes(assignment)
130
- ) {
131
- return ''
132
- }
195
+ if (
196
+ options.excludeKey.length &&
197
+ options.excludeKey.includes(assignment)
198
+ ) {
199
+ return "";
200
+ }
133
201
 
134
- let templateStringLiteral =
135
- data.value && compileType(data.value, data.type)
202
+ let templateStringLiteral =
203
+ data.value && compileType(data.value, data.type);
136
204
 
137
- if (options.excludeValue === templateStringLiteral) {
138
- templateStringLiteral = '``'
139
- }
205
+ if (options.excludeValue === templateStringLiteral) {
206
+ templateStringLiteral = "``";
207
+ }
140
208
 
141
- metadata[assignment].attributes = data.attributes.length
142
- let attributes = {}
143
- if (metadata[assignment].attributes) {
144
- // use Object.create(null) ?
145
- attributes = `{\n${data.attributes
146
- .map((data) => {
147
- return ' ' + compileType(data)
148
- })
149
- .join(',\n')}\n }`
150
- }
151
- //
152
- let message = ''
153
- if (!options.disableMinify) {
154
- if (metadata[assignment].attributes) {
155
- if (metadata[assignment].params) {
156
- message = `(${options.params}) => ({
209
+ metadata[assignment].attributes = data.attributes.length;
210
+ let attributes = {};
211
+ if (metadata[assignment].attributes) {
212
+ attributes = `{\n${data.attributes
213
+ .map((data) => {
214
+ return ` ${compileType(data)}`;
215
+ })
216
+ .join(",\n")}\n }`;
217
+ }
218
+
219
+ let message = "";
220
+ if (!options.disableMinify) {
221
+ if (metadata[assignment].attributes) {
222
+ if (metadata[assignment].params) {
223
+ message = `(${options.params}) => ({
157
224
  value:${templateStringLiteral},
158
225
  attributes:${attributes}
159
- })\n`
160
- } else {
161
- message = `{
226
+ })\n`;
227
+ } else {
228
+ message = `{
162
229
  value: ${templateStringLiteral},
163
230
  attributes: ${attributes}
164
- }\n`
165
- }
166
- } else if (metadata[assignment].params) {
167
- message = `(${options.params}) => ${templateStringLiteral}\n`
168
- } else {
169
- message = `${templateStringLiteral}\n`
170
- }
171
- } else {
172
- // consistent API
173
- message = `(${metadata[assignment].params ? options.params : ''}) => ({
231
+ }\n`;
232
+ }
233
+ } else if (metadata[assignment].params) {
234
+ message = `(${options.params}) => ${templateStringLiteral}\n`;
235
+ } else {
236
+ message = `${templateStringLiteral}\n`;
237
+ }
238
+ } else {
239
+ // consistent API
240
+ message = `(${metadata[assignment].params ? options.params : ""}) => ({
174
241
  value:${templateStringLiteral},
175
242
  attributes:${attributes}
176
- })\n`
177
- }
178
- //if (options.treeShaking) {
179
- if (assignment === metadata[assignment].id) {
180
- exports.push(`${assignment}`)
181
- } else {
182
- exports.push(`'${metadata[assignment].id}': ${assignment}`)
183
- }
184
- return `export const ${assignment} = ${message}`
185
- /*} else {
186
- if (assignment === metadata[assignment].id) {
187
- exports.push(`${assignment}: ${message}`)
188
- } else {
189
- exports.push(`'${metadata[assignment].id}': ${message}`)
190
- }
191
- }*/
192
- return ''
193
- },
194
- Comment: (data) => {
195
- if (options.comments) return `// # ${data.content}\n`
196
- return ''
197
- },
198
- GroupComment: (data) => {
199
- if (options.comments) return `// ## ${data.content}\n`
200
- return ''
201
- },
202
- ResourceComment: (data) => {
203
- if (options.comments) return `// ### ${data.content}\n`
204
- return ''
205
- },
206
- Junk: (data) => {
207
- if (options.errorOnJunk) {
208
- throw new Error('Junk found', { cause: data })
209
- }
210
- console.error('Error: Skipping Junk', JSON.stringify(data, null, 2))
211
- return ''
212
- },
213
- // Element
214
- TextElement: (data) => {
215
- if (data.value === options.emptyString) return
216
- return data.value.replaceAll('`', '\\`') // escape string literal
217
- },
218
- Placeable: (data, parent) => {
219
- return `${options.useIsolating ? '\u2068' : ''}\${${compileType(
220
- data.expression,
221
- parent
222
- )}}${options.useIsolating ? '\u2069' : ''}`
223
- },
224
- // Expression
225
- StringLiteral: (data, parent) => {
226
- // JSON.stringify at parent level
227
- if (['NamedArgument'].includes(parent)) {
228
- return `${data.value}`
229
- }
230
- return `"${data.value}"`
231
- },
232
- NumberLiteral: (data) => {
233
- const decimal = Number.parseFloat(data.value)
234
- const number = Number.isInteger(decimal)
235
- ? Number.parseInt(data.value)
236
- : decimal
237
- return Intl.NumberFormat(options.locale).format(number)
238
- },
239
- VariableReference: (data, parent) => {
240
- functions.__formatVariable = true
241
- metadata[variable].params = true
242
- const value = `${options.params}?.${data.id.name}`
243
- if (['Message', 'Variant', 'Attribute'].includes(parent)) {
244
- return `__formatVariable(${value})`
245
- }
246
- return value
247
- },
248
- MessageReference: (data) => {
249
- const messageName = compileType(data.id)
250
- metadata[variable].params ||= metadata[messageName].params
251
- if (!options.disableMinify) {
252
- if (metadata[messageName].params) {
253
- return `${messageName}(${options.params})`
254
- }
255
- return `${messageName}`
256
- }
257
- return `${messageName}(${
258
- metadata[messageName].params ? options.params : ''
259
- })`
260
- },
261
- TermReference: (data) => {
262
- const termName = compileType(data.id)
263
- metadata[variable].params ||= metadata[termName].params
243
+ })\n`;
244
+ }
264
245
 
265
- let params
266
- if (metadata[termName].params) {
267
- let { named } = compileFunctionArguments(data)
268
- named = JSON.stringify(named)
269
- if (named) {
270
- params = `{ ...${options.params}, ${named.substring(
271
- 1,
272
- named.length - 1
273
- )} }`
274
- } else {
275
- params = options.params
276
- }
277
- }
278
- if (!options.disableMinify) {
279
- if (metadata[termName].params) {
280
- return `${termName}(${params})`
281
- }
282
- return `${termName}`
283
- }
284
- return `${termName}(${params ? params : ''})`
285
- },
286
- NamedArgument: (data) => {
287
- // Inconsistent: `NamedArgument` uses `name` instead of `id` for Identifier
288
- const key = data.name.name // Don't transform value
289
- const value = compileType(data.value, data.type)
290
- return `${key}: ${value}`
291
- },
292
- SelectExpression: (data) => {
293
- functions.__select = true
294
- metadata[variable].params = true
295
- const value = compileType(data.selector)
296
- //const options = data.selector
297
- let fallback
298
- return `__select(\n ${value},\n {\n${data.variants
299
- .filter((data) => {
300
- if (data.default) {
301
- fallback = compileType(data.value, data.type)
302
- }
303
- return !data.default
304
- })
305
- .map((data) => {
306
- return ' ' + compileType(data)
307
- })
308
- .join(',\n')}\n },\n ${fallback}\n )`
309
- },
310
- Variant: (data, parent) => {
311
- // Inconsistent: `Variant` uses `key` instead of `id` for Identifier
312
- const key = compileType(data.key)
313
- const value = compileType(data.value, data.type)
314
- return ` '${key}': ${value}`
315
- },
316
- FunctionReference: (data) => {
317
- return `${types[data.id.name](compileFunctionArguments(data))}`
318
- },
319
- // Functions
320
- DATETIME: (data) => {
321
- functions.__formatDateTime = true
322
- const { positional, named } = data
323
- const value = positional.shift()
324
- return `__formatDateTime(${value}, ${JSON.stringify(named)})`
325
- },
326
- RELATIVETIME: (data) => {
327
- functions.__formatRelativeTime = true
328
- const { positional, named } = data
329
- const value = positional.shift()
330
- return `__formatRelativeTime(${value}, ${JSON.stringify(named)})`
331
- },
332
- NUMBER: (data) => {
333
- functions.__formatNumber = true
334
- const { positional, named } = data
335
- const value = positional.shift()
336
- return `__formatNumber(${value}, ${JSON.stringify(named)})`
337
- }
338
- }
246
+ if (assignment === metadata[assignment].id) {
247
+ exports.push(`${assignment}`);
248
+ } else {
249
+ exports.push(`'${metadata[assignment].id}': ${assignment}`);
250
+ }
251
+ return `export const ${assignment} = ${message}`;
252
+ },
253
+ Comment: (data) => {
254
+ if (options.comments) return `// # ${data.content}\n`;
255
+ return "";
256
+ },
257
+ GroupComment: (data) => {
258
+ if (options.comments) return `// ## ${data.content}\n`;
259
+ return "";
260
+ },
261
+ ResourceComment: (data) => {
262
+ if (options.comments) return `// ### ${data.content}\n`;
263
+ return "";
264
+ },
265
+ Junk: (data) => {
266
+ if (options.errorOnJunk) {
267
+ throw new Error("Junk found", { cause: data });
268
+ }
269
+ return "";
270
+ },
271
+ // Element
272
+ TextElement: (data) => {
273
+ return data.value.replaceAll("`", "\\`"); // escape string literal
274
+ },
275
+ Placeable: (data, parent) => {
276
+ return `${options.useIsolating ? "\u2068" : ""}\${${compileType(
277
+ data.expression,
278
+ parent,
279
+ )}}${options.useIsolating ? "\u2069" : ""}`;
280
+ },
281
+ // Expression
282
+ StringLiteral: (data, parent) => {
283
+ // JSON.stringify at parent level
284
+ if (["NamedArgument"].includes(parent)) {
285
+ return `${data.value}`;
286
+ }
287
+ return `"${data.value}"`;
288
+ },
289
+ NumberLiteral: (data) => {
290
+ const decimal = Number.parseFloat(data.value);
291
+ const number = Number.isInteger(decimal)
292
+ ? Number.parseInt(data.value, 10)
293
+ : decimal;
294
+ return Intl.NumberFormat(options.locale).format(number);
295
+ },
296
+ VariableReference: (data, parent) => {
297
+ functions.__formatVariable = true;
298
+ metadata[variable].params = true;
299
+ const value = `${options.params}?.${data.id.name}`;
300
+ if (["Message", "Variant", "Attribute"].includes(parent)) {
301
+ return `__formatVariable(${value})`;
302
+ }
303
+ return value;
304
+ },
305
+ MessageReference: (data) => {
306
+ const messageName = compileType(data.id);
307
+ metadata[variable].params ||= metadata[messageName].params;
308
+ if (!options.disableMinify) {
309
+ if (metadata[messageName].params) {
310
+ return `${messageName}(${options.params})`;
311
+ }
312
+ return `${messageName}`;
313
+ }
314
+ return `${messageName}(${
315
+ metadata[messageName].params ? options.params : ""
316
+ })`;
317
+ },
318
+ TermReference: (data) => {
319
+ const termName = compileType(data.id);
320
+ metadata[variable].params ||= metadata[termName].params;
339
321
 
340
- if (/\t/.test(src)) {
341
- console.error(
342
- 'Source file contains tab characters (\t), replacing with <space>x4'
343
- )
344
- src = src.replace(/\t/g, ' ')
345
- }
322
+ let params;
323
+ if (metadata[termName].params) {
324
+ let { named } = compileFunctionArguments(data);
325
+ named = JSON.stringify(named);
326
+ if (named) {
327
+ params = `{ ...${options.params}, ${named.substring(
328
+ 1,
329
+ named.length - 1,
330
+ )} }`;
331
+ } else {
332
+ params = options.params;
333
+ }
334
+ }
335
+ if (!options.disableMinify) {
336
+ if (metadata[termName].params) {
337
+ return `${termName}(${params})`;
338
+ }
339
+ return `${termName}`;
340
+ }
341
+ return `${termName}(${params ? params : ""})`;
342
+ },
343
+ NamedArgument: (data) => {
344
+ // Inconsistent: `NamedArgument` uses `name` instead of `id` for Identifier
345
+ const key = data.name.name; // Don't transform value
346
+ const value = compileType(data.value, data.type);
347
+ return `${key}: ${value}`;
348
+ },
349
+ SelectExpression: (data) => {
350
+ functions.__select = true;
351
+ metadata[variable].params = true;
352
+ const value = compileType(data.selector);
353
+ let fallback;
354
+ return `__select(\n ${value},\n {\n${data.variants
355
+ .filter((data) => {
356
+ if (data.default) {
357
+ fallback = compileType(data.value, data.type);
358
+ }
359
+ return !data.default;
360
+ })
361
+ .map((data) => {
362
+ return ` ${compileType(data)}`;
363
+ })
364
+ .join(",\n")}\n },\n ${fallback}\n )`;
365
+ },
366
+ Variant: (data, parent) => {
367
+ // Inconsistent: `Variant` uses `key` instead of `id` for Identifier
368
+ const key = compileType(data.key);
369
+ const value = compileType(data.value, data.type);
370
+ return ` '${key}': ${value}`;
371
+ },
372
+ FunctionReference: (data) => {
373
+ return `${types[data.id.name](compileFunctionArguments(data))}`;
374
+ },
375
+ // Functions
376
+ DATETIME: (data) => {
377
+ functions.__formatDateTime = true;
378
+ const { positional, named } = data;
379
+ const value = positional[0];
380
+ return `__formatDateTime(${value}, ${JSON.stringify(named)})`;
381
+ },
382
+ RELATIVETIME: (data) => {
383
+ functions.__formatRelativeTime = true;
384
+ const { positional, named } = data;
385
+ const value = positional[0];
386
+ return `__formatRelativeTime(${value}, ${JSON.stringify(named)})`;
387
+ },
388
+ NUMBER: (data) => {
389
+ functions.__formatNumber = true;
390
+ const { positional, named } = data;
391
+ const value = positional[0];
392
+ return `__formatNumber(${value}, ${JSON.stringify(named)})`;
393
+ },
394
+ };
346
395
 
347
- const { body } = parse(src)
348
- let translations = ``
349
- for (const data of body) {
350
- translations += compileType(data)
351
- }
396
+ if (/\t/.test(src)) {
397
+ src = src.replace(/\t/g, " ");
398
+ }
352
399
 
353
- let output = ``
354
- if (
355
- functions.__formatVariable ||
356
- functions.__formatDateTime ||
357
- functions.__formatNumber
358
- ) {
359
- output += `const __locales = ${JSON.stringify(opts.locale)}\n`
360
- }
361
- /*
362
- const relativeTimeFormat = new Intl.RelativeTimeFormat(lang, {
363
- localeMatcher: 'best fit',
364
- numeric: 'always',
365
- style: 'long'
366
- })
400
+ const { body } = parse(src);
401
+ let translations = ``;
402
+ for (const data of body) {
403
+ translations += compileType(data);
404
+ }
367
405
 
368
- const formatTime = (value) => {
369
- value = new Date(value)
370
- if (isNaN(value.getTime())) return value
371
- try {
372
- const [duration, unit] = relativeTimeDiff(value)
373
- return relativeTimeFormat.format(duration, unit)
374
- } catch (e) {
375
- return dateTimeFormat.format(value)
376
- }
377
- }
378
- */
379
- if (functions.__formatRelativeTime) {
380
- output += `
406
+ let output = ``;
407
+ if (
408
+ functions.__formatVariable ||
409
+ functions.__formatDateTime ||
410
+ functions.__formatNumber ||
411
+ functions.__formatRelativeTime ||
412
+ functions.__select
413
+ ) {
414
+ output += `const __locales = ${JSON.stringify(options.locale)}\nconst __intlCache = {}\n`;
415
+ }
416
+ if (functions.__formatRelativeTime) {
417
+ output += `
381
418
  const __relativeTimeDiff = (d) => {
382
419
  const msPerMinute = 60 * 1000
383
420
  const msPerHour = msPerMinute * 60
@@ -411,60 +448,66 @@ const __formatRelativeTime = (value, options) => {
411
448
  if (typeof value === 'string') value = new Date(value)
412
449
  if (isNaN(value.getTime())) return value
413
450
  try {
414
- const [duration, unit] = __relativeTimeDiff(value)
415
- return new Intl.RelativeTimeFormat(__locales, options).format(duration, unit)
416
- } catch (e) {}
417
- return new Intl.DateTimeFormat(__locales, options).format(value)
418
- }
419
- `
451
+ const [duration, unit] = __relativeTimeDiff(value)
452
+ const k = JSON.stringify(options) ?? ''
453
+ return (__intlCache['R'+k] ??= new Intl.RelativeTimeFormat(__locales, options)).format(duration, unit)
454
+ } catch (e) {
455
+ // RelativeTimeFormat unsupported or invalid options, fall back to DateTimeFormat
420
456
  }
421
- if (functions.__formatDateTime) {
422
- output += `
457
+ const k = JSON.stringify(options) ?? ''
458
+ return (__intlCache['D'+k] ??= new Intl.DateTimeFormat(__locales, options)).format(value)
459
+ }
460
+ `;
461
+ }
462
+ if (functions.__formatDateTime) {
463
+ output += `
423
464
  const __formatDateTime = (value, options) => {
424
465
  if (typeof value === 'string') value = new Date(value)
425
466
  if (isNaN(value.getTime())) return value
426
- return new Intl.DateTimeFormat(__locales, options).format(value)
467
+ const k = JSON.stringify(options) ?? ''
468
+ return (__intlCache['D'+k] ??= new Intl.DateTimeFormat(__locales, options)).format(value)
427
469
  }
428
- `
429
- }
430
- if (functions.__formatVariable || functions.__formatNumber) {
431
- output += `
470
+ `;
471
+ }
472
+ if (functions.__formatVariable || functions.__formatNumber) {
473
+ output += `
432
474
  const __formatNumber = (value, options) => {
433
- return new Intl.NumberFormat(__locales, options).format(value)
475
+ const k = JSON.stringify(options) ?? ''
476
+ return (__intlCache['N'+k] ??= new Intl.NumberFormat(__locales, options)).format(value)
434
477
  }
435
- `
436
- }
437
- if (functions.__formatVariable) {
438
- output += `
478
+ `;
479
+ }
480
+ if (functions.__formatVariable) {
481
+ output += `
439
482
  const __formatVariable = (value) => {
440
483
  if (typeof value === 'string') return value
441
- const decimal = Number.parseFloat(value)
442
- const number = Number.isInteger(decimal) ? Number.parseInt(value) : decimal
484
+ const decimal = Number.parseFloat(value)
485
+ const number = Number.isInteger(decimal) ? Number.parseInt(value, 10) : decimal
443
486
  return __formatNumber(number)
444
487
  }
445
- `
446
- }
447
- if (functions.__select) {
448
- output += `
488
+ `;
489
+ }
490
+ if (functions.__select) {
491
+ output += `
449
492
  const __select = (value, cases, fallback, options) => {
450
- const pluralRules = new Intl.PluralRules(__locales, options)
451
- const rule = pluralRules.select(value)
493
+ const k = JSON.stringify(options) ?? ''
494
+ const rule = (__intlCache['P'+k] ??= new Intl.PluralRules(__locales, options)).select(value)
452
495
  return cases[value] ?? cases[rule] ?? fallback
453
496
  }
454
- `
455
- }
456
- output += `\n` + translations
457
- output += `const __exports = {\n ${exports.join(',\n ')}\n}`
458
- output += `\nexport default ${options.exportDefault}`
497
+ `;
498
+ }
499
+ output += `\n${translations}`;
500
+ output += `const __exports = {\n ${exports.join(",\n ")}\n}`;
501
+ output += `\nexport default ${options.exportDefault}`;
459
502
 
460
- return output
461
- }
503
+ return output;
504
+ };
462
505
 
463
506
  const variableNotation = {
464
- camelCase,
465
- pascalCase,
466
- snakeCase,
467
- constantCase
468
- }
507
+ camelCase,
508
+ pascalCase,
509
+ snakeCase,
510
+ constantCase,
511
+ };
469
512
 
470
- export default compile
513
+ export default compile;