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.
Files changed (3) hide show
  1. package/README.md +18 -16
  2. package/index.js +398 -294
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # fluent-transpiler
2
- Transpile Fluent (ftl) files into optomized, tree-shakable, JavaScript EcmaScript Modules (esm).
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 | Description |
33
- |--------|-------------|
34
- | locale | What locale(s) to be used. Multiple can be set to allow for fallback. i.e. en-CA |
35
- | comments | Include comments in output file. Default: true |
36
- | 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`, `() => ''`, `() => ({})`) |
37
- | errorOnJunk | Throw error when `Junk` is parsed. Default: true |
38
- | variableNotation | What variable notation to use with exports. Default: `camelCase` |
39
- | useIsolating | Wrap placeable with \u2068 and \u2069. Default: false |
40
- | exportDefault | Allows the overwriting of the `export default` to allow for custom uses. Default: See code |
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
- 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)) options.includeMessages = [options.includeMessages]
27
- if (!Array.isArray(options.excludeMessages)) options.excludeMessages = [options.excludeMessages]
28
-
29
- const metadata = {}
30
- const exports = []
31
- const functions = {} // global functions
32
- let variable
33
-
34
- const regexpValidVariable = /^[a-zA-Z]+[a-zA-Z0-9]*$/
35
- const compileAssignment = (data) => {
36
- variable = compileType(data)
37
- metadata[variable] = {
38
- id: data.name,
39
- term: false,
40
- params: false
41
- }
42
- return variable
43
- }
44
-
45
- const compileFunctionArguments = (data) => {
46
- const positional = data.arguments?.positional.map(data => {
47
- return types[data.type](data)
48
- })
49
- const named = data.arguments?.named.reduce((obj, data) => {
50
- // NamedArgument
51
- const key = data.name.name
52
- const value = compileType(data.value, data.type)
53
- obj[key] = value
54
- return obj
55
- }, {})
56
- return {positional, named}
57
- }
58
-
59
- const compileType = (data, parent) => {
60
- try {
61
- return types[data.type](data, parent)
62
- } catch(e) {
63
- console.error('Error:', e.message, data, e.stack)
64
- throw new Error(e.message, {cause:data, stack:e.stack})
65
- }
66
- }
67
-
68
- const types = {
69
- Identifier: (data) => {
70
- const value = variableNotation[options.variableNotation](data.name)
71
- // Check for reserved words - TODO add in rest
72
- if (['const','default','enum','if'].includes(value)) {
73
- return '_'+value
74
- }
75
- return value
76
- },
77
- Attribute: (data) => {
78
- const key = compileType(data.id)
79
- const value = compileType(data.value, data.type)
80
- return ` ${key}: ${value}`
81
- },
82
- Pattern: (data, parent) => {
83
- return '`' + data.elements.map(data => {
84
- return compileType(data, parent)
85
- }).join('') + '`'
86
- },
87
- // resources
88
- Term: (data) => {
89
- const assignment = compileAssignment(data.id)
90
- const templateStringLiteral = compileType(data.value)
91
- metadata[assignment].term = true
92
- if (metadata[assignment].params) {
93
- return `const ${assignment} = (${options.params}) => ${templateStringLiteral}\n`
94
- }
95
- return `const ${assignment} = ${templateStringLiteral}\n`
96
- },
97
- Message: (data) => {
98
- const assignment = compileAssignment(data.id)
99
-
100
- if (options.includeMessages.length && !options.includeMessages.includes(assignment)) {
101
- return ''
102
- }
103
-
104
- if (options.excludeMessages.length && options.excludeMessages.includes(assignment)) {
105
- return ''
106
- }
107
-
108
- const templateStringLiteral = data.value && compileType(data.value, data.type)
109
- metadata[assignment].attributes = data.attributes.length
110
- let attributes = {}
111
- if (metadata[assignment].attributes) {
112
- // use Object.create(null) ?
113
- attributes = `{\n${data.attributes.map(data => {
114
- return ' '+compileType(data)
115
- }).join(',\n')}\n }`
116
- }
117
- //
118
- let message = ''
119
- if (!options.disableMinify) {
120
- if (metadata[assignment].attributes) {
121
- if (metadata[assignment].params) {
122
- message = `(${options.params}) => ({
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
- } else {
127
- message = `{
143
+ } else {
144
+ message = `{
128
145
  value: ${templateStringLiteral},
129
146
  attributes: ${attributes}
130
147
  }\n`
131
- }
132
- } else if (metadata[assignment].params) {
133
- message = `(${options.params}) => ${templateStringLiteral}\n`
134
- } else {
135
- message = `${templateStringLiteral}\n`
136
- }
137
- } else {
138
- // consistent API
139
- message = `(${metadata[assignment].params ? options.params : ''}) => ({
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
- //if (options.treeShaking) {
145
- if (assignment === metadata[assignment].id) {
146
- exports.push(`${assignment}`)
147
- } else {
148
- exports.push(`'${metadata[assignment].id}': ${assignment}`)
149
- }
150
- return `export const ${assignment} = ${message}`
151
- /*} else {
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
- return ''
159
- },
160
- Comment: (data) => {
161
- if (options.comments) return `// # ${data.content}\n`
162
- return ''
163
- },
164
- GroupComment: (data) => {
165
- if (options.comments) return `// ## ${data.content}\n`
166
- return ''
167
- },
168
- ResourceComment: (data) => {
169
- if (options.comments) return `// ### ${data.content}\n`
170
- return ''
171
- },
172
- Junk: (data) => {
173
- if (options.errorOnJunk) {
174
- throw new Error('Junk found', {cause:data})
175
- }
176
- console.error('Error: Skipping Junk', JSON.stringify(data, null, 2))
177
- return ''
178
- },
179
- // Element
180
- TextElement: (data) => {
181
- return data.value
182
- },
183
- Placeable: (data, parent) => {
184
- return `${options.useIsolating ? '\u2068' : '' }\${${compileType(data.expression, parent)}}${options.useIsolating ? '\u2069' : '' }`
185
- },
186
- // Expression
187
- StringLiteral: (data, parent) => {
188
- // JSON.stringify at parent level
189
- if (['NamedArgument'].includes(parent)) {
190
- return `${data.value}`
191
- }
192
- return `"${data.value}"`
193
- },
194
- NumberLiteral: (data) => {
195
- const decimal = Number.parseFloat(data.value)
196
- const number = Number.isInteger(decimal) ? Number.parseInt(data.value) : decimal
197
- return Intl.NumberFormat(options.locale).format(number)
198
- },
199
- VariableReference: (data, parent) => {
200
- functions.__formatVariable = true
201
- metadata[variable].params = true
202
- const value = `${options.params}?.${data.id.name}`
203
- if (['Message','Variant','Attribute'].includes(parent)) {
204
- return `__formatVariable(${value})`
205
- }
206
- return value
207
- },
208
- MessageReference: (data) => {
209
- const messageName = compileType(data.id)
210
- metadata[variable].params ||= metadata[messageName].params
211
- if (!options.disableMinify) {
212
- if (metadata[messageName].params) {
213
- return `${messageName}(${options.params})`
214
- }
215
- return `${messageName}`
216
- }
217
- return `${messageName}(${metadata[messageName].params ? options.params : ''})`
218
- },
219
- TermReference: (data) => {
220
- const termName = compileType(data.id)
221
- metadata[variable].params ||= metadata[termName].params
222
-
223
- let params
224
- if (metadata[termName].params) {
225
- let {named} = compileFunctionArguments(data)
226
- named = JSON.stringify(named)
227
- if (named) {
228
- params = `{ ...${options.params}, ${named.substring(1, named.length-1)} }`
229
- } else {
230
- params = options.params
231
- }
232
- }
233
- if (!options.disableMinify) {
234
- if (metadata[termName].params) {
235
- return `${termName}(${params})`
236
- }
237
- return `${termName}`
238
- }
239
- return `${termName}(${params ? params : ''})`
240
- },
241
- NamedArgument:(data) => {
242
- // Inconsistent: `NamedArgument` uses `name` instead of `id` for Identifier
243
- const key = data.name.name // Don't transform value
244
- const value = compileType(data.value, data.type)
245
- return `${key}: ${value}`
246
- },
247
- SelectExpression: (data) => {
248
- functions.__select = true
249
- metadata[variable].params = true
250
- const value = compileType(data.selector)
251
- //const options = data.selector
252
- let fallback
253
- return `__select(\n ${value},\n {\n${data.variants.filter(data => {
254
- if (data.default) {
255
- fallback = compileType(data.value, data.type)
256
- }
257
- return !data.default
258
- }).map(data => {
259
- return ' '+compileType(data)
260
- }).join(',\n')}\n },\n ${fallback}\n )`
261
- },
262
- Variant: (data, parent) => {
263
- // Inconsistent: `Variant` uses `key` instead of `id` for Identifier
264
- const key = compileType(data.key)
265
- const value = compileType(data.value, data.type)
266
- return ` '${key}': ${value}`
267
- },
268
- FunctionReference: (data) => {
269
- return `${types[data.id.name](compileFunctionArguments(data))}`
270
- },
271
- // Functions
272
- DATETIME: (data) => {
273
- functions.__formatDateTime = true
274
- const {positional, named} = data
275
- const value = positional.shift()
276
- return `__formatDateTime(${value}, ${JSON.stringify(named)})`
277
- },
278
- NUMBER: (data) => {
279
- functions.__formatNumber = true
280
- const {positional, named} = data
281
- const value = positional.shift()
282
- return `__formatNumber(${value}, ${JSON.stringify(named)})`
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
- if (/\t/.test(src)) {
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
- const {body} = parse(src)
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
- let output = ``
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
- if (functions.__formatDateTime) {
303
- output += `
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
- if (functions.__formatVariable || functions.__formatNumber) {
310
- output += `
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
- if (functions.__formatVariable) {
317
- output += `
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
- if (functions.__select) {
327
- output += `
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
- output += `\n`+translations
336
- output += `const __exports = {\n ${exports.join(',\n ')}\n}`
337
- output += `\nexport default ${options.exportDefault}`
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
- camelCase,
345
- pascalCase,
346
- snakeCase,
347
- constantCase
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.2",
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
  ],