fluent-transpiler 0.0.2 → 0.1.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/README.md +18 -16
- package/index.js +396 -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,415 @@ 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 new Intl.RelativeTimeFormat(lang, options).format(duration, unit)
|
|
398
|
+
} catch (e) {}
|
|
399
|
+
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
400
|
+
}
|
|
401
|
+
`
|
|
402
|
+
}
|
|
403
|
+
if (functions.__formatDateTime) {
|
|
404
|
+
output += `
|
|
304
405
|
const __formatDateTime = (value, options) => {
|
|
406
|
+
if (typeof value === 'string') value = new Date(value)
|
|
407
|
+
if (isNaN(value.getTime())) return value
|
|
305
408
|
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
306
409
|
}
|
|
307
410
|
`
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
411
|
+
}
|
|
412
|
+
if (functions.__formatVariable || functions.__formatNumber) {
|
|
413
|
+
output += `
|
|
311
414
|
const __formatNumber = (value, options) => {
|
|
312
415
|
return new Intl.NumberFormat(__locales, options).format(value)
|
|
313
416
|
}
|
|
314
417
|
`
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
418
|
+
}
|
|
419
|
+
if (functions.__formatVariable) {
|
|
420
|
+
output += `
|
|
318
421
|
const __formatVariable = (value) => {
|
|
319
422
|
if (typeof value === 'string') return value
|
|
320
423
|
const decimal = Number.parseFloat(value)
|
|
@@ -322,29 +425,28 @@ const __formatVariable = (value) => {
|
|
|
322
425
|
return __formatNumber(number)
|
|
323
426
|
}
|
|
324
427
|
`
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
428
|
+
}
|
|
429
|
+
if (functions.__select) {
|
|
430
|
+
output += `
|
|
328
431
|
const __select = (value, cases, fallback, options) => {
|
|
329
432
|
const pluralRules = new Intl.PluralRules(__locales, options)
|
|
330
433
|
const rule = pluralRules.select(value)
|
|
331
434
|
return cases[value] ?? cases[rule] ?? fallback
|
|
332
435
|
}
|
|
333
436
|
`
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return output
|
|
340
|
-
}
|
|
437
|
+
}
|
|
438
|
+
output += `\n` + translations
|
|
439
|
+
output += `const __exports = {\n ${exports.join(',\n ')}\n}`
|
|
440
|
+
output += `\nexport default ${options.exportDefault}`
|
|
341
441
|
|
|
442
|
+
return output
|
|
443
|
+
}
|
|
342
444
|
|
|
343
445
|
const variableNotation = {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
446
|
+
camelCase,
|
|
447
|
+
pascalCase,
|
|
448
|
+
snakeCase,
|
|
449
|
+
constantCase
|
|
348
450
|
}
|
|
349
451
|
|
|
350
|
-
export default compile
|
|
452
|
+
export default compile
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluent-transpiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ftl": "cli.js"
|
|
9
9
|
},
|
|
10
|
-
"files":[
|
|
10
|
+
"files": [
|
|
11
11
|
"cli.js",
|
|
12
12
|
"index.js"
|
|
13
13
|
],
|