fluent-transpiler 0.2.1 → 0.3.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/cli.js +61 -29
- package/index.js +50 -38
- package/package.json +10 -7
package/cli.js
CHANGED
|
@@ -6,10 +6,10 @@ import { Command, Option } from 'commander'
|
|
|
6
6
|
import compile from './index.js'
|
|
7
7
|
|
|
8
8
|
const fileExists = async (filepath) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
const stats = await stat(filepath)
|
|
10
|
+
if (!stats.isFile()) {
|
|
11
|
+
throw new Error(`${filepath} is not a file`)
|
|
12
|
+
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
new Command()
|
|
@@ -17,38 +17,70 @@ new Command()
|
|
|
17
17
|
.description('Compile Fluent (.ftl) files to JavaScript (.js or .mjs)')
|
|
18
18
|
//.version(package.version)
|
|
19
19
|
.argument('<input>', 'Path to the Fluent file to compile')
|
|
20
|
-
.requiredOption(
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
.requiredOption(
|
|
21
|
+
'--locale <locale...>',
|
|
22
|
+
'What locale(s) to be used. Multiple can be set to allow for fallback. i.e. en-CA'
|
|
23
|
+
)
|
|
24
|
+
.addOption(
|
|
25
|
+
new Option('--comments', 'Include comments in output file.').preset(true)
|
|
26
|
+
)
|
|
27
|
+
.addOption(
|
|
28
|
+
new Option(
|
|
29
|
+
'--include-key <includeMessageKey...>',
|
|
30
|
+
'Allowed messages to be included. Default to include all.'
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
.addOption(
|
|
34
|
+
new Option(
|
|
35
|
+
'--exclude-key <excludeMessageKey...>',
|
|
36
|
+
'Ignored messages to be excluded. Default to exclude none.'
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
.addOption(
|
|
40
|
+
new Option(
|
|
41
|
+
'--exclude-value <excludeMessageValue>',
|
|
42
|
+
'Set message to an empty string when it contains this value. Default to not allowing empty strings.'
|
|
43
|
+
)
|
|
23
44
|
)
|
|
24
|
-
.addOption(new Option('--include <includeMessages...>', 'Allowed messages to be included. Default to include all.'))
|
|
25
|
-
.addOption(new Option('--exclude <excludeMessages...>', 'Ignored messages to be excluded. Default to exclude none.'))
|
|
26
45
|
/*.addOption(new Option('--tree-shaking', 'Export all messages to allow tree shaking')
|
|
27
46
|
.preset(true)
|
|
28
47
|
)*/
|
|
29
|
-
.addOption(
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
.addOption(
|
|
49
|
+
new Option(
|
|
50
|
+
'--variable-notation <variableNotation>',
|
|
51
|
+
'What variable notation to use with exports'
|
|
52
|
+
)
|
|
53
|
+
.choices(['camelCase', 'pascalCase', 'constantCase', 'snakeCase'])
|
|
54
|
+
.default('camelCase')
|
|
32
55
|
)
|
|
33
|
-
.addOption(
|
|
34
|
-
|
|
56
|
+
.addOption(
|
|
57
|
+
new Option(
|
|
58
|
+
'--disable-minify',
|
|
59
|
+
'If disabled, all exported messages will have the same interface `(params) => ({value, attributes})`.'
|
|
60
|
+
).preset(true)
|
|
35
61
|
)
|
|
36
|
-
.addOption(
|
|
37
|
-
|
|
62
|
+
.addOption(
|
|
63
|
+
new Option(
|
|
64
|
+
'--use-isolating',
|
|
65
|
+
'Wrap placeable with \\u2068 and \\u2069.'
|
|
66
|
+
).preset(true)
|
|
67
|
+
)
|
|
68
|
+
.addOption(
|
|
69
|
+
new Option(
|
|
70
|
+
'-o, --output <output>',
|
|
71
|
+
'Path to store the resulting JavaScript file. Will be in ESM.'
|
|
72
|
+
)
|
|
38
73
|
)
|
|
39
|
-
.addOption(new Option('-o, --output <output>', 'Path to store the resulting JavaScript file. Will be in ESM.'))
|
|
40
74
|
.action(async (input, options) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
await fileExists(input)
|
|
76
|
+
|
|
77
|
+
const ftl = await readFile(input, { encoding: 'utf8' })
|
|
78
|
+
|
|
79
|
+
const js = compile(ftl, options)
|
|
80
|
+
if (options.output) {
|
|
81
|
+
await writeFile(options.output, js, 'utf8')
|
|
82
|
+
} else {
|
|
83
|
+
console.log(js)
|
|
84
|
+
}
|
|
52
85
|
})
|
|
53
86
|
.parse()
|
|
54
|
-
|
package/index.js
CHANGED
|
@@ -2,18 +2,19 @@ import { parse } from '@fluent/syntax'
|
|
|
2
2
|
import { camelCase, pascalCase, constantCase, snakeCase } from 'change-case'
|
|
3
3
|
|
|
4
4
|
const exportDefault = `(id, params) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const source = __exports[id] ?? __exports['_'+id]
|
|
6
|
+
if (typeof source === 'undefined') return '*** '+id+' ***'
|
|
7
|
+
if (typeof source === 'function') return source(params)
|
|
8
|
+
return source
|
|
9
9
|
}
|
|
10
10
|
`
|
|
11
11
|
export const compile = (src, opts) => {
|
|
12
12
|
const options = {
|
|
13
13
|
comments: true,
|
|
14
14
|
errorOnJunk: true,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
includeKey: [],
|
|
16
|
+
excludeKey: [],
|
|
17
|
+
excludeValue: undefined,
|
|
17
18
|
//treeShaking: false,
|
|
18
19
|
variableNotation: 'camelCase',
|
|
19
20
|
disableMinify: false, // TODO needs better name strictInterface?
|
|
@@ -23,10 +24,15 @@ export const compile = (src, opts) => {
|
|
|
23
24
|
...opts
|
|
24
25
|
}
|
|
25
26
|
if (!Array.isArray(options.locale)) options.locale = [options.locale]
|
|
26
|
-
if (!Array.isArray(options.
|
|
27
|
-
options.
|
|
28
|
-
if (!Array.isArray(options.
|
|
29
|
-
options.
|
|
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 })
|
|
30
36
|
|
|
31
37
|
const metadata = {}
|
|
32
38
|
const exports = []
|
|
@@ -113,21 +119,26 @@ export const compile = (src, opts) => {
|
|
|
113
119
|
const assignment = compileAssignment(data.id)
|
|
114
120
|
|
|
115
121
|
if (
|
|
116
|
-
options.
|
|
117
|
-
!options.
|
|
122
|
+
options.includeKey.length &&
|
|
123
|
+
!options.includeKey.includes(assignment)
|
|
118
124
|
) {
|
|
119
125
|
return ''
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
if (
|
|
123
|
-
options.
|
|
124
|
-
options.
|
|
129
|
+
options.excludeKey.length &&
|
|
130
|
+
options.excludeKey.includes(assignment)
|
|
125
131
|
) {
|
|
126
132
|
return ''
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
const templateStringLiteral =
|
|
130
136
|
data.value && compileType(data.value, data.type)
|
|
137
|
+
|
|
138
|
+
if (options.excludeValue === templateStringLiteral) {
|
|
139
|
+
templateStringLiteral = '``'
|
|
140
|
+
}
|
|
141
|
+
|
|
131
142
|
metadata[assignment].attributes = data.attributes.length
|
|
132
143
|
let attributes = {}
|
|
133
144
|
if (metadata[assignment].attributes) {
|
|
@@ -173,12 +184,12 @@ export const compile = (src, opts) => {
|
|
|
173
184
|
}
|
|
174
185
|
return `export const ${assignment} = ${message}`
|
|
175
186
|
/*} else {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
187
|
+
if (assignment === metadata[assignment].id) {
|
|
188
|
+
exports.push(`${assignment}: ${message}`)
|
|
189
|
+
} else {
|
|
190
|
+
exports.push(`'${metadata[assignment].id}': ${message}`)
|
|
191
|
+
}
|
|
192
|
+
}*/
|
|
182
193
|
return ''
|
|
183
194
|
},
|
|
184
195
|
Comment: (data) => {
|
|
@@ -202,6 +213,7 @@ export const compile = (src, opts) => {
|
|
|
202
213
|
},
|
|
203
214
|
// Element
|
|
204
215
|
TextElement: (data) => {
|
|
216
|
+
if (data.value === options.emptyString) return
|
|
205
217
|
return data.value.replaceAll('`', '\\`') // escape string literal
|
|
206
218
|
},
|
|
207
219
|
Placeable: (data, parent) => {
|
|
@@ -358,10 +370,10 @@ const formatTime = (value) => {
|
|
|
358
370
|
value = new Date(value)
|
|
359
371
|
if (isNaN(value.getTime())) return value
|
|
360
372
|
try {
|
|
361
|
-
|
|
362
|
-
|
|
373
|
+
const [duration, unit] = relativeTimeDiff(value)
|
|
374
|
+
return relativeTimeFormat.format(duration, unit)
|
|
363
375
|
} catch (e) {
|
|
364
|
-
|
|
376
|
+
return dateTimeFormat.format(value)
|
|
365
377
|
}
|
|
366
378
|
}
|
|
367
379
|
*/
|
|
@@ -375,24 +387,24 @@ const __relativeTimeDiff = (d) => {
|
|
|
375
387
|
const msPerMonth = msPerDay * 30
|
|
376
388
|
const msPerYear = msPerDay * 365.25
|
|
377
389
|
const elapsed = d - new Date()
|
|
378
|
-
|
|
390
|
+
|
|
379
391
|
if (Math.abs(elapsed) < msPerMinute) {
|
|
380
392
|
return [Math.round(elapsed / 1000), 'second']
|
|
381
393
|
}
|
|
382
394
|
if (Math.abs(elapsed) < msPerHour) {
|
|
383
|
-
|
|
395
|
+
return [Math.round(elapsed / msPerMinute), 'minute']
|
|
384
396
|
}
|
|
385
397
|
if (Math.abs(elapsed) < msPerDay) {
|
|
386
|
-
|
|
398
|
+
return [Math.round(elapsed / msPerHour), 'hour']
|
|
387
399
|
}
|
|
388
400
|
if (Math.abs(elapsed) < msPerWeek * 2) {
|
|
389
|
-
|
|
401
|
+
return [Math.round(elapsed / msPerDay), 'day']
|
|
390
402
|
}
|
|
391
403
|
if (Math.abs(elapsed) < msPerMonth) {
|
|
392
|
-
|
|
404
|
+
return [Math.round(elapsed / msPerWeek), 'week']
|
|
393
405
|
}
|
|
394
406
|
if (Math.abs(elapsed) < msPerYear) {
|
|
395
|
-
|
|
407
|
+
return [Math.round(elapsed / msPerMonth), 'month']
|
|
396
408
|
}
|
|
397
409
|
return [Math.round(elapsed / msPerYear), 'year']
|
|
398
410
|
}
|
|
@@ -400,8 +412,8 @@ const __formatRelativeTime = (value, options) => {
|
|
|
400
412
|
if (typeof value === 'string') value = new Date(value)
|
|
401
413
|
if (isNaN(value.getTime())) return value
|
|
402
414
|
try {
|
|
403
|
-
|
|
404
|
-
|
|
415
|
+
const [duration, unit] = __relativeTimeDiff(value)
|
|
416
|
+
return new Intl.RelativeTimeFormat(__locales, options).format(duration, unit)
|
|
405
417
|
} catch (e) {}
|
|
406
418
|
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
407
419
|
}
|
|
@@ -410,16 +422,16 @@ const __formatRelativeTime = (value, options) => {
|
|
|
410
422
|
if (functions.__formatDateTime) {
|
|
411
423
|
output += `
|
|
412
424
|
const __formatDateTime = (value, options) => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
425
|
+
if (typeof value === 'string') value = new Date(value)
|
|
426
|
+
if (isNaN(value.getTime())) return value
|
|
427
|
+
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
416
428
|
}
|
|
417
429
|
`
|
|
418
430
|
}
|
|
419
431
|
if (functions.__formatVariable || functions.__formatNumber) {
|
|
420
432
|
output += `
|
|
421
433
|
const __formatNumber = (value, options) => {
|
|
422
|
-
|
|
434
|
+
return new Intl.NumberFormat(__locales, options).format(value)
|
|
423
435
|
}
|
|
424
436
|
`
|
|
425
437
|
}
|
|
@@ -436,9 +448,9 @@ const __formatVariable = (value) => {
|
|
|
436
448
|
if (functions.__select) {
|
|
437
449
|
output += `
|
|
438
450
|
const __select = (value, cases, fallback, options) => {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
const pluralRules = new Intl.PluralRules(__locales, options)
|
|
452
|
+
const rule = pluralRules.select(value)
|
|
453
|
+
return cases[value] ?? cases[rule] ?? fallback
|
|
442
454
|
}
|
|
443
455
|
`
|
|
444
456
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluent-transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Transpile Fluent (ftl) files into optimized, tree-shakable, JavaScript EcmaScript Modules (esm).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test": "npm run test:cli && npm run test:unit",
|
|
16
16
|
"test:cli": "./cli.js --locale en-CA --locale en --use-isolating --variable-notation camelCase test/files/index.ftl --output test/files/index.mjs",
|
|
17
|
-
"test:unit": "
|
|
17
|
+
"test:unit": "node --test"
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
@@ -39,12 +39,15 @@
|
|
|
39
39
|
},
|
|
40
40
|
"homepage": "https://github.com/willfarrell/fluent-transpiler",
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@fluent/syntax": "0.
|
|
43
|
-
"change-case": "4.
|
|
44
|
-
"commander": "
|
|
42
|
+
"@fluent/syntax": "0.19.0",
|
|
43
|
+
"change-case": "5.4.4",
|
|
44
|
+
"commander": "13.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@fluent/bundle": "^0.
|
|
48
|
-
|
|
47
|
+
"@fluent/bundle": "^0.19.0"
|
|
48
|
+
},
|
|
49
|
+
"funding": {
|
|
50
|
+
"type": "github",
|
|
51
|
+
"url": "https://github.com/sponsors/willfarrell"
|
|
49
52
|
}
|
|
50
53
|
}
|