fluent-transpiler 0.0.1 → 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/cli.js +6 -1
- package/index.js +402 -273
- package/package.json +7 -3
- package/test/files/index.ftl +0 -123
- package/test/files/index.mjs +0 -125
- package/test/index.js +0 -84
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/cli.js
CHANGED
|
@@ -21,9 +21,14 @@ new Command()
|
|
|
21
21
|
.addOption(new Option('--comments', 'Include comments in output file.')
|
|
22
22
|
.preset(true)
|
|
23
23
|
)
|
|
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
|
+
/*.addOption(new Option('--tree-shaking', 'Export all messages to allow tree shaking')
|
|
27
|
+
.preset(true)
|
|
28
|
+
)*/
|
|
24
29
|
.addOption(new Option('--variable-notation <variableNotation>', 'What variable notation to use with exports')
|
|
25
30
|
.choices(['camelCase','pascalCase','constantCase','snakeCase'])
|
|
26
|
-
|
|
31
|
+
.default('camelCase')
|
|
27
32
|
)
|
|
28
33
|
.addOption(new Option('--disable-minify', 'If disabled, all exported messages will have the same interface `(params) => ({value, attributes})`.')
|
|
29
34
|
.preset(true)
|
package/index.js
CHANGED
|
@@ -9,279 +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
|
-
|
|
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}) => ({
|
|
107
140
|
value:${templateStringLiteral},
|
|
108
141
|
attributes:${attributes}
|
|
109
142
|
})\n`
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
} else {
|
|
144
|
+
message = `{
|
|
112
145
|
value: ${templateStringLiteral},
|
|
113
146
|
attributes: ${attributes}
|
|
114
147
|
}\n`
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 : ''}) => ({
|
|
123
157
|
value:${templateStringLiteral},
|
|
124
158
|
attributes:${attributes}
|
|
125
159
|
})\n`
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return ''
|
|
138
|
-
},
|
|
139
|
-
Junk: (data) => {
|
|
140
|
-
if (options.errorOnJunk) {
|
|
141
|
-
throw new Error('Junk found', {cause:data})
|
|
142
|
-
}
|
|
143
|
-
console.error('Error: Skipping Junk', JSON.stringify(data, null, 2))
|
|
144
|
-
return ''
|
|
145
|
-
},
|
|
146
|
-
// Element
|
|
147
|
-
TextElement: (data) => {
|
|
148
|
-
return data.value
|
|
149
|
-
},
|
|
150
|
-
Placeable: (data, parent) => {
|
|
151
|
-
return `${options.useIsolating ? '\u2068' : '' }\${${compileType(data.expression, parent)}}${options.useIsolating ? '\u2069' : '' }`
|
|
152
|
-
},
|
|
153
|
-
// Expression
|
|
154
|
-
StringLiteral: (data, parent) => {
|
|
155
|
-
// JSON.stringify at parent level
|
|
156
|
-
if (['NamedArgument'].includes(parent)) {
|
|
157
|
-
return `${data.value}`
|
|
158
|
-
}
|
|
159
|
-
return `"${data.value}"`
|
|
160
|
-
},
|
|
161
|
-
NumberLiteral: (data) => {
|
|
162
|
-
const decimal = Number.parseFloat(data.value)
|
|
163
|
-
const number = Number.isInteger(decimal) ? Number.parseInt(data.value) : decimal
|
|
164
|
-
return Intl.NumberFormat(options.locale).format(number)
|
|
165
|
-
},
|
|
166
|
-
VariableReference: (data, parent) => {
|
|
167
|
-
functions.__formatVariable = true
|
|
168
|
-
metadata[variable].params = true
|
|
169
|
-
const value = `${options.params}?.${data.id.name}`
|
|
170
|
-
if (['Message','Variant','Attribute'].includes(parent)) {
|
|
171
|
-
return `__formatVariable(${value})`
|
|
172
|
-
}
|
|
173
|
-
return value
|
|
174
|
-
},
|
|
175
|
-
MessageReference: (data) => {
|
|
176
|
-
const messageName = compileType(data.id)
|
|
177
|
-
metadata[variable].params ||= metadata[messageName].params
|
|
178
|
-
if (!options.disableMinify) {
|
|
179
|
-
if (metadata[messageName].params) {
|
|
180
|
-
return `${messageName}(${options.params})`
|
|
181
|
-
}
|
|
182
|
-
return `${messageName}`
|
|
183
|
-
}
|
|
184
|
-
return `${messageName}(${metadata[messageName].params ? options.params : ''})`
|
|
185
|
-
},
|
|
186
|
-
TermReference: (data) => {
|
|
187
|
-
const termName = compileType(data.id)
|
|
188
|
-
metadata[variable].params ||= metadata[termName].params
|
|
189
|
-
|
|
190
|
-
let params
|
|
191
|
-
if (metadata[termName].params) {
|
|
192
|
-
let {named} = compileFunctionArguments(data)
|
|
193
|
-
named = JSON.stringify(named)
|
|
194
|
-
if (named) {
|
|
195
|
-
params = `{ ...${options.params}, ${named.substring(1, named.length-1)} }`
|
|
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 {
|
|
169
|
+
if (assignment === metadata[assignment].id) {
|
|
170
|
+
exports.push(`${assignment}: ${message}`)
|
|
196
171
|
} else {
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (!options.disableMinify) {
|
|
201
|
-
if (metadata[termName].params) {
|
|
202
|
-
return `${termName}(${params})`
|
|
172
|
+
exports.push(`'${metadata[assignment].id}': ${message}`)
|
|
203
173
|
}
|
|
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
|
-
|
|
174
|
+
}*/
|
|
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']
|
|
251
374
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
console.error('Source file contains tab characters (\t), replacing with <space>x4')
|
|
255
|
-
src = src.replace(/\t/g, ' ')
|
|
375
|
+
if (Math.abs(elapsed) < msPerHour) {
|
|
376
|
+
return [Math.round(elapsed / msPerMinute), 'minute']
|
|
256
377
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let translations = ``
|
|
260
|
-
for(const data of body) {
|
|
261
|
-
translations += compileType(data)
|
|
378
|
+
if (Math.abs(elapsed) < msPerDay) {
|
|
379
|
+
return [Math.round(elapsed / msPerHour), 'hour']
|
|
262
380
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (functions.__formatVariable || functions.__formatDateTime || functions.__formatNumber) {
|
|
266
|
-
output += `const __locales = ${JSON.stringify(opts.locale)}\n`
|
|
381
|
+
if (Math.abs(elapsed) < msPerWeek * 2) {
|
|
382
|
+
return [Math.round(elapsed / msPerDay), 'day']
|
|
267
383
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 += `
|
|
271
407
|
const __formatDateTime = (value, options) => {
|
|
408
|
+
if (typeof value === 'string') value = new Date(value)
|
|
409
|
+
if (isNaN(value.getTime())) return value
|
|
272
410
|
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
273
411
|
}
|
|
274
412
|
`
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
413
|
+
}
|
|
414
|
+
if (functions.__formatVariable || functions.__formatNumber) {
|
|
415
|
+
output += `
|
|
278
416
|
const __formatNumber = (value, options) => {
|
|
279
417
|
return new Intl.NumberFormat(__locales, options).format(value)
|
|
280
418
|
}
|
|
281
419
|
`
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
420
|
+
}
|
|
421
|
+
if (functions.__formatVariable) {
|
|
422
|
+
output += `
|
|
285
423
|
const __formatVariable = (value) => {
|
|
286
424
|
if (typeof value === 'string') return value
|
|
287
425
|
const decimal = Number.parseFloat(value)
|
|
@@ -289,37 +427,28 @@ const __formatVariable = (value) => {
|
|
|
289
427
|
return __formatNumber(number)
|
|
290
428
|
}
|
|
291
429
|
`
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
430
|
+
}
|
|
431
|
+
if (functions.__select) {
|
|
432
|
+
output += `
|
|
295
433
|
const __select = (value, cases, fallback, options) => {
|
|
296
434
|
const pluralRules = new Intl.PluralRules(__locales, options)
|
|
297
435
|
const rule = pluralRules.select(value)
|
|
298
436
|
return cases[value] ?? cases[rule] ?? fallback
|
|
299
437
|
}
|
|
300
438
|
`
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.map(key => {
|
|
306
|
-
if (key === metadata[key].id) return key
|
|
307
|
-
return `'${metadata[key].id}':${key}`
|
|
308
|
-
})
|
|
309
|
-
.join(', ')
|
|
310
|
-
}}\n`
|
|
311
|
-
output += `\nexport default ${options.exportDefault}`
|
|
312
|
-
|
|
313
|
-
return output
|
|
314
|
-
}
|
|
439
|
+
}
|
|
440
|
+
output += `\n` + translations
|
|
441
|
+
output += `const __exports = {\n ${exports.join(',\n ')}\n}`
|
|
442
|
+
output += `\nexport default ${options.exportDefault}`
|
|
315
443
|
|
|
444
|
+
return output
|
|
445
|
+
}
|
|
316
446
|
|
|
317
447
|
const variableNotation = {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
448
|
+
camelCase,
|
|
449
|
+
pascalCase,
|
|
450
|
+
snakeCase,
|
|
451
|
+
constantCase
|
|
322
452
|
}
|
|
323
|
-
// for `export defaults`
|
|
324
453
|
|
|
325
|
-
export default compile
|
|
454
|
+
export default compile
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluent-transpiler",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Transpile Fluent (ftl) files into
|
|
3
|
+
"version": "0.1.0",
|
|
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": [
|
|
11
|
+
"cli.js",
|
|
12
|
+
"index.js"
|
|
13
|
+
],
|
|
10
14
|
"scripts": {
|
|
11
15
|
"test": "npm run test:cli && npm run test:unit",
|
|
12
16
|
"test:cli": "./cli.js --locale en-CA --locale en --use-isolating --variable-notation camelCase test/files/index.ftl --output test/files/index.mjs",
|
|
@@ -33,7 +37,7 @@
|
|
|
33
37
|
"bugs": {
|
|
34
38
|
"url": "https://github.com/willfarrell/fluent-transpiler/issues"
|
|
35
39
|
},
|
|
36
|
-
"homepage": "https://
|
|
40
|
+
"homepage": "https://github.com/willfarrell/fluent-transpiler",
|
|
37
41
|
"dependencies": {
|
|
38
42
|
"@fluent/syntax": "0.18.1",
|
|
39
43
|
"change-case": "4.1.2",
|
package/test/files/index.ftl
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# https://projectfluent.org/fluent/guide/
|
|
2
|
-
# Writing Text
|
|
3
|
-
text = text: hard coded.
|
|
4
|
-
|
|
5
|
-
## Placeables
|
|
6
|
-
replaceParam = param: { $string } | { $integer } | {$decimal} | {$number} .
|
|
7
|
-
|
|
8
|
-
-term = Firefox
|
|
9
|
-
replaceTerm = term: { -term }.
|
|
10
|
-
|
|
11
|
-
-brand-name =
|
|
12
|
-
{ $case ->
|
|
13
|
-
*[nominative] Firefox
|
|
14
|
-
[locative] Firefoksie
|
|
15
|
-
}
|
|
16
|
-
parameterized-terms = Informacje o { -brand-name(case: "locative") }.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-term-with-var = {$number}
|
|
20
|
-
termWithVariable = {-term-with-var}
|
|
21
|
-
|
|
22
|
-
### Message References
|
|
23
|
-
messageValue = message: { replaceTerm }
|
|
24
|
-
messageNestedParamValue = message: { replaceParam }
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Special Characters
|
|
28
|
-
openingBrace = This message features an opening curly brace: {"{"}.
|
|
29
|
-
closingBrace = This message features a closing curly brace: {"}"}.
|
|
30
|
-
|
|
31
|
-
blankIsRemoved = This message starts with no blanks.
|
|
32
|
-
blankIsPreserved = {" "}This message starts with 4 spaces.
|
|
33
|
-
|
|
34
|
-
leadingBracket =
|
|
35
|
-
This message has an opening square bracket
|
|
36
|
-
at the beginning of the third line:
|
|
37
|
-
{"["}.
|
|
38
|
-
|
|
39
|
-
literalQuote = Text in "double quotes".
|
|
40
|
-
# This is OK, but cryptic and hard to read and edit.
|
|
41
|
-
literalEscapedQuote = Text in {"\""}double quotes{"\""}.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
privacyLabel = Privacy{"\u00A0"}Policy
|
|
45
|
-
|
|
46
|
-
# The dash character is an EM DASH but depending on the font face,
|
|
47
|
-
# it might look like an EN DASH.
|
|
48
|
-
dash = It's a dash—or is it?
|
|
49
|
-
|
|
50
|
-
dashUnicode = It's a dash{"\u2014"}or is it?
|
|
51
|
-
|
|
52
|
-
emoji = 😂
|
|
53
|
-
emojiUnicode = {"\u01F602"}
|
|
54
|
-
|
|
55
|
-
## Multiline Text
|
|
56
|
-
singleLine = Text can be written in a single line.
|
|
57
|
-
|
|
58
|
-
multiLine = Text can also span multiple lines
|
|
59
|
-
as long as each new line is indented
|
|
60
|
-
by at least one space.
|
|
61
|
-
|
|
62
|
-
block =
|
|
63
|
-
Sometimes it's more readable to format
|
|
64
|
-
multiline text as a "block", which means
|
|
65
|
-
starting it on a new line. All lines must
|
|
66
|
-
be indented by at least one space.
|
|
67
|
-
|
|
68
|
-
leadingBlankSpaces = This message's value starts with the word "This".
|
|
69
|
-
leadingBlankLines =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
This message's value starts with the word "This".
|
|
73
|
-
The blank lines under the identifier are ignored.
|
|
74
|
-
|
|
75
|
-
blankLines =
|
|
76
|
-
|
|
77
|
-
The blank line above this line is ignored.
|
|
78
|
-
This is a second line of the value.
|
|
79
|
-
|
|
80
|
-
The blank line above this line is preserved.
|
|
81
|
-
|
|
82
|
-
multiLineIndent =
|
|
83
|
-
This message has 4 spaces of indent
|
|
84
|
-
on the second line of its value.
|
|
85
|
-
|
|
86
|
-
## Functions
|
|
87
|
-
timeElapsed = Time elapsed: { NUMBER($number, maximumFractionDigits: 0) }s.
|
|
88
|
-
|
|
89
|
-
todayIs = Today is { DATETIME($date) }
|
|
90
|
-
|
|
91
|
-
## Selectors
|
|
92
|
-
selectorNumberCardinal =
|
|
93
|
-
{ $number ->
|
|
94
|
-
[zero] There are {$number} (zero).
|
|
95
|
-
[one] There are {$number} (one).
|
|
96
|
-
*[other] There are { $number } (other).
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
selectorNumberOrdinal =
|
|
100
|
-
{ NUMBER($number, type: "ordinal") ->
|
|
101
|
-
[-1] There are {$number} (-1).
|
|
102
|
-
[zero] There are {$number} (zero).
|
|
103
|
-
[one] There are {$number} (one).
|
|
104
|
-
[two] There are {$number} (two).
|
|
105
|
-
[3.0] There are {$number} (3).
|
|
106
|
-
[few] There are {$number} (few).
|
|
107
|
-
[many] There are {$number} (many).
|
|
108
|
-
[toomany] There are {$number} (toomany).
|
|
109
|
-
*[other] There are { $number } (other).
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
subSelector = {selectorNumberOrdinal}
|
|
113
|
-
|
|
114
|
-
## Attributes
|
|
115
|
-
loginInput = Predefined value
|
|
116
|
-
.placeholder = email@example.com
|
|
117
|
-
.ariaLabel = Login input value
|
|
118
|
-
.title = Type your login email
|
|
119
|
-
|
|
120
|
-
attributeHowTo =
|
|
121
|
-
To add an attribute to this messages, write
|
|
122
|
-
{".attr = Value"} on a new line.
|
|
123
|
-
.attr = An actual attribute (not part of the text value above)
|
package/test/files/index.mjs
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
const __locales = "en-CA"
|
|
2
|
-
|
|
3
|
-
const __formatDateTime = (value, options) => {
|
|
4
|
-
return new Intl.DateTimeFormat(__locales, options).format(value)
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const __formatNumber = (value, options) => {
|
|
8
|
-
return new Intl.NumberFormat(__locales, options).format(value)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const __formatVariable = (value) => {
|
|
12
|
-
if (typeof value === 'string') return value
|
|
13
|
-
const decimal = Number.parseFloat(value)
|
|
14
|
-
const number = Number.isInteger(decimal) ? Number.parseInt(value) : decimal
|
|
15
|
-
return __formatNumber(number)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const __select = (value, cases, fallback, options) => {
|
|
19
|
-
const pluralRules = new Intl.PluralRules(__locales, options)
|
|
20
|
-
const rule = pluralRules.select(value)
|
|
21
|
-
return cases[value] ?? cases[rule] ?? fallback
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const text = `text: hard coded.`
|
|
25
|
-
// ## Placeables
|
|
26
|
-
export const replaceParam = (params) => `param: ${__formatVariable(params?.string)} | ${__formatVariable(params?.integer)} | ${__formatVariable(params?.decimal)} | ${__formatVariable(params?.number)} .`
|
|
27
|
-
const term = `Firefox`
|
|
28
|
-
export const replaceTerm = `term: ${term}.`
|
|
29
|
-
const brandName = (params) => `${__select(
|
|
30
|
-
params?.case,
|
|
31
|
-
{
|
|
32
|
-
'locative': `Firefoksie`
|
|
33
|
-
},
|
|
34
|
-
`Firefox`
|
|
35
|
-
)}`
|
|
36
|
-
export const parameterizedTerms = (params) => `Informacje o ${brandName({ ...params, "case":"locative" })}.`
|
|
37
|
-
const termWithVar = (params) => `${params?.number}`
|
|
38
|
-
export const termWithVariable = (params) => `${termWithVar(params)}`
|
|
39
|
-
// ### Message References
|
|
40
|
-
export const messageValue = `message: ${replaceTerm}`
|
|
41
|
-
export const messageNestedParamValue = (params) => `message: ${replaceParam(params)}`
|
|
42
|
-
// ## Special Characters
|
|
43
|
-
export const openingBrace = `This message features an opening curly brace: ${"{"}.`
|
|
44
|
-
export const closingBrace = `This message features a closing curly brace: ${"}"}.`
|
|
45
|
-
export const blankIsRemoved = `This message starts with no blanks.`
|
|
46
|
-
export const blankIsPreserved = `${" "}This message starts with 4 spaces.`
|
|
47
|
-
export const leadingBracket = `This message has an opening square bracket
|
|
48
|
-
at the beginning of the third line:
|
|
49
|
-
${"["}.`
|
|
50
|
-
export const literalQuote = `Text in "double quotes".`
|
|
51
|
-
export const literalEscapedQuote = `Text in ${"\""}double quotes${"\""}.`
|
|
52
|
-
export const privacyLabel = `Privacy${"\u00A0"}Policy`
|
|
53
|
-
export const dash = `It's a dash—or is it?`
|
|
54
|
-
export const dashUnicode = `It's a dash${"\u2014"}or is it?`
|
|
55
|
-
export const emoji = `😂`
|
|
56
|
-
export const emojiUnicode = `${"\u01F602"}`
|
|
57
|
-
// ## Multiline Text
|
|
58
|
-
export const singleLine = `Text can be written in a single line.`
|
|
59
|
-
export const multiLine = `Text can also span multiple lines
|
|
60
|
-
as long as each new line is indented
|
|
61
|
-
by at least one space.`
|
|
62
|
-
export const block = `Sometimes it's more readable to format
|
|
63
|
-
multiline text as a "block", which means
|
|
64
|
-
starting it on a new line. All lines must
|
|
65
|
-
be indented by at least one space.`
|
|
66
|
-
export const leadingBlankSpaces = `This message's value starts with the word "This".`
|
|
67
|
-
export const leadingBlankLines = `This message's value starts with the word "This".
|
|
68
|
-
The blank lines under the identifier are ignored.`
|
|
69
|
-
export const blankLines = `The blank line above this line is ignored.
|
|
70
|
-
This is a second line of the value.
|
|
71
|
-
|
|
72
|
-
The blank line above this line is preserved.`
|
|
73
|
-
export const multiLineIndent = `This message has 4 spaces of indent
|
|
74
|
-
on the second line of its value.`
|
|
75
|
-
// ## Functions
|
|
76
|
-
export const timeElapsed = (params) => `Time elapsed: ${__formatNumber(params?.number, {"maximumFractionDigits":"0"})}s.`
|
|
77
|
-
export const todayIs = (params) => `Today is ${__formatDateTime(params?.date, {})}`
|
|
78
|
-
// ## Selectors
|
|
79
|
-
export const selectorNumberCardinal = (params) => `${__select(
|
|
80
|
-
params?.number,
|
|
81
|
-
{
|
|
82
|
-
'zero': `There are ${__formatVariable(params?.number)} (zero).`,
|
|
83
|
-
'one': `There are ${__formatVariable(params?.number)} (one).`
|
|
84
|
-
},
|
|
85
|
-
`There are ${__formatVariable(params?.number)} (other).`
|
|
86
|
-
)}`
|
|
87
|
-
export const selectorNumberOrdinal = (params) => `${__select(
|
|
88
|
-
__formatNumber(params?.number, {"type":"ordinal"}),
|
|
89
|
-
{
|
|
90
|
-
'-1': `There are ${__formatVariable(params?.number)} (-1).`,
|
|
91
|
-
'zero': `There are ${__formatVariable(params?.number)} (zero).`,
|
|
92
|
-
'one': `There are ${__formatVariable(params?.number)} (one).`,
|
|
93
|
-
'two': `There are ${__formatVariable(params?.number)} (two).`,
|
|
94
|
-
'3': `There are ${__formatVariable(params?.number)} (3).`,
|
|
95
|
-
'few': `There are ${__formatVariable(params?.number)} (few).`,
|
|
96
|
-
'many': `There are ${__formatVariable(params?.number)} (many).`,
|
|
97
|
-
'toomany': `There are ${__formatVariable(params?.number)} (toomany).`
|
|
98
|
-
},
|
|
99
|
-
`There are ${__formatVariable(params?.number)} (other).`
|
|
100
|
-
)}`
|
|
101
|
-
export const subSelector = (params) => `${selectorNumberOrdinal(params)}`
|
|
102
|
-
// ## Attributes
|
|
103
|
-
export const loginInput = {
|
|
104
|
-
value: `Predefined value`,
|
|
105
|
-
attributes: {
|
|
106
|
-
placeholder: `email@example.com`,
|
|
107
|
-
ariaLabel: `Login input value`,
|
|
108
|
-
title: `Type your login email`
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
export const attributeHowTo = {
|
|
112
|
-
value: `To add an attribute to this messages, write
|
|
113
|
-
${".attr = Value"} on a new line.`,
|
|
114
|
-
attributes: {
|
|
115
|
-
attr: `An actual attribute (not part of the text value above)`
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const __exports = {text, replaceParam, replaceTerm, 'parameterized-terms':parameterizedTerms, termWithVariable, messageValue, messageNestedParamValue, openingBrace, closingBrace, blankIsRemoved, blankIsPreserved, leadingBracket, literalQuote, literalEscapedQuote, privacyLabel, dash, dashUnicode, emoji, emojiUnicode, singleLine, multiLine, block, leadingBlankSpaces, leadingBlankLines, blankLines, multiLineIndent, timeElapsed, todayIs, selectorNumberCardinal, selectorNumberOrdinal, subSelector, loginInput, attributeHowTo}
|
|
119
|
-
|
|
120
|
-
export default (id, params) => {
|
|
121
|
-
const source = __exports[id] ?? __exports['_'+id]
|
|
122
|
-
if (typeof source === 'undefined') return '*** '+id+' ***'
|
|
123
|
-
if (typeof source === 'function') return source(params)
|
|
124
|
-
return source
|
|
125
|
-
}
|
package/test/index.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import { deepEqual } from 'node:assert'
|
|
3
|
-
import {readFile,writeFile} from 'node:fs/promises'
|
|
4
|
-
import compile from '../index.js'
|
|
5
|
-
|
|
6
|
-
import { FluentBundle, FluentResource } from "@fluent/bundle"
|
|
7
|
-
|
|
8
|
-
// input
|
|
9
|
-
const ftl = await readFile('./test/files/index.ftl', {encoding:'utf8'})
|
|
10
|
-
|
|
11
|
-
// compiled
|
|
12
|
-
const js = compile(ftl, {locale:'en-CA', variableNotation: 'camelCase', useIsolating:false})
|
|
13
|
-
await writeFile('./test/files/index.mjs', js, 'utf8')
|
|
14
|
-
const {default:fluentCompiled} = await import('./files/index.mjs') // TODO fix default
|
|
15
|
-
|
|
16
|
-
// core
|
|
17
|
-
let bundle = new FluentBundle('en-CA', {useIsolating:false});
|
|
18
|
-
let errors = bundle.addResource(new FluentResource(ftl));
|
|
19
|
-
if (errors.length) {
|
|
20
|
-
console.log('Error: bundle.addResource', errors)
|
|
21
|
-
}
|
|
22
|
-
const fluentBundle = (id, params) => {
|
|
23
|
-
const message = bundle.getMessage(id)
|
|
24
|
-
if (!message) {
|
|
25
|
-
console.log('Error', id, message)
|
|
26
|
-
return 'error'
|
|
27
|
-
}
|
|
28
|
-
const attributes = {}
|
|
29
|
-
for(const attr in message.attributes) {
|
|
30
|
-
attributes[attr] = bundle.formatPattern(message.attributes[attr], params)
|
|
31
|
-
}
|
|
32
|
-
const value = bundle.formatPattern(message.value, params)
|
|
33
|
-
if (Object.keys(message.attributes).length) {
|
|
34
|
-
return {value, attributes}
|
|
35
|
-
}
|
|
36
|
-
return value
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// compile
|
|
40
|
-
test(`Should throw error when Junk is parsed`, async (t) => {
|
|
41
|
-
try {
|
|
42
|
-
const js = compile(`-brand-name = {}`, {locale:'en-CA', errorOnJunk:true})
|
|
43
|
-
console.log(js)
|
|
44
|
-
throw new Error('fail')
|
|
45
|
-
} catch (e) {
|
|
46
|
-
deepEqual(e.message, 'Junk found')
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test(`Should include comments`, async (t) => {
|
|
51
|
-
const js = compile(`
|
|
52
|
-
# Comment
|
|
53
|
-
## GroupComment
|
|
54
|
-
### ResourceComment
|
|
55
|
-
`, {locale:'en-CA', comments:true})
|
|
56
|
-
deepEqual(js.includes(`// # Comment
|
|
57
|
-
// ## GroupComment
|
|
58
|
-
// ### ResourceComment`), true)
|
|
59
|
-
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// tests
|
|
63
|
-
const params = {string:'0.0',integer:-2, decimal: 3.5, number:9999999.00, date: new Date()}
|
|
64
|
-
for (const id of Object.keys(fluentCompiled)) {
|
|
65
|
-
if (id === 'default') continue
|
|
66
|
-
test(`Should format ${id}`, async (t) => {
|
|
67
|
-
deepEqual(fluentCompiled(id, params), fluentBundle(id, params))
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// selector
|
|
72
|
-
for(const param of [-1,0,1,2,3,4,5,10]) {
|
|
73
|
-
let id = 'selectorNumberCardinal'
|
|
74
|
-
test(`Should format ${id} with ${param}`, async (t) => {
|
|
75
|
-
params.number = param
|
|
76
|
-
deepEqual(fluentCompiled(id, params), fluentBundle(id, params))
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
id = 'selectorNumberOrdinal'
|
|
80
|
-
test(`Should format ${id} with ${param}`, async (t) => {
|
|
81
|
-
params.number = param
|
|
82
|
-
deepEqual(fluentCompiled(id, params), fluentBundle(id, params))
|
|
83
|
-
})
|
|
84
|
-
}
|