fluent-transpiler 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js 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
- .default('camelCase')
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
@@ -10,18 +10,24 @@ const exportDefault = `(id, params) => {
10
10
  `
11
11
  export const compile = (src, opts) => {
12
12
  const options = {
13
- variableNotation: 'camelCase',
14
- disableMinify: false, // TODO needs better name
15
- params: 'params',
16
13
  comments: true,
17
14
  errorOnJunk: true,
15
+ includeMessages: [],
16
+ excludeMessages: [],
17
+ //treeShaking: false,
18
+ variableNotation: 'camelCase',
19
+ disableMinify: false, // TODO needs better name strictInterface?
18
20
  useIsolating: false,
21
+ params: 'params',
19
22
  exportDefault,
20
23
  ...opts,
21
24
  }
22
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]
23
28
 
24
29
  const metadata = {}
30
+ const exports = []
25
31
  const functions = {} // global functions
26
32
  let variable
27
33
 
@@ -90,6 +96,15 @@ export const compile = (src, opts) => {
90
96
  },
91
97
  Message: (data) => {
92
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
+
93
108
  const templateStringLiteral = data.value && compileType(data.value, data.type)
94
109
  metadata[assignment].attributes = data.attributes.length
95
110
  let attributes = {}
@@ -100,29 +115,47 @@ export const compile = (src, opts) => {
100
115
  }).join(',\n')}\n }`
101
116
  }
102
117
  //
118
+ let message = ''
103
119
  if (!options.disableMinify) {
104
- if (metadata[variable].attributes) {
120
+ if (metadata[assignment].attributes) {
105
121
  if (metadata[assignment].params) {
106
- return `export const ${assignment} = (${options.params}) => ({
122
+ message = `(${options.params}) => ({
107
123
  value:${templateStringLiteral},
108
124
  attributes:${attributes}
109
125
  })\n`
110
- }
111
- return `export const ${assignment} = {
126
+ } else {
127
+ message = `{
112
128
  value: ${templateStringLiteral},
113
129
  attributes: ${attributes}
114
130
  }\n`
131
+ }
132
+ } else if (metadata[assignment].params) {
133
+ message = `(${options.params}) => ${templateStringLiteral}\n`
134
+ } else {
135
+ message = `${templateStringLiteral}\n`
115
136
  }
116
- if (metadata[assignment].params) {
117
- return `export const ${assignment} = (${options.params}) => ${templateStringLiteral}\n`
118
- }
119
- return `export const ${assignment} = ${templateStringLiteral}\n`
120
- }
137
+ } else {
121
138
  // consistent API
122
- return `export const ${variable} = (${metadata[assignment].params ? options.params : ''}) => ({
139
+ message = `(${metadata[assignment].params ? options.params : ''}) => ({
123
140
  value:${templateStringLiteral},
124
141
  attributes:${attributes}
125
142
  })\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 {
152
+ if (assignment === metadata[assignment].id) {
153
+ exports.push(`${assignment}: ${message}`)
154
+ } else {
155
+ exports.push(`'${metadata[assignment].id}': ${message}`)
156
+ }
157
+ }*/
158
+ return ''
126
159
  },
127
160
  Comment: (data) => {
128
161
  if (options.comments) return `// # ${data.content}\n`
@@ -300,14 +333,7 @@ const __select = (value, cases, fallback, options) => {
300
333
  `
301
334
  }
302
335
  output += `\n`+translations
303
- output += `const __exports = {${Object.keys(metadata)
304
- .filter(key => !metadata[key].term)
305
- .map(key => {
306
- if (key === metadata[key].id) return key
307
- return `'${metadata[key].id}':${key}`
308
- })
309
- .join(', ')
310
- }}\n`
336
+ output += `const __exports = {\n ${exports.join(',\n ')}\n}`
311
337
  output += `\nexport default ${options.exportDefault}`
312
338
 
313
339
  return output
@@ -320,6 +346,5 @@ const variableNotation = {
320
346
  snakeCase,
321
347
  constantCase
322
348
  }
323
- // for `export defaults`
324
349
 
325
350
  export default compile
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "fluent-transpiler",
3
- "version": "0.0.1",
4
- "description": "Transpile Fluent (ftl) files into optomized, tree-shakable, JavaScript EcmaScript Modules (esm).",
3
+ "version": "0.0.2",
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://middy.js.org",
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",
@@ -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)
@@ -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
- }