fluent-transpiler 0.0.2 → 0.1.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/README.md +18 -16
- package/index.js +398 -294
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# fluent-transpiler
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
Transpile Fluent (ftl) files into optimized, tree-shakable, JavaScript EcmaScript Modules (esm).
|
|
3
4
|
|
|
4
5
|
## Install
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
npm i -D fluent-transpiler
|
|
7
9
|
```
|
|
8
10
|
|
|
9
11
|
## CLI
|
|
12
|
+
|
|
10
13
|
```bash
|
|
11
14
|
Usage: ftl [options] <input>
|
|
12
15
|
|
|
@@ -24,26 +27,25 @@ Options:
|
|
|
24
27
|
--use-isolating Wrap placeable with \u2068 and \u2069.
|
|
25
28
|
-o, --output <output> Path to store the resulting JavaScript file. Will be in ESM.
|
|
26
29
|
-h, --help display help for command
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
```
|
|
29
31
|
|
|
30
32
|
## NodeJS
|
|
31
33
|
|
|
32
|
-
| Option
|
|
33
|
-
|
|
34
|
-
| locale
|
|
35
|
-
| comments
|
|
36
|
-
| disableMinify
|
|
37
|
-
| errorOnJunk
|
|
38
|
-
| variableNotation | What variable notation to use with exports. Default: `camelCase`
|
|
39
|
-
| useIsolating
|
|
40
|
-
| exportDefault
|
|
34
|
+
| Option | Description |
|
|
35
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
36
|
+
| locale | What locale(s) to be used. Multiple can be set to allow for fallback. i.e. en-CA |
|
|
37
|
+
| comments | Include comments in output file. Default: true |
|
|
38
|
+
| disableMinify | If disabled, all exported messages will have the same interface `(params) => ({value, attributes})`. Default: each exported message could be a different type based on what is needed to generate the message (`string`, `object`, `() => ''`, `() => ({})`) |
|
|
39
|
+
| errorOnJunk | Throw error when `Junk` is parsed. Default: true |
|
|
40
|
+
| variableNotation | What variable notation to use with exports. Default: `camelCase` |
|
|
41
|
+
| useIsolating | Wrap placeable with \u2068 and \u2069. Default: false |
|
|
42
|
+
| exportDefault | Allows the overwriting of the `export default` to allow for custom uses. Default: See code |
|
|
41
43
|
|
|
42
44
|
```javascript
|
|
43
|
-
import {readFile,writeFile} from 'node:fs/promises'
|
|
45
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
44
46
|
import fluentTranspiler from 'fluent-transpiler'
|
|
45
47
|
|
|
46
|
-
const ftl = await readFile('./path/to/en.ftl', {encoding:'utf8'})
|
|
47
|
-
const js = fluentTranspiler(ftl, {locale:'en-CA'})
|
|
48
|
+
const ftl = await readFile('./path/to/en.ftl', { encoding: 'utf8' })
|
|
49
|
+
const js = fluentTranspiler(ftl, { locale: 'en-CA' })
|
|
48
50
|
await writeFile('./path/to/en.mjs', js, 'utf8')
|
|
49
|
-
```
|
|
51
|
+
```
|
package/index.js
CHANGED
|
@@ -9,312 +9,417 @@ const exportDefault = `(id, params) => {
|
|
|
9
9
|
}
|
|
10
10
|
`
|
|
11
11
|
export const compile = (src, opts) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
12
|
+
const options = {
|
|
13
|
+
comments: true,
|
|
14
|
+
errorOnJunk: true,
|
|
15
|
+
includeMessages: [],
|
|
16
|
+
excludeMessages: [],
|
|
17
|
+
//treeShaking: false,
|
|
18
|
+
variableNotation: 'camelCase',
|
|
19
|
+
disableMinify: false, // TODO needs better name strictInterface?
|
|
20
|
+
useIsolating: false,
|
|
21
|
+
params: 'params',
|
|
22
|
+
exportDefault,
|
|
23
|
+
...opts
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(options.locale)) options.locale = [options.locale]
|
|
26
|
+
if (!Array.isArray(options.includeMessages))
|
|
27
|
+
options.includeMessages = [options.includeMessages]
|
|
28
|
+
if (!Array.isArray(options.excludeMessages))
|
|
29
|
+
options.excludeMessages = [options.excludeMessages]
|
|
30
|
+
|
|
31
|
+
const metadata = {}
|
|
32
|
+
const exports = []
|
|
33
|
+
const functions = {} // global functions
|
|
34
|
+
let variable
|
|
35
|
+
|
|
36
|
+
const regexpValidVariable = /^[a-zA-Z]+[a-zA-Z0-9]*$/
|
|
37
|
+
const compileAssignment = (data) => {
|
|
38
|
+
variable = compileType(data)
|
|
39
|
+
metadata[variable] = {
|
|
40
|
+
id: data.name,
|
|
41
|
+
term: false,
|
|
42
|
+
params: false
|
|
43
|
+
}
|
|
44
|
+
return variable
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const compileFunctionArguments = (data) => {
|
|
48
|
+
const positional = data.arguments?.positional.map((data) => {
|
|
49
|
+
return types[data.type](data)
|
|
50
|
+
})
|
|
51
|
+
const named = data.arguments?.named.reduce((obj, data) => {
|
|
52
|
+
// NamedArgument
|
|
53
|
+
const key = data.name.name
|
|
54
|
+
const value = compileType(data.value, data.type)
|
|
55
|
+
obj[key] = value
|
|
56
|
+
return obj
|
|
57
|
+
}, {})
|
|
58
|
+
return { positional, named }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const compileType = (data, parent) => {
|
|
62
|
+
try {
|
|
63
|
+
return types[data.type](data, parent)
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Error:', e.message, data, e.stack)
|
|
66
|
+
throw new Error(e.message, { cause: data, stack: e.stack })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const types = {
|
|
71
|
+
Identifier: (data) => {
|
|
72
|
+
const value = variableNotation[options.variableNotation](data.name)
|
|
73
|
+
// Check for reserved words - TODO add in rest
|
|
74
|
+
if (['const', 'default', 'enum', 'if'].includes(value)) {
|
|
75
|
+
return '_' + value
|
|
76
|
+
}
|
|
77
|
+
return value
|
|
78
|
+
},
|
|
79
|
+
Attribute: (data) => {
|
|
80
|
+
const key = compileType(data.id)
|
|
81
|
+
const value = compileType(data.value, data.type)
|
|
82
|
+
return ` ${key}: ${value}`
|
|
83
|
+
},
|
|
84
|
+
Pattern: (data, parent) => {
|
|
85
|
+
return (
|
|
86
|
+
'`' +
|
|
87
|
+
data.elements
|
|
88
|
+
.map((data) => {
|
|
89
|
+
return compileType(data, parent)
|
|
90
|
+
})
|
|
91
|
+
.join('') +
|
|
92
|
+
'`'
|
|
93
|
+
)
|
|
94
|
+
},
|
|
95
|
+
// resources
|
|
96
|
+
Term: (data) => {
|
|
97
|
+
const assignment = compileAssignment(data.id)
|
|
98
|
+
const templateStringLiteral = compileType(data.value)
|
|
99
|
+
metadata[assignment].term = true
|
|
100
|
+
if (metadata[assignment].params) {
|
|
101
|
+
return `const ${assignment} = (${options.params}) => ${templateStringLiteral}\n`
|
|
102
|
+
}
|
|
103
|
+
return `const ${assignment} = ${templateStringLiteral}\n`
|
|
104
|
+
},
|
|
105
|
+
Message: (data) => {
|
|
106
|
+
const assignment = compileAssignment(data.id)
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
options.includeMessages.length &&
|
|
110
|
+
!options.includeMessages.includes(assignment)
|
|
111
|
+
) {
|
|
112
|
+
return ''
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
options.excludeMessages.length &&
|
|
117
|
+
options.excludeMessages.includes(assignment)
|
|
118
|
+
) {
|
|
119
|
+
return ''
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const templateStringLiteral =
|
|
123
|
+
data.value && compileType(data.value, data.type)
|
|
124
|
+
metadata[assignment].attributes = data.attributes.length
|
|
125
|
+
let attributes = {}
|
|
126
|
+
if (metadata[assignment].attributes) {
|
|
127
|
+
// use Object.create(null) ?
|
|
128
|
+
attributes = `{\n${data.attributes
|
|
129
|
+
.map((data) => {
|
|
130
|
+
return ' ' + compileType(data)
|
|
131
|
+
})
|
|
132
|
+
.join(',\n')}\n }`
|
|
133
|
+
}
|
|
134
|
+
//
|
|
135
|
+
let message = ''
|
|
136
|
+
if (!options.disableMinify) {
|
|
137
|
+
if (metadata[assignment].attributes) {
|
|
138
|
+
if (metadata[assignment].params) {
|
|
139
|
+
message = `(${options.params}) => ({
|
|
123
140
|
value:${templateStringLiteral},
|
|
124
141
|
attributes:${attributes}
|
|
125
142
|
})\n`
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
} else {
|
|
144
|
+
message = `{
|
|
128
145
|
value: ${templateStringLiteral},
|
|
129
146
|
attributes: ${attributes}
|
|
130
147
|
}\n`
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
}
|
|
149
|
+
} else if (metadata[assignment].params) {
|
|
150
|
+
message = `(${options.params}) => ${templateStringLiteral}\n`
|
|
151
|
+
} else {
|
|
152
|
+
message = `${templateStringLiteral}\n`
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// consistent API
|
|
156
|
+
message = `(${metadata[assignment].params ? options.params : ''}) => ({
|
|
140
157
|
value:${templateStringLiteral},
|
|
141
158
|
attributes:${attributes}
|
|
142
159
|
})\n`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
}
|
|
161
|
+
//if (options.treeShaking) {
|
|
162
|
+
if (assignment === metadata[assignment].id) {
|
|
163
|
+
exports.push(`${assignment}`)
|
|
164
|
+
} else {
|
|
165
|
+
exports.push(`'${metadata[assignment].id}': ${assignment}`)
|
|
166
|
+
}
|
|
167
|
+
return `export const ${assignment} = ${message}`
|
|
168
|
+
/*} else {
|
|
152
169
|
if (assignment === metadata[assignment].id) {
|
|
153
170
|
exports.push(`${assignment}: ${message}`)
|
|
154
171
|
} else {
|
|
155
172
|
exports.push(`'${metadata[assignment].id}': ${message}`)
|
|
156
173
|
}
|
|
157
174
|
}*/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
175
|
+
return ''
|
|
176
|
+
},
|
|
177
|
+
Comment: (data) => {
|
|
178
|
+
if (options.comments) return `// # ${data.content}\n`
|
|
179
|
+
return ''
|
|
180
|
+
},
|
|
181
|
+
GroupComment: (data) => {
|
|
182
|
+
if (options.comments) return `// ## ${data.content}\n`
|
|
183
|
+
return ''
|
|
184
|
+
},
|
|
185
|
+
ResourceComment: (data) => {
|
|
186
|
+
if (options.comments) return `// ### ${data.content}\n`
|
|
187
|
+
return ''
|
|
188
|
+
},
|
|
189
|
+
Junk: (data) => {
|
|
190
|
+
if (options.errorOnJunk) {
|
|
191
|
+
throw new Error('Junk found', { cause: data })
|
|
192
|
+
}
|
|
193
|
+
console.error('Error: Skipping Junk', JSON.stringify(data, null, 2))
|
|
194
|
+
return ''
|
|
195
|
+
},
|
|
196
|
+
// Element
|
|
197
|
+
TextElement: (data) => {
|
|
198
|
+
return data.value
|
|
199
|
+
},
|
|
200
|
+
Placeable: (data, parent) => {
|
|
201
|
+
return `${options.useIsolating ? '\u2068' : ''}\${${compileType(
|
|
202
|
+
data.expression,
|
|
203
|
+
parent
|
|
204
|
+
)}}${options.useIsolating ? '\u2069' : ''}`
|
|
205
|
+
},
|
|
206
|
+
// Expression
|
|
207
|
+
StringLiteral: (data, parent) => {
|
|
208
|
+
// JSON.stringify at parent level
|
|
209
|
+
if (['NamedArgument'].includes(parent)) {
|
|
210
|
+
return `${data.value}`
|
|
211
|
+
}
|
|
212
|
+
return `"${data.value}"`
|
|
213
|
+
},
|
|
214
|
+
NumberLiteral: (data) => {
|
|
215
|
+
const decimal = Number.parseFloat(data.value)
|
|
216
|
+
const number = Number.isInteger(decimal)
|
|
217
|
+
? Number.parseInt(data.value)
|
|
218
|
+
: decimal
|
|
219
|
+
return Intl.NumberFormat(options.locale).format(number)
|
|
220
|
+
},
|
|
221
|
+
VariableReference: (data, parent) => {
|
|
222
|
+
functions.__formatVariable = true
|
|
223
|
+
metadata[variable].params = true
|
|
224
|
+
const value = `${options.params}?.${data.id.name}`
|
|
225
|
+
if (['Message', 'Variant', 'Attribute'].includes(parent)) {
|
|
226
|
+
return `__formatVariable(${value})`
|
|
227
|
+
}
|
|
228
|
+
return value
|
|
229
|
+
},
|
|
230
|
+
MessageReference: (data) => {
|
|
231
|
+
const messageName = compileType(data.id)
|
|
232
|
+
metadata[variable].params ||= metadata[messageName].params
|
|
233
|
+
if (!options.disableMinify) {
|
|
234
|
+
if (metadata[messageName].params) {
|
|
235
|
+
return `${messageName}(${options.params})`
|
|
236
|
+
}
|
|
237
|
+
return `${messageName}`
|
|
238
|
+
}
|
|
239
|
+
return `${messageName}(${
|
|
240
|
+
metadata[messageName].params ? options.params : ''
|
|
241
|
+
})`
|
|
242
|
+
},
|
|
243
|
+
TermReference: (data) => {
|
|
244
|
+
const termName = compileType(data.id)
|
|
245
|
+
metadata[variable].params ||= metadata[termName].params
|
|
246
|
+
|
|
247
|
+
let params
|
|
248
|
+
if (metadata[termName].params) {
|
|
249
|
+
let { named } = compileFunctionArguments(data)
|
|
250
|
+
named = JSON.stringify(named)
|
|
251
|
+
if (named) {
|
|
252
|
+
params = `{ ...${options.params}, ${named.substring(
|
|
253
|
+
1,
|
|
254
|
+
named.length - 1
|
|
255
|
+
)} }`
|
|
256
|
+
} else {
|
|
257
|
+
params = options.params
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!options.disableMinify) {
|
|
261
|
+
if (metadata[termName].params) {
|
|
262
|
+
return `${termName}(${params})`
|
|
263
|
+
}
|
|
264
|
+
return `${termName}`
|
|
265
|
+
}
|
|
266
|
+
return `${termName}(${params ? params : ''})`
|
|
267
|
+
},
|
|
268
|
+
NamedArgument: (data) => {
|
|
269
|
+
// Inconsistent: `NamedArgument` uses `name` instead of `id` for Identifier
|
|
270
|
+
const key = data.name.name // Don't transform value
|
|
271
|
+
const value = compileType(data.value, data.type)
|
|
272
|
+
return `${key}: ${value}`
|
|
273
|
+
},
|
|
274
|
+
SelectExpression: (data) => {
|
|
275
|
+
functions.__select = true
|
|
276
|
+
metadata[variable].params = true
|
|
277
|
+
const value = compileType(data.selector)
|
|
278
|
+
//const options = data.selector
|
|
279
|
+
let fallback
|
|
280
|
+
return `__select(\n ${value},\n {\n${data.variants
|
|
281
|
+
.filter((data) => {
|
|
282
|
+
if (data.default) {
|
|
283
|
+
fallback = compileType(data.value, data.type)
|
|
284
|
+
}
|
|
285
|
+
return !data.default
|
|
286
|
+
})
|
|
287
|
+
.map((data) => {
|
|
288
|
+
return ' ' + compileType(data)
|
|
289
|
+
})
|
|
290
|
+
.join(',\n')}\n },\n ${fallback}\n )`
|
|
291
|
+
},
|
|
292
|
+
Variant: (data, parent) => {
|
|
293
|
+
// Inconsistent: `Variant` uses `key` instead of `id` for Identifier
|
|
294
|
+
const key = compileType(data.key)
|
|
295
|
+
const value = compileType(data.value, data.type)
|
|
296
|
+
return ` '${key}': ${value}`
|
|
297
|
+
},
|
|
298
|
+
FunctionReference: (data) => {
|
|
299
|
+
return `${types[data.id.name](compileFunctionArguments(data))}`
|
|
300
|
+
},
|
|
301
|
+
// Functions
|
|
302
|
+
DATETIME: (data) => {
|
|
303
|
+
functions.__formatDateTime = true
|
|
304
|
+
const { positional, named } = data
|
|
305
|
+
const value = positional.shift()
|
|
306
|
+
return `__formatDateTime(${value}, ${JSON.stringify(named)})`
|
|
307
|
+
},
|
|
308
|
+
RELATIVETIME: (data) => {
|
|
309
|
+
functions.__formatRelativeTime = true
|
|
310
|
+
const { positional, named } = data
|
|
311
|
+
const value = positional.shift()
|
|
312
|
+
return `__formatRelativeTime(${value}, ${JSON.stringify(named)})`
|
|
313
|
+
},
|
|
314
|
+
NUMBER: (data) => {
|
|
315
|
+
functions.__formatNumber = true
|
|
316
|
+
const { positional, named } = data
|
|
317
|
+
const value = positional.shift()
|
|
318
|
+
return `__formatNumber(${value}, ${JSON.stringify(named)})`
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (/\t/.test(src)) {
|
|
323
|
+
console.error(
|
|
324
|
+
'Source file contains tab characters (\t), replacing with <space>x4'
|
|
325
|
+
)
|
|
326
|
+
src = src.replace(/\t/g, ' ')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const { body } = parse(src)
|
|
330
|
+
let translations = ``
|
|
331
|
+
for (const data of body) {
|
|
332
|
+
translations += compileType(data)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let output = ``
|
|
336
|
+
if (
|
|
337
|
+
functions.__formatVariable ||
|
|
338
|
+
functions.__formatDateTime ||
|
|
339
|
+
functions.__formatNumber
|
|
340
|
+
) {
|
|
341
|
+
output += `const __locales = ${JSON.stringify(opts.locale)}\n`
|
|
342
|
+
}
|
|
343
|
+
/*
|
|
344
|
+
const relativeTimeFormat = new Intl.RelativeTimeFormat(lang, {
|
|
345
|
+
localeMatcher: 'best fit',
|
|
346
|
+
numeric: 'always',
|
|
347
|
+
style: 'long'
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const formatTime = (value) => {
|
|
351
|
+
value = new Date(value)
|
|
352
|
+
if (isNaN(value.getTime())) return value
|
|
353
|
+
try {
|
|
354
|
+
const [duration, unit] = relativeTimeDiff(value)
|
|
355
|
+
return relativeTimeFormat.format(duration, unit)
|
|
356
|
+
} catch (e) {
|
|
357
|
+
return dateTimeFormat.format(value)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
*/
|
|
361
|
+
if (functions.__formatRelativeTime) {
|
|
362
|
+
output += `
|
|
363
|
+
const __relativeTimeDiff = (d) => {
|
|
364
|
+
const msPerMinute = 60 * 1000
|
|
365
|
+
const msPerHour = msPerMinute * 60
|
|
366
|
+
const msPerDay = msPerHour * 24
|
|
367
|
+
const msPerWeek = msPerDay * 7
|
|
368
|
+
const msPerMonth = msPerDay * 30
|
|
369
|
+
const msPerYear = msPerDay * 365.25
|
|
370
|
+
const elapsed = d - new Date()
|
|
371
|
+
|
|
372
|
+
if (Math.abs(elapsed) < msPerMinute) {
|
|
373
|
+
return [Math.round(elapsed / 1000), 'second']
|
|
284
374
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
console.error('Source file contains tab characters (\t), replacing with <space>x4')
|
|
288
|
-
src = src.replace(/\t/g, ' ')
|
|
375
|
+
if (Math.abs(elapsed) < msPerHour) {
|
|
376
|
+
return [Math.round(elapsed / msPerMinute), 'minute']
|
|
289
377
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
let translations = ``
|
|
293
|
-
for(const data of body) {
|
|
294
|
-
translations += compileType(data)
|
|
378
|
+
if (Math.abs(elapsed) < msPerDay) {
|
|
379
|
+
return [Math.round(elapsed / msPerHour), 'hour']
|
|
295
380
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (functions.__formatVariable || functions.__formatDateTime || functions.__formatNumber) {
|
|
299
|
-
output += `const __locales = ${JSON.stringify(opts.locale)}\n`
|
|
381
|
+
if (Math.abs(elapsed) < msPerWeek * 2) {
|
|
382
|
+
return [Math.round(elapsed / msPerDay), 'day']
|
|
300
383
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
384
|
+
if (Math.abs(elapsed) < msPerMonth) {
|
|
385
|
+
return [Math.round(elapsed / msPerWeek), 'week']
|
|
386
|
+
}
|
|
387
|
+
if (Math.abs(elapsed) < msPerYear) {
|
|
388
|
+
return [Math.round(elapsed / msPerMonth), 'month']
|
|
389
|
+
}
|
|
390
|
+
return [Math.round(elapsed / msPerYear), 'year']
|
|
391
|
+
}
|
|
392
|
+
const __formatRelativeTime = (value, options) => {
|
|
393
|
+
if (typeof value === 'string') value = new Date(value)
|
|
394
|
+
if (isNaN(value.getTime())) return value
|
|
395
|
+
try {
|
|
396
|
+
const [duration, unit] = relativeTimeDiff(value)
|
|
397
|
+
return relativeTimeFormat.format(duration, unit)
|
|
398
|
+
} catch (e) {
|
|
399
|
+
return dateTimeFormat.format(value)
|
|
400
|
+
}
|
|
401
|
+
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
402
|
+
}
|
|
403
|
+
`
|
|
404
|
+
}
|
|
405
|
+
if (functions.__formatDateTime) {
|
|
406
|
+
output += `
|
|
304
407
|
const __formatDateTime = (value, options) => {
|
|
408
|
+
if (typeof value === 'string') value = new Date(value)
|
|
409
|
+
if (isNaN(value.getTime())) return value
|
|
305
410
|
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
306
411
|
}
|
|
307
412
|
`
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
413
|
+
}
|
|
414
|
+
if (functions.__formatVariable || functions.__formatNumber) {
|
|
415
|
+
output += `
|
|
311
416
|
const __formatNumber = (value, options) => {
|
|
312
417
|
return new Intl.NumberFormat(__locales, options).format(value)
|
|
313
418
|
}
|
|
314
419
|
`
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
420
|
+
}
|
|
421
|
+
if (functions.__formatVariable) {
|
|
422
|
+
output += `
|
|
318
423
|
const __formatVariable = (value) => {
|
|
319
424
|
if (typeof value === 'string') return value
|
|
320
425
|
const decimal = Number.parseFloat(value)
|
|
@@ -322,29 +427,28 @@ const __formatVariable = (value) => {
|
|
|
322
427
|
return __formatNumber(number)
|
|
323
428
|
}
|
|
324
429
|
`
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
430
|
+
}
|
|
431
|
+
if (functions.__select) {
|
|
432
|
+
output += `
|
|
328
433
|
const __select = (value, cases, fallback, options) => {
|
|
329
434
|
const pluralRules = new Intl.PluralRules(__locales, options)
|
|
330
435
|
const rule = pluralRules.select(value)
|
|
331
436
|
return cases[value] ?? cases[rule] ?? fallback
|
|
332
437
|
}
|
|
333
438
|
`
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return output
|
|
340
|
-
}
|
|
439
|
+
}
|
|
440
|
+
output += `\n` + translations
|
|
441
|
+
output += `const __exports = {\n ${exports.join(',\n ')}\n}`
|
|
442
|
+
output += `\nexport default ${options.exportDefault}`
|
|
341
443
|
|
|
444
|
+
return output
|
|
445
|
+
}
|
|
342
446
|
|
|
343
447
|
const variableNotation = {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
448
|
+
camelCase,
|
|
449
|
+
pascalCase,
|
|
450
|
+
snakeCase,
|
|
451
|
+
constantCase
|
|
348
452
|
}
|
|
349
453
|
|
|
350
|
-
export default compile
|
|
454
|
+
export default compile
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluent-transpiler",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Transpile Fluent (ftl) files into optimized, tree-shakable, JavaScript EcmaScript Modules (esm).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ftl": "cli.js"
|
|
9
9
|
},
|
|
10
|
-
"files":[
|
|
10
|
+
"files": [
|
|
11
11
|
"cli.js",
|
|
12
12
|
"index.js"
|
|
13
13
|
],
|