fluent-transpiler 0.3.1 → 0.4.0

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