fluent-format 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/.claude/settings.local.json +11 -0
- package/.vscodeignore +19 -0
- package/CLAUDE.md +111 -0
- package/README.md +132 -0
- package/biome.json +45 -0
- package/bun.lock +54 -0
- package/cli.ts +103 -0
- package/example.ftl +13 -0
- package/extension.ts +132 -0
- package/formatter.ts +227 -0
- package/index.ts +1 -0
- package/language-configuration.json +19 -0
- package/package.json +99 -0
- package/test-complex.ftl +18 -0
- package/test-locales/en/main.ftl +3 -0
- package/test-locales/en/settings.ftl +5 -0
- package/test-locales/ja/main.ftl +3 -0
- package/test-sort.ftl +12 -0
- package/tsconfig.extension.json +17 -0
- package/tsconfig.json +29 -0
package/.vscodeignore
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.vscode/**
|
|
2
|
+
.vscode-test/**
|
|
3
|
+
node_modules/**
|
|
4
|
+
.gitignore
|
|
5
|
+
.yarnrc
|
|
6
|
+
**/*.map
|
|
7
|
+
**/*.ts
|
|
8
|
+
!out/**/*.js
|
|
9
|
+
tsconfig.json
|
|
10
|
+
tsconfig.extension.json
|
|
11
|
+
test-*.ftl
|
|
12
|
+
example.ftl
|
|
13
|
+
test-locales/**
|
|
14
|
+
debug*.ts
|
|
15
|
+
*.md
|
|
16
|
+
!README.md
|
|
17
|
+
.git
|
|
18
|
+
.DS_Store
|
|
19
|
+
bun.lockb
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
+
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Default to using Bun instead of Node.js.
|
|
8
|
+
|
|
9
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
15
|
+
|
|
16
|
+
## APIs
|
|
17
|
+
|
|
18
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
19
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
20
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
21
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
22
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
23
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
24
|
+
- Bun.$`ls` instead of execa.
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
Use `bun test` to run tests.
|
|
29
|
+
|
|
30
|
+
```ts#index.test.ts
|
|
31
|
+
import { test, expect } from "bun:test";
|
|
32
|
+
|
|
33
|
+
test("hello world", () => {
|
|
34
|
+
expect(1).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Frontend
|
|
39
|
+
|
|
40
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
41
|
+
|
|
42
|
+
Server:
|
|
43
|
+
|
|
44
|
+
```ts#index.ts
|
|
45
|
+
import index from "./index.html"
|
|
46
|
+
|
|
47
|
+
Bun.serve({
|
|
48
|
+
routes: {
|
|
49
|
+
"/": index,
|
|
50
|
+
"/api/users/:id": {
|
|
51
|
+
GET: (req) => {
|
|
52
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// optional websocket support
|
|
57
|
+
websocket: {
|
|
58
|
+
open: (ws) => {
|
|
59
|
+
ws.send("Hello, world!");
|
|
60
|
+
},
|
|
61
|
+
message: (ws, message) => {
|
|
62
|
+
ws.send(message);
|
|
63
|
+
},
|
|
64
|
+
close: (ws) => {
|
|
65
|
+
// handle close
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
development: {
|
|
69
|
+
hmr: true,
|
|
70
|
+
console: true,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
76
|
+
|
|
77
|
+
```html#index.html
|
|
78
|
+
<html>
|
|
79
|
+
<body>
|
|
80
|
+
<h1>Hello, world!</h1>
|
|
81
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With the following `frontend.tsx`:
|
|
87
|
+
|
|
88
|
+
```tsx#frontend.tsx
|
|
89
|
+
import React from "react";
|
|
90
|
+
|
|
91
|
+
// import .css files directly and it works
|
|
92
|
+
import './index.css';
|
|
93
|
+
|
|
94
|
+
import { createRoot } from "react-dom/client";
|
|
95
|
+
|
|
96
|
+
const root = createRoot(document.body);
|
|
97
|
+
|
|
98
|
+
export default function Frontend() {
|
|
99
|
+
return <h1>Hello, world!</h1>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
root.render(<Frontend />);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then, run index.ts
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bun --hot ./index.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# fluent-format
|
|
2
|
+
|
|
3
|
+
A CLI tool to format [Fluent](https://projectfluent.org/) (`.ftl`) files using the official `@fluent/syntax` parser.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Format single `.ftl` files or entire directories recursively
|
|
8
|
+
- Sort messages alphabetically within blank-line-separated groups
|
|
9
|
+
- Check if files are properly formatted (useful for CI/CD)
|
|
10
|
+
- Write formatted output back to files or print to stdout
|
|
11
|
+
- Built with [Bun](https://bun.sh) for speed
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Format and print to stdout
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun run cli.ts <file-or-directory>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Format and write to file
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun run cli.ts --write <file-or-directory>
|
|
31
|
+
# or
|
|
32
|
+
bun run cli.ts -w <file-or-directory>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Check if files are formatted
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun run cli.ts --check <file-or-directory>
|
|
39
|
+
# or
|
|
40
|
+
bun run cli.ts -c <file-or-directory>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This will exit with code 1 if any files need formatting, making it perfect for CI/CD pipelines.
|
|
44
|
+
|
|
45
|
+
### Sort messages alphabetically
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bun run cli.ts --sort <file-or-directory>
|
|
49
|
+
# or
|
|
50
|
+
bun run cli.ts -s <file-or-directory>
|
|
51
|
+
|
|
52
|
+
# Combine with --write to save sorted output
|
|
53
|
+
bun run cli.ts --sort --write <file-or-directory>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The sort feature groups messages by blank lines. Within each group, messages are sorted alphabetically by their ID. Group comments (comments at the start of a group) are preserved and attached to the first message after sorting.
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Format a single file and print to stdout
|
|
62
|
+
bun run cli.ts example.ftl
|
|
63
|
+
|
|
64
|
+
# Format a single file and write changes
|
|
65
|
+
bun run cli.ts --write example.ftl
|
|
66
|
+
|
|
67
|
+
# Sort and format a file
|
|
68
|
+
bun run cli.ts --sort --write example.ftl
|
|
69
|
+
|
|
70
|
+
# Format all .ftl files in a directory
|
|
71
|
+
bun run cli.ts --write ./locales
|
|
72
|
+
|
|
73
|
+
# Sort all .ftl files in a directory
|
|
74
|
+
bun run cli.ts --sort --write ./locales
|
|
75
|
+
|
|
76
|
+
# Check if files are formatted (for CI)
|
|
77
|
+
bun run cli.ts --check ./locales
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Install globally
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bun link
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then you can use it anywhere:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
fluent-format --write my-file.ftl
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
This tool uses the official `@fluent/syntax` parser to:
|
|
95
|
+
1. Parse `.ftl` files into an AST (Abstract Syntax Tree)
|
|
96
|
+
2. Optionally sort messages alphabetically within groups (separated by blank lines)
|
|
97
|
+
3. Serialize the AST back to properly formatted Fluent syntax
|
|
98
|
+
4. Ensure consistent formatting across all your Fluent translation files
|
|
99
|
+
|
|
100
|
+
### Sorting behavior
|
|
101
|
+
|
|
102
|
+
When using the `--sort` option:
|
|
103
|
+
- Messages are grouped by blank lines in the original file
|
|
104
|
+
- Within each group, messages are sorted alphabetically by their ID
|
|
105
|
+
- Comments at the beginning of a group are preserved and remain at the top of that group
|
|
106
|
+
- Blank lines between groups are maintained
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
|
|
110
|
+
**Before sorting:**
|
|
111
|
+
```fluent
|
|
112
|
+
# Authentication
|
|
113
|
+
logout = Log out
|
|
114
|
+
login = Log in
|
|
115
|
+
signup = Sign up
|
|
116
|
+
|
|
117
|
+
# Settings
|
|
118
|
+
theme = Theme
|
|
119
|
+
language = Language
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**After sorting:**
|
|
123
|
+
```fluent
|
|
124
|
+
# Authentication
|
|
125
|
+
login = Log in
|
|
126
|
+
logout = Log out
|
|
127
|
+
signup = Sign up
|
|
128
|
+
|
|
129
|
+
# Settings
|
|
130
|
+
language = Language
|
|
131
|
+
theme = Theme
|
|
132
|
+
```
|
package/biome.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.3.15/schema.json",
|
|
3
|
+
"assist": {
|
|
4
|
+
"actions": {
|
|
5
|
+
"source": {
|
|
6
|
+
"organizeImports": "on"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"files": {
|
|
11
|
+
"includes": [
|
|
12
|
+
"**/*.ts",
|
|
13
|
+
"**/*.tsx",
|
|
14
|
+
"**/*.md",
|
|
15
|
+
"**/*.mdx",
|
|
16
|
+
"**/*.json",
|
|
17
|
+
"!**/_generated",
|
|
18
|
+
"!**/*.gen.ts",
|
|
19
|
+
"!**/*.js",
|
|
20
|
+
"!**/.vinxi",
|
|
21
|
+
"!**/*.queries.ts",
|
|
22
|
+
"!**/node_modules",
|
|
23
|
+
"!**/__pycache__",
|
|
24
|
+
"!**/.venv"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"formatter": {
|
|
28
|
+
"indentStyle": "space",
|
|
29
|
+
"indentWidth": 2
|
|
30
|
+
},
|
|
31
|
+
"linter": {
|
|
32
|
+
"rules": {
|
|
33
|
+
"correctness": {
|
|
34
|
+
"noUnusedVariables": "error",
|
|
35
|
+
"noUnusedImports": "error",
|
|
36
|
+
"useJsxKeyInIterable": "off"
|
|
37
|
+
},
|
|
38
|
+
"a11y": {
|
|
39
|
+
"useSemanticElements": "off",
|
|
40
|
+
"useFocusableInteractive": "off",
|
|
41
|
+
"useKeyWithClickEvents": "off"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/bun.lock
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "fluent-format",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@fluent/syntax": "^0.19.0",
|
|
9
|
+
"commander": "^14.0.3",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"@types/vscode": "^1.109.0",
|
|
14
|
+
"vscode-languageclient": "^9.0.1",
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"typescript": "^5",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"packages": {
|
|
22
|
+
"@fluent/syntax": ["@fluent/syntax@0.19.0", "", {}, "sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ=="],
|
|
23
|
+
|
|
24
|
+
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
|
25
|
+
|
|
26
|
+
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
|
27
|
+
|
|
28
|
+
"@types/vscode": ["@types/vscode@1.109.0", "", {}, "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw=="],
|
|
29
|
+
|
|
30
|
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
|
31
|
+
|
|
32
|
+
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
|
33
|
+
|
|
34
|
+
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
|
35
|
+
|
|
36
|
+
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
|
37
|
+
|
|
38
|
+
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
|
39
|
+
|
|
40
|
+
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
41
|
+
|
|
42
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
43
|
+
|
|
44
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
45
|
+
|
|
46
|
+
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
|
47
|
+
|
|
48
|
+
"vscode-languageclient": ["vscode-languageclient@9.0.1", "", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.5" } }, "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA=="],
|
|
49
|
+
|
|
50
|
+
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
|
51
|
+
|
|
52
|
+
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
|
53
|
+
}
|
|
54
|
+
}
|
package/cli.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { formatFile, formatDirectory } from "./formatter";
|
|
5
|
+
import { existsSync, statSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name("fluent-format")
|
|
12
|
+
.description("A CLI tool to format Fluent (.ftl) files")
|
|
13
|
+
.version("0.1.0")
|
|
14
|
+
.argument("<path>", "File or directory to format")
|
|
15
|
+
.option("-w, --write", "Write formatted output to file", false)
|
|
16
|
+
.option(
|
|
17
|
+
"-c, --check",
|
|
18
|
+
"Check if files are formatted (exit with error if not)",
|
|
19
|
+
false,
|
|
20
|
+
)
|
|
21
|
+
.option(
|
|
22
|
+
"-s, --sort",
|
|
23
|
+
"Sort messages alphabetically within blank-line-separated groups",
|
|
24
|
+
false,
|
|
25
|
+
)
|
|
26
|
+
.action(
|
|
27
|
+
async (
|
|
28
|
+
path: string,
|
|
29
|
+
options: { write: boolean; check: boolean; sort: boolean },
|
|
30
|
+
) => {
|
|
31
|
+
const targetPath = join(process.cwd(), path);
|
|
32
|
+
|
|
33
|
+
if (!existsSync(targetPath)) {
|
|
34
|
+
console.error(`Error: Path not found: ${targetPath}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const stats = statSync(targetPath);
|
|
39
|
+
const formatOptions = { sort: options.sort };
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (stats.isFile()) {
|
|
43
|
+
if (!path.endsWith(".ftl")) {
|
|
44
|
+
console.error("Error: Only .ftl files are supported");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await formatFile(
|
|
49
|
+
targetPath,
|
|
50
|
+
options.write,
|
|
51
|
+
formatOptions,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (options.check) {
|
|
55
|
+
if (!result.isFormatted) {
|
|
56
|
+
console.error(`✗ ${path} needs formatting`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
} else {
|
|
59
|
+
console.log(`✓ ${path} is formatted`);
|
|
60
|
+
}
|
|
61
|
+
} else if (options.write) {
|
|
62
|
+
console.log(`✓ Formatted ${path}`);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(result.content);
|
|
65
|
+
}
|
|
66
|
+
} else if (stats.isDirectory()) {
|
|
67
|
+
const results = await formatDirectory(
|
|
68
|
+
targetPath,
|
|
69
|
+
options.write,
|
|
70
|
+
formatOptions,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (options.check) {
|
|
74
|
+
const unformatted = results.filter((r) => !r.isFormatted);
|
|
75
|
+
if (unformatted.length > 0) {
|
|
76
|
+
unformatted.forEach((r) =>
|
|
77
|
+
console.error(`✗ ${r.path} needs formatting`),
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`✓ All ${results.length} files are formatted`);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
results.forEach((r) => {
|
|
85
|
+
if (options.write) {
|
|
86
|
+
console.log(`✓ Formatted ${r.path}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
if (!options.write) {
|
|
90
|
+
console.log(`Found ${results.length} .ftl files`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(
|
|
96
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
program.parse();
|
package/example.ftl
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Simple Fluent file for testing
|
|
2
|
+
hello = Hello, World!
|
|
3
|
+
welcome = Welcome to { $name }!
|
|
4
|
+
# Comment with details
|
|
5
|
+
goodbye = Goodbye,{ $name }
|
|
6
|
+
.title = Farewell
|
|
7
|
+
multi-line =
|
|
8
|
+
This is a multi-line
|
|
9
|
+
message that spans
|
|
10
|
+
multiple lines
|
|
11
|
+
# Another message
|
|
12
|
+
button-label = Click Me
|
|
13
|
+
.aria-label = Clickable button
|
package/extension.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as vscode from "vscode";
|
|
2
|
+
import { formatFluentContent } from "./formatter";
|
|
3
|
+
|
|
4
|
+
export function activate(context: vscode.ExtensionContext) {
|
|
5
|
+
console.log("Fluent Format extension is now active");
|
|
6
|
+
|
|
7
|
+
// Register format command
|
|
8
|
+
const formatCommand = vscode.commands.registerCommand(
|
|
9
|
+
"fluent-format.format",
|
|
10
|
+
async () => {
|
|
11
|
+
await formatDocument(false);
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// Register format and sort command
|
|
16
|
+
const formatSortCommand = vscode.commands.registerCommand(
|
|
17
|
+
"fluent-format.formatSort",
|
|
18
|
+
async () => {
|
|
19
|
+
await formatDocument(true);
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Register document formatter provider
|
|
24
|
+
const formatterProvider =
|
|
25
|
+
vscode.languages.registerDocumentFormattingEditProvider(
|
|
26
|
+
{ scheme: "file", language: "fluent" },
|
|
27
|
+
{
|
|
28
|
+
provideDocumentFormattingEdits(
|
|
29
|
+
document: vscode.TextDocument,
|
|
30
|
+
): vscode.TextEdit[] {
|
|
31
|
+
const config = vscode.workspace.getConfiguration("fluentFormat");
|
|
32
|
+
const sortOnFormat = config.get<boolean>("sortOnFormat", false);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const content = document.getText();
|
|
36
|
+
const formatted = formatFluentContent(content, {
|
|
37
|
+
sort: sortOnFormat,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const fullRange = new vscode.Range(
|
|
41
|
+
document.positionAt(0),
|
|
42
|
+
document.positionAt(content.length),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return [vscode.TextEdit.replace(fullRange, formatted)];
|
|
46
|
+
} catch (error) {
|
|
47
|
+
vscode.window.showErrorMessage(
|
|
48
|
+
`Fluent Format Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
49
|
+
);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Format on save
|
|
57
|
+
const formatOnSave = vscode.workspace.onWillSaveTextDocument((event) => {
|
|
58
|
+
const config = vscode.workspace.getConfiguration("fluentFormat");
|
|
59
|
+
const formatOnSaveEnabled = config.get<boolean>("formatOnSave", false);
|
|
60
|
+
|
|
61
|
+
if (formatOnSaveEnabled && event.document.languageId === "fluent") {
|
|
62
|
+
const sortOnFormat = config.get<boolean>("sortOnFormat", false);
|
|
63
|
+
event.waitUntil(formatDocumentPromise(event.document, sortOnFormat));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
context.subscriptions.push(
|
|
68
|
+
formatCommand,
|
|
69
|
+
formatSortCommand,
|
|
70
|
+
formatterProvider,
|
|
71
|
+
formatOnSave,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function formatDocument(sort: boolean): Promise<void> {
|
|
76
|
+
const editor = vscode.window.activeTextEditor;
|
|
77
|
+
|
|
78
|
+
if (!editor) {
|
|
79
|
+
vscode.window.showErrorMessage("No active editor");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (editor.document.languageId !== "fluent") {
|
|
84
|
+
vscode.window.showErrorMessage("Current file is not a Fluent (.ftl) file");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const document = editor.document;
|
|
90
|
+
const content = document.getText();
|
|
91
|
+
const formatted = formatFluentContent(content, { sort });
|
|
92
|
+
|
|
93
|
+
const fullRange = new vscode.Range(
|
|
94
|
+
document.positionAt(0),
|
|
95
|
+
document.positionAt(content.length),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await editor.edit((editBuilder) => {
|
|
99
|
+
editBuilder.replace(fullRange, formatted);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
vscode.window.showInformationMessage(
|
|
103
|
+
sort ? "Fluent file formatted and sorted" : "Fluent file formatted",
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
vscode.window.showErrorMessage(
|
|
107
|
+
`Fluent Format Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function formatDocumentPromise(
|
|
113
|
+
document: vscode.TextDocument,
|
|
114
|
+
sort: boolean,
|
|
115
|
+
): Promise<vscode.TextEdit[]> {
|
|
116
|
+
try {
|
|
117
|
+
const content = document.getText();
|
|
118
|
+
const formatted = formatFluentContent(content, { sort });
|
|
119
|
+
|
|
120
|
+
const fullRange = new vscode.Range(
|
|
121
|
+
document.positionAt(0),
|
|
122
|
+
document.positionAt(content.length),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return [vscode.TextEdit.replace(fullRange, formatted)];
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error("Format error:", error);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function deactivate() {}
|
package/formatter.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parse,
|
|
3
|
+
Resource,
|
|
4
|
+
serialize,
|
|
5
|
+
Entry,
|
|
6
|
+
Message,
|
|
7
|
+
Term,
|
|
8
|
+
lineOffset,
|
|
9
|
+
} from "@fluent/syntax";
|
|
10
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
|
|
13
|
+
export interface FormatResult {
|
|
14
|
+
path: string;
|
|
15
|
+
content: string;
|
|
16
|
+
isFormatted: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FormatOptions {
|
|
20
|
+
sort?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getEntryId(entry: Entry): string | null {
|
|
24
|
+
if (entry.type === "Message") {
|
|
25
|
+
return (entry as Message).id.name;
|
|
26
|
+
}
|
|
27
|
+
if (entry.type === "Term") {
|
|
28
|
+
return (entry as Term).id.name;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function groupEntriesByBlankLines(
|
|
34
|
+
content: string,
|
|
35
|
+
entries: Entry[],
|
|
36
|
+
): Entry[][] {
|
|
37
|
+
if (entries.length === 0) return [];
|
|
38
|
+
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
const groups: Entry[][] = [];
|
|
41
|
+
let currentGroup: Entry[] = [];
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < entries.length; i++) {
|
|
44
|
+
const entry = entries[i];
|
|
45
|
+
const nextEntry = entries[i + 1];
|
|
46
|
+
|
|
47
|
+
currentGroup.push(entry);
|
|
48
|
+
|
|
49
|
+
// Check if there's a blank line between this entry and the next one
|
|
50
|
+
if (nextEntry && entry.span && nextEntry.span) {
|
|
51
|
+
const currentEndLine = lineOffset(content, entry.span.end);
|
|
52
|
+
const nextStartLine = lineOffset(content, nextEntry.span.start);
|
|
53
|
+
|
|
54
|
+
// Check if there's a blank line in between
|
|
55
|
+
let hasBlankLine = false;
|
|
56
|
+
for (
|
|
57
|
+
let lineNum = currentEndLine + 1;
|
|
58
|
+
lineNum < nextStartLine;
|
|
59
|
+
lineNum++
|
|
60
|
+
) {
|
|
61
|
+
if (lineNum < lines.length && lines[lineNum].trim() === "") {
|
|
62
|
+
hasBlankLine = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasBlankLine) {
|
|
68
|
+
groups.push(currentGroup);
|
|
69
|
+
currentGroup = [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (currentGroup.length > 0) {
|
|
75
|
+
groups.push(currentGroup);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return groups;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface EntryUnit {
|
|
82
|
+
leadingComments: Entry[];
|
|
83
|
+
entry: Entry | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createEntryUnits(entries: Entry[]): EntryUnit[] {
|
|
87
|
+
const units: EntryUnit[] = [];
|
|
88
|
+
let leadingComments: Entry[] = [];
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const entryId = getEntryId(entry);
|
|
92
|
+
|
|
93
|
+
if (!entryId) {
|
|
94
|
+
// This is a comment or other non-message/term entry
|
|
95
|
+
leadingComments.push(entry);
|
|
96
|
+
} else {
|
|
97
|
+
// This is a message or term
|
|
98
|
+
units.push({
|
|
99
|
+
leadingComments: [...leadingComments],
|
|
100
|
+
entry,
|
|
101
|
+
});
|
|
102
|
+
leadingComments = [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle any trailing comments
|
|
107
|
+
if (leadingComments.length > 0) {
|
|
108
|
+
units.push({
|
|
109
|
+
leadingComments,
|
|
110
|
+
entry: null,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return units;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sortEntriesInGroups(content: string, entries: Entry[]): Entry[] {
|
|
118
|
+
const groups = groupEntriesByBlankLines(content, entries);
|
|
119
|
+
const sortedEntries: Entry[] = [];
|
|
120
|
+
|
|
121
|
+
for (const group of groups) {
|
|
122
|
+
if (group.length === 0) continue;
|
|
123
|
+
|
|
124
|
+
// Find and extract group comment from any entry
|
|
125
|
+
let groupComment = null;
|
|
126
|
+
for (const entry of group) {
|
|
127
|
+
if (
|
|
128
|
+
(entry.type === "Message" || entry.type === "Term") &&
|
|
129
|
+
(entry as any).comment
|
|
130
|
+
) {
|
|
131
|
+
groupComment = (entry as any).comment;
|
|
132
|
+
(entry as any).comment = null; // Remove comment from original entry
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Sort all entries in the group by their ID
|
|
138
|
+
const sorted = [...group].sort((a, b) => {
|
|
139
|
+
const idA = getEntryId(a);
|
|
140
|
+
const idB = getEntryId(b);
|
|
141
|
+
|
|
142
|
+
if (!idA && !idB) return 0;
|
|
143
|
+
if (!idA) return 1;
|
|
144
|
+
if (!idB) return -1;
|
|
145
|
+
|
|
146
|
+
return idA.localeCompare(idB);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Attach group comment to the first entry
|
|
150
|
+
if (groupComment && sorted.length > 0) {
|
|
151
|
+
const firstEntry = sorted[0] as any;
|
|
152
|
+
if (firstEntry.type === "Message" || firstEntry.type === "Term") {
|
|
153
|
+
firstEntry.comment = groupComment;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
sortedEntries.push(...sorted);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return sortedEntries;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function formatFluentContent(
|
|
164
|
+
content: string,
|
|
165
|
+
options: FormatOptions = {},
|
|
166
|
+
): string {
|
|
167
|
+
try {
|
|
168
|
+
const resource: Resource = parse(content, { withSpans: true });
|
|
169
|
+
|
|
170
|
+
if (options.sort) {
|
|
171
|
+
resource.body = sortEntriesInGroups(content, resource.body);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return serialize(resource, { withJunk: true });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Failed to parse Fluent content: ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function formatFile(
|
|
183
|
+
filePath: string,
|
|
184
|
+
write: boolean = false,
|
|
185
|
+
options: FormatOptions = {},
|
|
186
|
+
): Promise<FormatResult> {
|
|
187
|
+
const content = readFileSync(filePath, "utf-8");
|
|
188
|
+
const formatted = formatFluentContent(content, options);
|
|
189
|
+
const isFormatted = content === formatted;
|
|
190
|
+
|
|
191
|
+
if (write && !isFormatted) {
|
|
192
|
+
writeFileSync(filePath, formatted, "utf-8");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
path: filePath,
|
|
197
|
+
content: formatted,
|
|
198
|
+
isFormatted,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function formatDirectory(
|
|
203
|
+
dirPath: string,
|
|
204
|
+
write: boolean = false,
|
|
205
|
+
options: FormatOptions = {},
|
|
206
|
+
): Promise<FormatResult[]> {
|
|
207
|
+
const results: FormatResult[] = [];
|
|
208
|
+
|
|
209
|
+
async function walk(dir: string) {
|
|
210
|
+
const entries = readdirSync(dir);
|
|
211
|
+
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const fullPath = join(dir, entry);
|
|
214
|
+
const stats = statSync(fullPath);
|
|
215
|
+
|
|
216
|
+
if (stats.isDirectory()) {
|
|
217
|
+
await walk(fullPath);
|
|
218
|
+
} else if (stats.isFile() && entry.endsWith(".ftl")) {
|
|
219
|
+
const result = await formatFile(fullPath, write, options);
|
|
220
|
+
results.push(result);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await walk(dirPath);
|
|
226
|
+
return results;
|
|
227
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Hello via Bun!");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comments": {
|
|
3
|
+
"lineComment": "#"
|
|
4
|
+
},
|
|
5
|
+
"brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
|
|
6
|
+
"autoClosingPairs": [
|
|
7
|
+
{ "open": "{", "close": "}" },
|
|
8
|
+
{ "open": "[", "close": "]" },
|
|
9
|
+
{ "open": "(", "close": ")" },
|
|
10
|
+
{ "open": "\"", "close": "\"" }
|
|
11
|
+
],
|
|
12
|
+
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
|
|
13
|
+
"folding": {
|
|
14
|
+
"markers": {
|
|
15
|
+
"start": "^\\s*#.*",
|
|
16
|
+
"end": "^\\s*$"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fluent-format",
|
|
3
|
+
"displayName": "Fluent Format",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Format and sort Fluent (.ftl) translation files",
|
|
6
|
+
"publisher": "fluent-format",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/yourusername/fluent-format"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"vscode": "^1.75.0"
|
|
14
|
+
},
|
|
15
|
+
"categories": [
|
|
16
|
+
"Formatters",
|
|
17
|
+
"Programming Languages"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"fluent",
|
|
21
|
+
"ftl",
|
|
22
|
+
"formatter",
|
|
23
|
+
"localization",
|
|
24
|
+
"l10n",
|
|
25
|
+
"i18n"
|
|
26
|
+
],
|
|
27
|
+
"main": "./out/extension.js",
|
|
28
|
+
"activationEvents": [
|
|
29
|
+
"onLanguage:fluent"
|
|
30
|
+
],
|
|
31
|
+
"contributes": {
|
|
32
|
+
"languages": [
|
|
33
|
+
{
|
|
34
|
+
"id": "fluent",
|
|
35
|
+
"extensions": [
|
|
36
|
+
".ftl"
|
|
37
|
+
],
|
|
38
|
+
"aliases": [
|
|
39
|
+
"Fluent",
|
|
40
|
+
"fluent"
|
|
41
|
+
],
|
|
42
|
+
"configuration": "./language-configuration.json"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"commands": [
|
|
46
|
+
{
|
|
47
|
+
"command": "fluent-format.format",
|
|
48
|
+
"title": "Format Fluent File"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"command": "fluent-format.formatSort",
|
|
52
|
+
"title": "Format and Sort Fluent File"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"configuration": {
|
|
56
|
+
"title": "Fluent Format",
|
|
57
|
+
"properties": {
|
|
58
|
+
"fluentFormat.sortOnFormat": {
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"default": false,
|
|
61
|
+
"description": "Sort messages alphabetically when formatting"
|
|
62
|
+
},
|
|
63
|
+
"fluentFormat.formatOnSave": {
|
|
64
|
+
"type": "boolean",
|
|
65
|
+
"default": false,
|
|
66
|
+
"description": "Automatically format Fluent files on save"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"module": "index.ts",
|
|
72
|
+
"type": "module",
|
|
73
|
+
"bin": {
|
|
74
|
+
"fluent-format": "./cli.ts"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"fluent-format": "bun run cli.ts",
|
|
78
|
+
"vscode:prepublish": "bun run compile",
|
|
79
|
+
"compile": "tsc -p tsconfig.extension.json",
|
|
80
|
+
"watch": "tsc -watch -p tsconfig.extension.json",
|
|
81
|
+
"package": "vsce package",
|
|
82
|
+
"format": "biome format --write",
|
|
83
|
+
"codex": "bunx @openai/codex@latest",
|
|
84
|
+
"claude": "bunx @anthropic-ai/claude-code@latest"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@biomejs/biome": "^2.3.15",
|
|
88
|
+
"@types/bun": "latest",
|
|
89
|
+
"@types/vscode": "^1.109.0",
|
|
90
|
+
"vscode-languageclient": "^9.0.1"
|
|
91
|
+
},
|
|
92
|
+
"peerDependencies": {
|
|
93
|
+
"typescript": "^5"
|
|
94
|
+
},
|
|
95
|
+
"dependencies": {
|
|
96
|
+
"@fluent/syntax": "^0.19.0",
|
|
97
|
+
"commander": "^14.0.3"
|
|
98
|
+
}
|
|
99
|
+
}
|
package/test-complex.ftl
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Authentication messages
|
|
2
|
+
logout = Log out
|
|
3
|
+
login = Log in
|
|
4
|
+
signup = Sign up
|
|
5
|
+
forgot-password = Forgot password?
|
|
6
|
+
|
|
7
|
+
# No comment group
|
|
8
|
+
zebra-feature = Zebra Feature
|
|
9
|
+
apple-feature = Apple Feature
|
|
10
|
+
banana-feature = Banana Feature
|
|
11
|
+
|
|
12
|
+
# Settings
|
|
13
|
+
theme-dark = Dark Theme
|
|
14
|
+
theme-light = Light Theme
|
|
15
|
+
language = Language
|
|
16
|
+
profile = Profile
|
|
17
|
+
|
|
18
|
+
single-message = This is alone
|
package/test-sort.ftl
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# First group
|
|
2
|
+
apple = Red fruit
|
|
3
|
+
banana = Yellow fruit
|
|
4
|
+
zebra = Zebra animal
|
|
5
|
+
# Second group
|
|
6
|
+
bird = Flying creature
|
|
7
|
+
cat = Feline pet
|
|
8
|
+
dog = Domestic animal
|
|
9
|
+
# Third group - already sorted
|
|
10
|
+
alpha = First letter
|
|
11
|
+
beta = Second letter
|
|
12
|
+
gamma = Third letter
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"outDir": "out",
|
|
6
|
+
"lib": ["ES2020"],
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["extension.ts", "formatter.ts"],
|
|
16
|
+
"exclude": ["node_modules", "out", "**/*.test.ts"]
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|