vue-i18n-lint 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +216 -0
- package/dist/cli.mjs +963 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.mjs +5 -0
- package/package.json +74 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ruben Gees
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# vue-i18n-lint
|
|
2
|
+
|
|
3
|
+
Fast and accurate linting for Vue i18n. Detects missing and unused translation keys across your locale files and source
|
|
4
|
+
code.
|
|
5
|
+
|
|
6
|
+
Intended for use in CI pipelines, pre-commit hooks, or as part of your development workflow to maintain translation
|
|
7
|
+
integrity.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx vue-i18n-lint [command] [options] [path]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`path` defaults to the current working directory.
|
|
16
|
+
|
|
17
|
+
<details>
|
|
18
|
+
<summary>pnpm</summary>
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pnpm dlx vue-i18n-lint [command] [options] [path]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
</details>
|
|
25
|
+
|
|
26
|
+
<details>
|
|
27
|
+
<summary>yarn</summary>
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
yarn dlx vue-i18n-lint [command] [options] [path]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
</details>
|
|
34
|
+
|
|
35
|
+
<details>
|
|
36
|
+
<summary>Bun</summary>
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bunx vue-i18n-lint [command] [options] [path]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
</details>
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
The cli can also be installed and run locally in your project:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm install -D vue-i18n-lint
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<details>
|
|
53
|
+
<summary>pnpm</summary>
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pnpm add -D vue-i18n-lint
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
</details>
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>yarn</summary>
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
yarn add -D vue-i18n-lint
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
</details>
|
|
69
|
+
|
|
70
|
+
<details>
|
|
71
|
+
<summary>Bun</summary>
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
bun add -D vue-i18n-lint
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
</details>
|
|
78
|
+
|
|
79
|
+
You can run it like this:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
vue-i18n-lint [command] [options] [path]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Subcommands
|
|
86
|
+
|
|
87
|
+
| Command | Description |
|
|
88
|
+
| ---------------- | ------------------------------------------------ |
|
|
89
|
+
| `lint` (default) | Detect missing and unused translation keys |
|
|
90
|
+
| `remove-unused` | Remove unused translation keys from locale files |
|
|
91
|
+
| `init` | Scaffold a configuration file |
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
| Option | Description | Default |
|
|
96
|
+
| ------------------------- | ------------------------------------------------------------- | ---------------------------------- |
|
|
97
|
+
| `--format` | Output format: `text`, `json`, or `toon` | `text` |
|
|
98
|
+
| `--locale-pattern` | Glob pattern for i18n locale files | `**/locales/*.json` |
|
|
99
|
+
| `--src-pattern` | Glob pattern for source files | `**/*.{ts,cts,mts,js,cjs,mjs,vue}` |
|
|
100
|
+
| `--ignore-patterns` | Comma-separated glob patterns to ignore | |
|
|
101
|
+
| `--ignore-keys` | Comma-separated keys to ignore in all checks | |
|
|
102
|
+
| `--ignore-missing-keys` | Comma-separated keys to ignore only in the missing keys check | |
|
|
103
|
+
| `--ignore-unused-keys` | Comma-separated keys to ignore only in the unused keys check | |
|
|
104
|
+
| `--missing-keys-severity` | Severity for missing keys: `error`, `warning`, or `off` | `error` |
|
|
105
|
+
| `--unused-keys-severity` | Severity for unused keys: `error`, `warning`, or `off` | `warning` |
|
|
106
|
+
|
|
107
|
+
### Example
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npx vue-i18n-lint --locale-pattern "**/i18n/*.{json}" ./my-project
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Configuration file
|
|
114
|
+
|
|
115
|
+
In addition to CLI options, vue-i18n-lint can be configured via a config file in your project root.
|
|
116
|
+
[c12](https://github.com/unjs/c12) is used for config loading, so the following file names are supported:
|
|
117
|
+
|
|
118
|
+
- `vue-i18n-lint.config.ts`
|
|
119
|
+
- `vue-i18n-lint.config.js`
|
|
120
|
+
- `vue-i18n-lint.config.json`
|
|
121
|
+
- `vue-i18n-lint.config.yaml`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// vue-i18n-lint.config.ts
|
|
125
|
+
import { defineConfig } from "vue-i18n-lint"
|
|
126
|
+
|
|
127
|
+
export default defineConfig({
|
|
128
|
+
localePattern: "**/i18n/*.json",
|
|
129
|
+
srcPattern: "**/*.{ts,vue}",
|
|
130
|
+
ignorePatterns: ["**/fixtures/**"],
|
|
131
|
+
ignoreKeys: ["dynamic.key"],
|
|
132
|
+
checks: {
|
|
133
|
+
missingKeys: {
|
|
134
|
+
severity: "error",
|
|
135
|
+
ignore: ["only.missing"],
|
|
136
|
+
},
|
|
137
|
+
unusedKeys: {
|
|
138
|
+
severity: "warning",
|
|
139
|
+
ignore: ["only.unused"],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Use the exported `defineConfig` helper for TypeScript autocompletion.
|
|
146
|
+
|
|
147
|
+
Dynamic missing keys are reported with `<dynamic>` as a placeholder (e.g. `status.<dynamic>`).
|
|
148
|
+
Use that string in `ignoreKeys` or `checks.missingKeys.ignore` to suppress them.
|
|
149
|
+
|
|
150
|
+
### gitignore
|
|
151
|
+
|
|
152
|
+
Files matched by any `.gitignore` in your project are automatically excluded when scanning locale and source files,
|
|
153
|
+
in addition to any `ignorePatterns` you configure.
|
|
154
|
+
|
|
155
|
+
## What it checks
|
|
156
|
+
|
|
157
|
+
- **Missing keys**: Translation keys used in source code but not defined in any locale file
|
|
158
|
+
- **Unused keys**: Translation keys defined in locale files but never referenced in source code
|
|
159
|
+
|
|
160
|
+
## Dynamic keys
|
|
161
|
+
|
|
162
|
+
Keys built from template literals or string concatenation with runtime variables are understood:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
t(`status.${type}`) // matched against locale keys as "status.*"
|
|
166
|
+
t("prefix." + key) // matched against locale keys as "prefix.*"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A dynamic key is not reported missing if at least one locale key matches its pattern. Locale keys
|
|
170
|
+
that match a dynamic pattern are not reported as unused.
|
|
171
|
+
|
|
172
|
+
If no locale key matches, the missing key is reported with `<dynamic>` as a placeholder,
|
|
173
|
+
e.g. `status.<dynamic>`.
|
|
174
|
+
|
|
175
|
+
Purely dynamic expressions with no static fragments (e.g. `t(variable)`) are ignored.
|
|
176
|
+
|
|
177
|
+
Dynamic key support is best-effort. There are many more complex cases that can't be detected by vue-i18n-lint (yet).
|
|
178
|
+
|
|
179
|
+
> [!TIP]
|
|
180
|
+
> Ignoring dynamic keys is done using the `<dynamic>` placeholder, e.g. `ignoreKeys: ["status.<dynamic>"]`
|
|
181
|
+
|
|
182
|
+
## Supported locale file formats
|
|
183
|
+
|
|
184
|
+
- JSON, JSONC, JSON5
|
|
185
|
+
- YAML
|
|
186
|
+
- JS/TS modules (expecting a default export of an object)
|
|
187
|
+
- `<i18n>` blocks in Vue SFCs
|
|
188
|
+
|
|
189
|
+
## Severity levels
|
|
190
|
+
|
|
191
|
+
Each check (`missingKeys`, `unusedKeys`) supports a `severity` setting:
|
|
192
|
+
|
|
193
|
+
| Value | Behavior |
|
|
194
|
+
| ----------- | ----------------------------------------------------------- |
|
|
195
|
+
| `"error"` | Prints output and sets exit code to `1` if issues are found |
|
|
196
|
+
| `"warning"` | Prints output but does not set exit code to `1` |
|
|
197
|
+
| `"off"` | Does not print output and does not affect exit code |
|
|
198
|
+
|
|
199
|
+
## Removing unused keys
|
|
200
|
+
|
|
201
|
+
The `remove-unused` subcommand removes unused translation keys directly from your locale files. Only the simple
|
|
202
|
+
file types (JSON, JSONC, JSON5, YAML) are supported (JS/TS files and `<i18n>` blocks are ignored).
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
npx vue-i18n-lint remove-unused [options] [path]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Pass `--dry-run` to see how many keys would be removed without making changes:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
npx vue-i18n-lint remove-unused --dry-run
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Requirements
|
|
215
|
+
|
|
216
|
+
- Node.js >= 22
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildApplication, buildCommand, buildRouteMap, run, text_en } from "@stricli/core";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
6
|
+
import { createDefu, defu } from "defu";
|
|
7
|
+
import { loadConfig } from "c12";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { styleText } from "node:util";
|
|
10
|
+
import { codeFrameColumns } from "@babel/code-frame";
|
|
11
|
+
import { encode } from "@toon-format/toon";
|
|
12
|
+
import { table } from "table";
|
|
13
|
+
import escape from "regexp.escape";
|
|
14
|
+
import { globby } from "globby";
|
|
15
|
+
import { parseJSON, parseJSON5, parseJSONC, parseYAML, stringifyJSON, stringifyJSON5, stringifyJSONC, stringifyYAML } from "confbox";
|
|
16
|
+
import { createJiti } from "jiti";
|
|
17
|
+
import { parse } from "@vue/compiler-sfc";
|
|
18
|
+
import { Visitor, parseSync } from "oxc-parser";
|
|
19
|
+
import { NodeTypes } from "@vue/compiler-core";
|
|
20
|
+
//#region src/utils.ts
|
|
21
|
+
const merge = createDefu((obj, key, value) => {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
obj[key] = value;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
});
|
|
28
|
+
function newPrefixSet(keys = []) {
|
|
29
|
+
const set = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const key of keys) {
|
|
31
|
+
const parts = key.split(".");
|
|
32
|
+
for (let i = 1; i <= parts.length; i++) set.add(parts.slice(0, i).join("."));
|
|
33
|
+
}
|
|
34
|
+
return set;
|
|
35
|
+
}
|
|
36
|
+
function mapGetOrInsert(map, key, defaultValue) {
|
|
37
|
+
if (!map.has(key)) map.set(key, defaultValue);
|
|
38
|
+
return map.get(key);
|
|
39
|
+
}
|
|
40
|
+
function getOrInsertComputed(map, key, callback) {
|
|
41
|
+
if (!map.has(key)) map.set(key, callback(key));
|
|
42
|
+
return map.get(key);
|
|
43
|
+
}
|
|
44
|
+
function offsetToPosition(source, offset) {
|
|
45
|
+
const lines = source.slice(0, offset).split("\n");
|
|
46
|
+
return {
|
|
47
|
+
line: lines.length,
|
|
48
|
+
column: (lines[lines.length - 1]?.length ?? 0) + 1
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function writeLine(writer, line = "") {
|
|
52
|
+
writer.write(`${line}\n`);
|
|
53
|
+
}
|
|
54
|
+
function formatFilePath(filePath, line, column) {
|
|
55
|
+
const base = `file://${resolve(filePath)}`;
|
|
56
|
+
if (line != null && column != null) return `${base}:${line}:${column}`;
|
|
57
|
+
if (line != null) return `${base}:${line}`;
|
|
58
|
+
return base;
|
|
59
|
+
}
|
|
60
|
+
function isPlainObject(value) {
|
|
61
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/command/init.ts
|
|
65
|
+
const configTemplate = `import { defineConfig } from "vue-i18n-lint"
|
|
66
|
+
|
|
67
|
+
export default defineConfig({
|
|
68
|
+
localePattern: "**/locales/*.json",
|
|
69
|
+
srcPattern: "**/*.{ts,cts,mts,js,cjs,mjs,vue}",
|
|
70
|
+
})
|
|
71
|
+
`;
|
|
72
|
+
const initCommand = buildCommand({
|
|
73
|
+
async func(_flags, path) {
|
|
74
|
+
const targetPath = path || ".";
|
|
75
|
+
const configPath = join(targetPath, "vue-i18n-lint.config.ts");
|
|
76
|
+
if (existsSync(configPath)) {
|
|
77
|
+
writeLine(this.process.stderr, `vue-i18n-lint.config.ts already exists in ${formatFilePath(targetPath)}.`);
|
|
78
|
+
this.process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await mkdir(targetPath, { recursive: true });
|
|
82
|
+
await writeFile(configPath, configTemplate, "utf-8");
|
|
83
|
+
writeLine(this.process.stdout, `Created vue-i18n-lint.config.ts in ${formatFilePath(targetPath)}.`);
|
|
84
|
+
},
|
|
85
|
+
parameters: { positional: {
|
|
86
|
+
kind: "tuple",
|
|
87
|
+
parameters: [{
|
|
88
|
+
brief: "Target path for the config file",
|
|
89
|
+
parse: String,
|
|
90
|
+
placeholder: "path",
|
|
91
|
+
optional: true
|
|
92
|
+
}]
|
|
93
|
+
} },
|
|
94
|
+
docs: { brief: "Create a default vue-i18n-lint configuration file" }
|
|
95
|
+
});
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/config/schema.ts
|
|
98
|
+
const severityEnum = z.enum([
|
|
99
|
+
"error",
|
|
100
|
+
"warning",
|
|
101
|
+
"off"
|
|
102
|
+
]);
|
|
103
|
+
const formatEnum = z.enum([
|
|
104
|
+
"text",
|
|
105
|
+
"json",
|
|
106
|
+
"toon"
|
|
107
|
+
]);
|
|
108
|
+
const checkSchema = (defaultSeverity) => z.union([z.object({
|
|
109
|
+
severity: severityEnum.default(defaultSeverity),
|
|
110
|
+
ignore: z.array(z.string().nonempty()).default([])
|
|
111
|
+
}), z.literal(false).transform(() => ({
|
|
112
|
+
severity: "off",
|
|
113
|
+
ignore: []
|
|
114
|
+
}))]).default({
|
|
115
|
+
severity: defaultSeverity,
|
|
116
|
+
ignore: []
|
|
117
|
+
});
|
|
118
|
+
const configSchema = z.object({
|
|
119
|
+
path: z.string().nonempty(),
|
|
120
|
+
format: formatEnum.default("text"),
|
|
121
|
+
localePattern: z.string().nonempty().default("**/locales/*.json"),
|
|
122
|
+
srcPattern: z.string().nonempty().default("**/*.{ts,cts,mts,js,cjs,mjs,vue}"),
|
|
123
|
+
ignorePatterns: z.array(z.string().nonempty()).default([]),
|
|
124
|
+
ignoreKeys: z.array(z.string().nonempty()).default([]),
|
|
125
|
+
checks: z.object({
|
|
126
|
+
missingKeys: checkSchema("error"),
|
|
127
|
+
unusedKeys: checkSchema("warning")
|
|
128
|
+
}).default({
|
|
129
|
+
missingKeys: {
|
|
130
|
+
severity: "error",
|
|
131
|
+
ignore: []
|
|
132
|
+
},
|
|
133
|
+
unusedKeys: {
|
|
134
|
+
severity: "warning",
|
|
135
|
+
ignore: []
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
});
|
|
139
|
+
z.object({
|
|
140
|
+
path: z.string().optional(),
|
|
141
|
+
format: formatEnum.optional(),
|
|
142
|
+
localePattern: z.string().optional(),
|
|
143
|
+
srcPattern: z.string().optional(),
|
|
144
|
+
ignorePatterns: z.array(z.string()).optional(),
|
|
145
|
+
ignoreKeys: z.array(z.string()).optional(),
|
|
146
|
+
ignoreMissingKeys: z.array(z.string()).optional(),
|
|
147
|
+
ignoreUnusedKeys: z.array(z.string()).optional(),
|
|
148
|
+
missingKeysSeverity: severityEnum.optional(),
|
|
149
|
+
unusedKeysSeverity: severityEnum.optional()
|
|
150
|
+
});
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/config/load.ts
|
|
153
|
+
async function loadVueI18nLintConfig(cliArgs) {
|
|
154
|
+
const path = cliArgs?.path || process.cwd();
|
|
155
|
+
const cliConfig = buildCliConfig(path, cliArgs);
|
|
156
|
+
const rawFileConfig = await loadFileConfig(path) ?? {};
|
|
157
|
+
const parsedFileConfig = configSchema.safeParse({
|
|
158
|
+
path,
|
|
159
|
+
...rawFileConfig
|
|
160
|
+
});
|
|
161
|
+
if (!parsedFileConfig.success) throw new Error(`Failed to load config:\n${z.prettifyError(parsedFileConfig.error)}`);
|
|
162
|
+
const mergedConfig = merge(cliConfig, parsedFileConfig.data);
|
|
163
|
+
const parsedMergedConfig = configSchema.safeParse(mergedConfig);
|
|
164
|
+
if (!parsedMergedConfig.success) throw new Error(`Failed to load config:\n${z.prettifyError(parsedMergedConfig.error)}`);
|
|
165
|
+
return parsedMergedConfig.data;
|
|
166
|
+
}
|
|
167
|
+
function buildCliConfig(path, cliArgs) {
|
|
168
|
+
return {
|
|
169
|
+
path,
|
|
170
|
+
format: cliArgs?.format,
|
|
171
|
+
localePattern: cliArgs?.localePattern,
|
|
172
|
+
srcPattern: cliArgs?.srcPattern,
|
|
173
|
+
ignorePatterns: cliArgs?.ignorePatterns,
|
|
174
|
+
ignoreKeys: cliArgs?.ignoreKeys,
|
|
175
|
+
checks: {
|
|
176
|
+
missingKeys: {
|
|
177
|
+
ignore: cliArgs?.ignoreMissingKeys,
|
|
178
|
+
severity: cliArgs?.missingKeysSeverity
|
|
179
|
+
},
|
|
180
|
+
unusedKeys: {
|
|
181
|
+
ignore: cliArgs?.ignoreUnusedKeys,
|
|
182
|
+
severity: cliArgs?.unusedKeysSeverity
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function loadFileConfig(path) {
|
|
188
|
+
try {
|
|
189
|
+
return (await loadConfig({
|
|
190
|
+
name: "vue-i18n-lint",
|
|
191
|
+
cwd: path
|
|
192
|
+
})).config;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
throw new Error(`Failed to load config file`, { cause: e });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/formatter.ts
|
|
199
|
+
function formatSummaryPart(value, severity) {
|
|
200
|
+
return styleText(value === 0 ? "green" : severity === "warning" ? "yellow" : "red", String(value));
|
|
201
|
+
}
|
|
202
|
+
function outputTypeWarnings(process, warnings) {
|
|
203
|
+
writeLine(process.stdout, styleText("bold", `Warnings (${warnings.length}):\n`));
|
|
204
|
+
const grouped = Object.groupBy(warnings, (it) => it.file);
|
|
205
|
+
for (const [file, fileTypeWarnings] of Object.entries(grouped)) {
|
|
206
|
+
writeLine(process.stdout, formatFilePath(file));
|
|
207
|
+
for (const typeWarning of fileTypeWarnings ?? []) writeLine(process.stdout, ` • Unexpected type ${styleText("italic", typeWarning.type)} for key ${typeWarning.key}`);
|
|
208
|
+
writeLine(process.stdout);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function outputMissingKeys(process, keys) {
|
|
212
|
+
writeLine(process.stdout, styleText("bold", `Missing keys (${keys.length}):\n`));
|
|
213
|
+
for (const key of keys) outputMissingKey(process, key);
|
|
214
|
+
}
|
|
215
|
+
function outputMissingKey(process, key) {
|
|
216
|
+
for (const source of key.sources) {
|
|
217
|
+
writeLine(process.stdout, ` ${formatFilePath(source.file)}:${source.location.start.line}:${source.location.start.column}`);
|
|
218
|
+
const content = readSourceFile(source.file);
|
|
219
|
+
if (content != null) writeLine(process.stdout, codeFrameColumns(content, source.location, {
|
|
220
|
+
highlightCode: true,
|
|
221
|
+
linesAbove: 1,
|
|
222
|
+
linesBelow: 1,
|
|
223
|
+
message: `Missing in ${styleText("bold", key.locales.join(", "))}`
|
|
224
|
+
}));
|
|
225
|
+
writeLine(process.stdout);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function readSourceFile(file) {
|
|
229
|
+
try {
|
|
230
|
+
return readFileSync(file, { encoding: "utf-8" });
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function outputUnusedKeys(process, keys) {
|
|
236
|
+
writeLine(process.stdout, styleText("bold", `Unused keys (${keys.length}):\n`));
|
|
237
|
+
const rows = keys.toSorted((a, b) => b.files.length - a.files.length || a.key.localeCompare(b.key)).map((unusedKey) => [unusedKey.key, unusedKey.files.map((file) => file.locale).join(", ")]);
|
|
238
|
+
const config = { drawHorizontalLine(index, size) {
|
|
239
|
+
return index === 0 || index === 1 || index === size;
|
|
240
|
+
} };
|
|
241
|
+
writeLine(process.stdout, table([["Key", "Locales"], ...rows], config));
|
|
242
|
+
}
|
|
243
|
+
function outputJson(process, data) {
|
|
244
|
+
writeLine(process.stdout, JSON.stringify(data, null, 2));
|
|
245
|
+
}
|
|
246
|
+
function outputToon(process, data) {
|
|
247
|
+
writeLine(process.stdout, encode(data));
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/processor.ts
|
|
251
|
+
function processFiles(localeFiles, sourceFiles, config) {
|
|
252
|
+
const result = { typeWarnings: calcTypeWarnings(localeFiles) };
|
|
253
|
+
if (config.checks.missingKeys.severity !== "off") {
|
|
254
|
+
const ignoreSet = /* @__PURE__ */ new Set([...config.ignoreKeys, ...config.checks.missingKeys.ignore ?? []]);
|
|
255
|
+
result.missing = calcMissingKeys(localeFiles, sourceFiles).filter((entry) => !ignoreSet.has(entry.key));
|
|
256
|
+
}
|
|
257
|
+
if (config.checks.unusedKeys.severity !== "off") {
|
|
258
|
+
const ignoreSet = /* @__PURE__ */ new Set([...config.ignoreKeys, ...config.checks.unusedKeys.ignore ?? []]);
|
|
259
|
+
result.unused = calcUnusedKeys(localeFiles, sourceFiles).filter((entry) => !ignoreSet.has(entry.key));
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
function calcTypeWarnings(localeFiles) {
|
|
264
|
+
return localeFiles.flatMap((localeFile) => localeFile.keys.filter((key) => key.type !== "string" && key.type !== "function").map((key) => ({
|
|
265
|
+
key: key.key,
|
|
266
|
+
locale: localeFile.locale,
|
|
267
|
+
file: localeFile.file,
|
|
268
|
+
type: key.type
|
|
269
|
+
})));
|
|
270
|
+
}
|
|
271
|
+
function calcMissingKeys(localeFiles, sourceFiles) {
|
|
272
|
+
const globalLocalePrefixes = new Map(localeFiles.map((it) => [it.locale, newPrefixSet(it.keys.map((k) => k.key))]));
|
|
273
|
+
const locales = /* @__PURE__ */ new Set([...globalLocalePrefixes.keys(), ...sourceFiles.flatMap((sf) => sf.localeFiles.map((lf) => lf.locale))]);
|
|
274
|
+
const missingKeys = /* @__PURE__ */ new Map();
|
|
275
|
+
const regexCache = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const sourceFile of sourceFiles) {
|
|
277
|
+
const localLocalePrefixes = new Map(sourceFile.localeFiles.map((it) => [it.locale, newPrefixSet(it.keys.map((k) => k.key))]));
|
|
278
|
+
for (const { key, file, location } of sourceFile.keys) if (typeof key !== "string") {
|
|
279
|
+
const keyStr = dynamicKeyToString(key);
|
|
280
|
+
const regex = getOrInsertComputed(regexCache, keyStr, () => buildDynamicKeyRegex(key));
|
|
281
|
+
const missingLocales = Array.from(locales).filter((locale) => !setMatchesRegex(localLocalePrefixes.get(locale), regex) && !setMatchesRegex(globalLocalePrefixes.get(locale), regex));
|
|
282
|
+
if (missingLocales.length > 0) mapGetOrInsert(missingKeys, keyStr, {
|
|
283
|
+
key: keyStr,
|
|
284
|
+
locales: missingLocales,
|
|
285
|
+
sources: []
|
|
286
|
+
}).sources.push({
|
|
287
|
+
file,
|
|
288
|
+
location
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
const missingLocales = Array.from(locales).filter((locale) => !localLocalePrefixes.get(locale)?.has(key) && !globalLocalePrefixes.get(locale)?.has(key));
|
|
292
|
+
if (missingLocales.length > 0) mapGetOrInsert(missingKeys, key, {
|
|
293
|
+
key,
|
|
294
|
+
locales: missingLocales,
|
|
295
|
+
sources: []
|
|
296
|
+
}).sources.push({
|
|
297
|
+
file,
|
|
298
|
+
location
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return Array.from(missingKeys.values());
|
|
303
|
+
}
|
|
304
|
+
function dynamicKeyToString(dynamicKey) {
|
|
305
|
+
return dynamicKey.map((part) => typeof part === "string" ? part : "<dynamic>").join("");
|
|
306
|
+
}
|
|
307
|
+
function buildDynamicKeyRegex(dynamicKey) {
|
|
308
|
+
return new RegExp("^" + dynamicKey.map((part) => typeof part === "string" ? escape(part) : ".*").join("") + "$");
|
|
309
|
+
}
|
|
310
|
+
function setMatchesRegex(set, regex) {
|
|
311
|
+
if (!set) return false;
|
|
312
|
+
return set.values().some((item) => regex.test(item));
|
|
313
|
+
}
|
|
314
|
+
function calcUnusedKeys(localeFiles, sourceFiles) {
|
|
315
|
+
const unusedKeys = /* @__PURE__ */ new Map();
|
|
316
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
317
|
+
const sourceDynamicKeys = /* @__PURE__ */ new Map();
|
|
318
|
+
for (const sourceFile of sourceFiles) {
|
|
319
|
+
const sourceFileKeys = /* @__PURE__ */ new Set();
|
|
320
|
+
const sourceFileDynamicKeys = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const k of sourceFile.keys) if (typeof k.key === "string") sourceFileKeys.add(k.key);
|
|
322
|
+
else {
|
|
323
|
+
const keyStr = dynamicKeyToString(k.key);
|
|
324
|
+
if (!sourceFileDynamicKeys.has(keyStr)) sourceFileDynamicKeys.set(keyStr, buildDynamicKeyRegex(k.key));
|
|
325
|
+
}
|
|
326
|
+
const sourceDynamicRegex = combineRegexes(Array.from(sourceFileDynamicKeys.values()));
|
|
327
|
+
calcUnusedKeysInLocaleFiles(unusedKeys, sourceFile.localeFiles, sourceFileKeys, sourceDynamicRegex);
|
|
328
|
+
for (const key of sourceFileKeys) sourceKeys.add(key);
|
|
329
|
+
for (const [keyStr, regex] of sourceFileDynamicKeys) if (!sourceDynamicKeys.has(keyStr)) sourceDynamicKeys.set(keyStr, regex);
|
|
330
|
+
}
|
|
331
|
+
calcUnusedKeysInLocaleFiles(unusedKeys, localeFiles, sourceKeys, combineRegexes(Array.from(sourceDynamicKeys.values())));
|
|
332
|
+
return Array.from(unusedKeys.values());
|
|
333
|
+
}
|
|
334
|
+
function combineRegexes(regexes) {
|
|
335
|
+
if (regexes.length === 0) return null;
|
|
336
|
+
return new RegExp(regexes.map((r) => `(?:${r.source})`).join("|"));
|
|
337
|
+
}
|
|
338
|
+
function calcUnusedKeysInLocaleFiles(unusedKeys, localeFiles, sourceKeys, sourceDynamicRegex) {
|
|
339
|
+
for (const localeFile of localeFiles) for (const { key } of localeFile.keys) {
|
|
340
|
+
const parts = key.split(".");
|
|
341
|
+
if (!parts.some((_, i) => {
|
|
342
|
+
const joined = parts.slice(0, i + 1).join(".");
|
|
343
|
+
return sourceKeys.has(joined) || sourceDynamicRegex?.test(joined);
|
|
344
|
+
})) mapGetOrInsert(unusedKeys, key, {
|
|
345
|
+
key,
|
|
346
|
+
files: []
|
|
347
|
+
}).files.push({
|
|
348
|
+
locale: localeFile.locale,
|
|
349
|
+
file: localeFile.file,
|
|
350
|
+
scope: localeFile.scope
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/error.ts
|
|
356
|
+
var ParseError = class extends Error {
|
|
357
|
+
file;
|
|
358
|
+
options;
|
|
359
|
+
constructor(message, file, options) {
|
|
360
|
+
super(message, { cause: options?.cause });
|
|
361
|
+
this.file = file;
|
|
362
|
+
this.options = options;
|
|
363
|
+
this.name = "ParseError";
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function formatErrorMessage(e) {
|
|
367
|
+
if (!(e instanceof Error)) return String(e);
|
|
368
|
+
if (e instanceof ParseError) {
|
|
369
|
+
const location = formatFilePath(e.file, e.options?.line, e.options?.column);
|
|
370
|
+
const detail = e.cause != null ? `: ${formatErrorMessage(e.cause)}` : "";
|
|
371
|
+
return `${location}: ${e.message}${detail}`;
|
|
372
|
+
}
|
|
373
|
+
if (e.cause != null) return `${e.message}: ${formatErrorMessage(e.cause)}`;
|
|
374
|
+
else return e.message;
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/parser/localeParser.ts
|
|
378
|
+
const jiti = createJiti(import.meta.url);
|
|
379
|
+
const supportedJsExtensions = [
|
|
380
|
+
".js",
|
|
381
|
+
".mjs",
|
|
382
|
+
".cjs",
|
|
383
|
+
".ts",
|
|
384
|
+
".mts",
|
|
385
|
+
".cts"
|
|
386
|
+
];
|
|
387
|
+
const fileParsers = {
|
|
388
|
+
".json": parseJSON,
|
|
389
|
+
".jsonc": parseJSONC,
|
|
390
|
+
".json5": parseJSON5,
|
|
391
|
+
".yaml": parseYAML,
|
|
392
|
+
".yml": parseYAML
|
|
393
|
+
};
|
|
394
|
+
async function parseLocaleFile(filePath) {
|
|
395
|
+
if (supportedJsExtensions.includes(extname(filePath))) return await readLocaleScriptFile(filePath);
|
|
396
|
+
return parseLocale(await readFile(filePath, { encoding: "utf-8" }), filePath);
|
|
397
|
+
}
|
|
398
|
+
async function readLocaleScriptFile(filePath) {
|
|
399
|
+
let result;
|
|
400
|
+
try {
|
|
401
|
+
result = await jiti.import(resolve(filePath), { default: true });
|
|
402
|
+
} catch (e) {
|
|
403
|
+
throw new ParseError("Failed to import locale module", filePath, { cause: e });
|
|
404
|
+
}
|
|
405
|
+
if (!isPlainObject(result)) throw new ParseError("Locale module default export is not an object", filePath);
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
function parseLocale(content, file, options = {}) {
|
|
409
|
+
const ext = options.ext ?? extname(file);
|
|
410
|
+
const parse = fileParsers[ext];
|
|
411
|
+
if (!parse) throw new ParseError(`Unsupported locale type: ${ext}`, file);
|
|
412
|
+
let result;
|
|
413
|
+
try {
|
|
414
|
+
result = parse(content);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
throw new ParseError("Failed to parse locale content", file, { cause: e });
|
|
417
|
+
}
|
|
418
|
+
if (!isPlainObject(result)) throw new ParseError("Locale content is not an object", file);
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/collector/localeExtractor.ts
|
|
423
|
+
function extractLocaleKeys(data, prefix = "") {
|
|
424
|
+
return Object.entries(data).flatMap(([key, value]) => {
|
|
425
|
+
if (typeof value === "object") {
|
|
426
|
+
if (value === null) return {
|
|
427
|
+
key: `${prefix}${key}`,
|
|
428
|
+
type: "null"
|
|
429
|
+
};
|
|
430
|
+
return extractLocaleKeys(value, `${prefix}${key}.`);
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
key: `${prefix}${key}`,
|
|
434
|
+
type: typeof value
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/collector/localeCollector.ts
|
|
440
|
+
async function collectLocaleFile(filePath) {
|
|
441
|
+
const locale = basename(filePath, extname(filePath));
|
|
442
|
+
const data = await parseLocaleFile(filePath);
|
|
443
|
+
return {
|
|
444
|
+
locale,
|
|
445
|
+
file: filePath,
|
|
446
|
+
rawData: data,
|
|
447
|
+
keys: extractLocaleKeys(data),
|
|
448
|
+
scope: "global"
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/parser/scriptParser.ts
|
|
453
|
+
function parseScript(file, content, options = {}) {
|
|
454
|
+
const source = options.wrapInParens ? `(${content})` : content;
|
|
455
|
+
const parseOptions = {};
|
|
456
|
+
const lang = asLang(options.lang);
|
|
457
|
+
if (lang) parseOptions.lang = lang;
|
|
458
|
+
const { program, errors } = parseSync(file, source, parseOptions);
|
|
459
|
+
if (errors.length > 0) {
|
|
460
|
+
const messages = errors.map((e) => ` • ${e.message}`).join("\n");
|
|
461
|
+
const rawOffset = errors[0].labels?.[0]?.start ?? 0;
|
|
462
|
+
const { line, column } = offsetToPosition(options.fileSource ?? content, rawOffset + (options.offset ?? 0));
|
|
463
|
+
throw new ParseError(`Failed to parse script:\n${messages}`, file, {
|
|
464
|
+
line,
|
|
465
|
+
column
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return program;
|
|
469
|
+
}
|
|
470
|
+
function asLang(lang) {
|
|
471
|
+
if (lang === "js" || lang === "jsx" || lang === "ts" || lang === "tsx" || lang === "dts") return lang;
|
|
472
|
+
}
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/types.ts
|
|
475
|
+
const DYNAMIC_PART = Symbol("dynamic");
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/collector/translationFunctions.ts
|
|
478
|
+
const TRANSLATION_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
479
|
+
"t",
|
|
480
|
+
"te",
|
|
481
|
+
"tm",
|
|
482
|
+
"tc",
|
|
483
|
+
"$t",
|
|
484
|
+
"$te",
|
|
485
|
+
"$tm",
|
|
486
|
+
"$tc"
|
|
487
|
+
]);
|
|
488
|
+
const TRANSLATION_CALL_REGEX = /\$?t[emc]?\s*\(/;
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/collector/jsCollector.ts
|
|
491
|
+
function collectJsKeys(program, offset = 0) {
|
|
492
|
+
const result = [];
|
|
493
|
+
new Visitor({ CallExpression(node) {
|
|
494
|
+
if (!isTranslationFunction(node)) return;
|
|
495
|
+
const arg = node.arguments[0];
|
|
496
|
+
if (!arg) return;
|
|
497
|
+
const keys = extractKey(arg, offset);
|
|
498
|
+
result.push(...keys);
|
|
499
|
+
} }).visit(program);
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
function isTranslationFunction(node) {
|
|
503
|
+
if (node.callee.type === "Identifier") return TRANSLATION_FUNCTIONS.has(node.callee.name);
|
|
504
|
+
if (node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier") return TRANSLATION_FUNCTIONS.has(node.callee.property.name);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
function extractKey(arg, offset) {
|
|
508
|
+
if (arg.type === "ConditionalExpression") return [...extractKey(arg.consequent, offset), ...extractKey(arg.alternate, offset)];
|
|
509
|
+
const normalizedParts = collect(arg).reduce((acc, curr) => {
|
|
510
|
+
const last = acc[acc.length - 1];
|
|
511
|
+
if (typeof last === "string" && typeof curr === "string") acc[acc.length - 1] = last + curr;
|
|
512
|
+
else if (last !== curr) acc.push(curr);
|
|
513
|
+
return acc;
|
|
514
|
+
}, []);
|
|
515
|
+
if (normalizedParts.length === 0) return [];
|
|
516
|
+
if (normalizedParts.every((p) => typeof p !== "string")) return [];
|
|
517
|
+
const start = offset + arg.start;
|
|
518
|
+
const end = offset + arg.end;
|
|
519
|
+
if (normalizedParts.length === 1 && typeof normalizedParts[0] === "string") {
|
|
520
|
+
const isQuoted = arg.type === "Literal" && typeof arg.value === "string" || arg.type === "TemplateLiteral";
|
|
521
|
+
return [{
|
|
522
|
+
key: normalizedParts[0],
|
|
523
|
+
start: isQuoted ? start + 1 : start,
|
|
524
|
+
end: isQuoted ? end - 1 : end
|
|
525
|
+
}];
|
|
526
|
+
}
|
|
527
|
+
return [{
|
|
528
|
+
key: normalizedParts,
|
|
529
|
+
start,
|
|
530
|
+
end
|
|
531
|
+
}];
|
|
532
|
+
}
|
|
533
|
+
function collect(arg) {
|
|
534
|
+
if (arg.type === "Literal" && (typeof arg.value === "string" || typeof arg.value === "number" || typeof arg.value === "boolean")) return [String(arg.value)];
|
|
535
|
+
if (arg.type === "TemplateLiteral") {
|
|
536
|
+
const parts = [];
|
|
537
|
+
for (let i = 0; i < arg.quasis.length; i++) {
|
|
538
|
+
const quasi = arg.quasis[i];
|
|
539
|
+
const expression = arg.expressions[i];
|
|
540
|
+
if (!quasi.tail && quasi.value.cooked) parts.push(quasi.value.cooked);
|
|
541
|
+
if (expression) parts.push(...collect(expression));
|
|
542
|
+
if (quasi.tail && quasi.value.cooked) parts.push(quasi.value.cooked);
|
|
543
|
+
}
|
|
544
|
+
return parts;
|
|
545
|
+
}
|
|
546
|
+
if (arg.type === "BinaryExpression" && arg.operator === "+") return [...collect(arg.left), ...collect(arg.right)];
|
|
547
|
+
return [DYNAMIC_PART];
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/collector/vueCollector.ts
|
|
551
|
+
function collectVueKeys(file, templateAst, options) {
|
|
552
|
+
return templateAst.children.flatMap((c) => walkVueNode(file, c, options));
|
|
553
|
+
}
|
|
554
|
+
function walkVueNode(file, node, options) {
|
|
555
|
+
switch (node.type) {
|
|
556
|
+
case NodeTypes.ELEMENT: return [
|
|
557
|
+
...node.children.flatMap((c) => walkVueNode(file, c, options)),
|
|
558
|
+
...node.props.flatMap((c) => walkVueNode(file, c, options)),
|
|
559
|
+
...collectFromElementNode(node)
|
|
560
|
+
];
|
|
561
|
+
case NodeTypes.INTERPOLATION: return walkVueNode(file, node.content, options);
|
|
562
|
+
case NodeTypes.DIRECTIVE:
|
|
563
|
+
if (node.name === "t") return collectFromDirective(file, node, options);
|
|
564
|
+
return node.exp ? walkVueNode(file, node.exp, options) : [];
|
|
565
|
+
case NodeTypes.SIMPLE_EXPRESSION: return node.isStatic ? [] : collectFromExpression(file, node, options);
|
|
566
|
+
default: return [];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function collectFromElementNode(node) {
|
|
570
|
+
if (node.tag === "i18n-t") {
|
|
571
|
+
for (const prop of node.props) if (prop.type === NodeTypes.ATTRIBUTE && (prop.name === "keypath" || prop.name === "path") && prop.value?.content) return [{
|
|
572
|
+
key: prop.value.content,
|
|
573
|
+
start: prop.value.loc.start.offset + 1,
|
|
574
|
+
end: prop.value.loc.end.offset - 1
|
|
575
|
+
}];
|
|
576
|
+
}
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
function collectFromDirective(file, node, options) {
|
|
580
|
+
if (!node.exp || node.exp.type !== NodeTypes.SIMPLE_EXPRESSION) return [];
|
|
581
|
+
const content = node.exp.content.trim();
|
|
582
|
+
if (!content) return [];
|
|
583
|
+
const bodyPart = parseScript(file, content, {
|
|
584
|
+
wrapInParens: true,
|
|
585
|
+
offset: node.exp.loc.start.offset - 1,
|
|
586
|
+
fileSource: options?.fileSource
|
|
587
|
+
}).body[0];
|
|
588
|
+
if (!bodyPart || bodyPart.type !== "ExpressionStatement") return [];
|
|
589
|
+
return walkDirective(bodyPart.expression, node.exp.loc.start.offset - 1);
|
|
590
|
+
}
|
|
591
|
+
function walkDirective(expression, offset) {
|
|
592
|
+
if (expression.type === "ParenthesizedExpression") return walkDirective(expression.expression, offset);
|
|
593
|
+
if (expression.type === "Literal" && typeof expression.value === "string") return [{
|
|
594
|
+
key: expression.value,
|
|
595
|
+
start: expression.start + offset + 1,
|
|
596
|
+
end: expression.end + offset - 1
|
|
597
|
+
}];
|
|
598
|
+
if (expression.type === "ObjectExpression") {
|
|
599
|
+
for (const prop of expression.properties) if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "path" && prop.value.type === "Literal" && typeof prop.value.value === "string") return [{
|
|
600
|
+
key: prop.value.value,
|
|
601
|
+
start: prop.value.start + offset + 1,
|
|
602
|
+
end: prop.value.end + offset - 1
|
|
603
|
+
}];
|
|
604
|
+
}
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
function collectFromExpression(file, node, options) {
|
|
608
|
+
const content = node.content;
|
|
609
|
+
if (!content.trim()) return [];
|
|
610
|
+
if (!TRANSLATION_CALL_REGEX.test(content)) return [];
|
|
611
|
+
return collectJsKeys(parseScript(file, content, {
|
|
612
|
+
wrapInParens: true,
|
|
613
|
+
offset: node.loc.start.offset - 1,
|
|
614
|
+
fileSource: options?.fileSource
|
|
615
|
+
}), node.loc.start.offset - 1);
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/collector/sourceCollector.ts
|
|
619
|
+
async function collectSourceFile(file) {
|
|
620
|
+
const source = await readFile(file, { encoding: "utf-8" });
|
|
621
|
+
if (extname(file) === ".vue") return collectFromVue(source, file);
|
|
622
|
+
return {
|
|
623
|
+
keys: rawKeysToFileKeys(collectFromScript(source, file), file, source),
|
|
624
|
+
localeFiles: []
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function collectFromScript(source, file) {
|
|
628
|
+
if (!TRANSLATION_CALL_REGEX.test(source)) return [];
|
|
629
|
+
return collectJsKeys(parseScript(file, source));
|
|
630
|
+
}
|
|
631
|
+
function collectFromVue(source, file) {
|
|
632
|
+
const { descriptor, errors } = parse(source, {
|
|
633
|
+
filename: basename(file),
|
|
634
|
+
templateParseOptions: { prefixIdentifiers: false }
|
|
635
|
+
});
|
|
636
|
+
if (errors.length > 0 && !descriptor.template && !descriptor.script && !descriptor.scriptSetup) throw new ParseError(`Failed to parse Vue file:\n${errors.map((e) => ` • ${e.message}`).join("\n")}`, file);
|
|
637
|
+
const rawKeys = [];
|
|
638
|
+
const templateAst = descriptor.template?.ast;
|
|
639
|
+
if (templateAst) rawKeys.push(...collectVueKeys(file, templateAst, { fileSource: source }));
|
|
640
|
+
for (const script of [descriptor.script, descriptor.scriptSetup]) {
|
|
641
|
+
if (!script) continue;
|
|
642
|
+
if (!TRANSLATION_CALL_REGEX.test(script.content)) continue;
|
|
643
|
+
const program = parseScript(file, script.content, {
|
|
644
|
+
lang: script.lang,
|
|
645
|
+
offset: script.loc.start.offset,
|
|
646
|
+
fileSource: source
|
|
647
|
+
});
|
|
648
|
+
rawKeys.push(...collectJsKeys(program, script.loc.start.offset));
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
keys: rawKeysToFileKeys(rawKeys, file, source),
|
|
652
|
+
localeFiles: collectI18nBlocks(descriptor, file)
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
function collectI18nBlocks(descriptor, file) {
|
|
656
|
+
const localeFiles = [];
|
|
657
|
+
for (const block of descriptor.customBlocks) {
|
|
658
|
+
if (block.type !== "i18n") continue;
|
|
659
|
+
localeFiles.push(...parseI18nBlock(block, file));
|
|
660
|
+
}
|
|
661
|
+
return localeFiles;
|
|
662
|
+
}
|
|
663
|
+
function parseI18nBlock(block, file) {
|
|
664
|
+
const lang = typeof block.attrs["lang"] === "string" ? block.attrs["lang"] : "json";
|
|
665
|
+
const data = parseLocale(block.content, file, { ext: `.${lang}` });
|
|
666
|
+
return Object.entries(data).map(([locale, localeData]) => {
|
|
667
|
+
if (!isPlainObject(localeData)) return {
|
|
668
|
+
locale,
|
|
669
|
+
file,
|
|
670
|
+
keys: [],
|
|
671
|
+
rawData: {},
|
|
672
|
+
scope: "local"
|
|
673
|
+
};
|
|
674
|
+
return {
|
|
675
|
+
locale,
|
|
676
|
+
file,
|
|
677
|
+
scope: "local",
|
|
678
|
+
sourceFile: file,
|
|
679
|
+
rawData: localeData,
|
|
680
|
+
keys: extractLocaleKeys(localeData)
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
function rawKeysToFileKeys(rawKeys, file, source) {
|
|
685
|
+
return rawKeys.map((k) => ({
|
|
686
|
+
key: k.key,
|
|
687
|
+
file,
|
|
688
|
+
location: {
|
|
689
|
+
start: offsetToPosition(source, k.start),
|
|
690
|
+
end: offsetToPosition(source, k.end)
|
|
691
|
+
}
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/command/shared.ts
|
|
696
|
+
async function collectFiles(config, process) {
|
|
697
|
+
const globOptions = {
|
|
698
|
+
cwd: config.path,
|
|
699
|
+
ignore: config.ignorePatterns,
|
|
700
|
+
gitignore: true
|
|
701
|
+
};
|
|
702
|
+
const [rawLocalePaths, rawSrcPaths] = await Promise.all([globby(config.localePattern, globOptions), globby(config.srcPattern, globOptions)]);
|
|
703
|
+
const [localeFiles, sourceFiles] = await Promise.all([Promise.all(rawLocalePaths.map((p) => collectFile(resolve(config.path, p), collectLocaleFile, process))), Promise.all(rawSrcPaths.map((p) => collectFile(resolve(config.path, p), collectSourceFile, process)))]);
|
|
704
|
+
const validLocaleFiles = localeFiles.filter((f) => f != null);
|
|
705
|
+
const validSourceFiles = sourceFiles.filter((f) => f != null);
|
|
706
|
+
return {
|
|
707
|
+
localeFiles: validLocaleFiles,
|
|
708
|
+
sourceFiles: validSourceFiles,
|
|
709
|
+
parseErrors: localeFiles.length - validLocaleFiles.length + (sourceFiles.length - validSourceFiles.length)
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
async function collectFile(file, collect, process) {
|
|
713
|
+
try {
|
|
714
|
+
return await collect(file);
|
|
715
|
+
} catch (e) {
|
|
716
|
+
writeLine(process.stderr, `Failed to process: ${formatErrorMessage(e)}`);
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/command/lint.ts
|
|
722
|
+
const lintCommand = buildCommand({
|
|
723
|
+
async func(flags, path) {
|
|
724
|
+
const startTime = performance.now();
|
|
725
|
+
const config = await loadVueI18nLintConfig({
|
|
726
|
+
path,
|
|
727
|
+
...flags
|
|
728
|
+
});
|
|
729
|
+
const { localeFiles, sourceFiles, parseErrors } = await collectFiles(config, this.process);
|
|
730
|
+
const result = processFiles(localeFiles, sourceFiles, config);
|
|
731
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
732
|
+
const missingSeverity = config.checks.missingKeys.severity;
|
|
733
|
+
const unusedSeverity = config.checks.unusedKeys.severity;
|
|
734
|
+
if (config.format === "text") {
|
|
735
|
+
if (parseErrors > 0) writeLine(this.process.stdout);
|
|
736
|
+
if (result.typeWarnings.length > 0) outputTypeWarnings(this.process, result.typeWarnings);
|
|
737
|
+
if (result.missing && result.missing.length > 0) outputMissingKeys(this.process, result.missing);
|
|
738
|
+
if (result.unused && result.unused.length > 0) outputUnusedKeys(this.process, result.unused);
|
|
739
|
+
const summaryParts = [];
|
|
740
|
+
if (result.missing != null) summaryParts.push(`${formatSummaryPart(result.missing.length, config.checks.missingKeys.severity)} missing`);
|
|
741
|
+
if (result.unused != null) summaryParts.push(`${formatSummaryPart(result.unused.length, config.checks.unusedKeys.severity)} unused`);
|
|
742
|
+
if (summaryParts.length > 0) writeLine(this.process.stdout, `Found ${summaryParts.join(" and ")} keys.`);
|
|
743
|
+
const errorSummary = parseErrors > 0 ? ` (${parseErrors} file${parseErrors === 1 ? "" : "s"} skipped due to errors)` : "";
|
|
744
|
+
writeLine(this.process.stdout, `Processed ${localeFiles.length} locale files and ${sourceFiles.length} source files in ${elapsed}ms${errorSummary}.`);
|
|
745
|
+
} else {
|
|
746
|
+
const outputData = {
|
|
747
|
+
missingKeys: result.missing,
|
|
748
|
+
unusedKeys: result.unused
|
|
749
|
+
};
|
|
750
|
+
if (config.format === "json") outputJson(this.process, outputData);
|
|
751
|
+
else if (config.format === "toon") outputToon(this.process, outputData);
|
|
752
|
+
}
|
|
753
|
+
const missingIsError = missingSeverity === "error" && result.missing && result.missing.length > 0;
|
|
754
|
+
const unusedIsError = unusedSeverity === "error" && result.unused && result.unused.length > 0;
|
|
755
|
+
if (missingIsError || unusedIsError || parseErrors > 0) this.process.exitCode = 1;
|
|
756
|
+
},
|
|
757
|
+
parameters: {
|
|
758
|
+
flags: {
|
|
759
|
+
format: {
|
|
760
|
+
kind: "enum",
|
|
761
|
+
values: formatEnum.options,
|
|
762
|
+
optional: true,
|
|
763
|
+
brief: "Output format"
|
|
764
|
+
},
|
|
765
|
+
localePattern: {
|
|
766
|
+
kind: "parsed",
|
|
767
|
+
parse: String,
|
|
768
|
+
optional: true,
|
|
769
|
+
brief: "Glob pattern for i18n locale files"
|
|
770
|
+
},
|
|
771
|
+
srcPattern: {
|
|
772
|
+
kind: "parsed",
|
|
773
|
+
parse: String,
|
|
774
|
+
optional: true,
|
|
775
|
+
brief: "Glob pattern for source files"
|
|
776
|
+
},
|
|
777
|
+
ignorePatterns: {
|
|
778
|
+
kind: "parsed",
|
|
779
|
+
parse: String,
|
|
780
|
+
optional: true,
|
|
781
|
+
variadic: ",",
|
|
782
|
+
brief: "Comma-separated glob patterns to ignore"
|
|
783
|
+
},
|
|
784
|
+
ignoreKeys: {
|
|
785
|
+
kind: "parsed",
|
|
786
|
+
parse: String,
|
|
787
|
+
optional: true,
|
|
788
|
+
variadic: ",",
|
|
789
|
+
brief: "Comma-separated keys to ignore in both missing and unused checks"
|
|
790
|
+
},
|
|
791
|
+
ignoreMissingKeys: {
|
|
792
|
+
kind: "parsed",
|
|
793
|
+
parse: String,
|
|
794
|
+
optional: true,
|
|
795
|
+
variadic: ",",
|
|
796
|
+
brief: "Comma-separated keys to ignore in the missing keys check"
|
|
797
|
+
},
|
|
798
|
+
ignoreUnusedKeys: {
|
|
799
|
+
kind: "parsed",
|
|
800
|
+
parse: String,
|
|
801
|
+
optional: true,
|
|
802
|
+
variadic: ",",
|
|
803
|
+
brief: "Comma-separated keys to ignore in the unused keys check"
|
|
804
|
+
},
|
|
805
|
+
missingKeysSeverity: {
|
|
806
|
+
kind: "enum",
|
|
807
|
+
values: severityEnum.options,
|
|
808
|
+
optional: true,
|
|
809
|
+
brief: "Severity for missing keys"
|
|
810
|
+
},
|
|
811
|
+
unusedKeysSeverity: {
|
|
812
|
+
kind: "enum",
|
|
813
|
+
values: severityEnum.options,
|
|
814
|
+
optional: true,
|
|
815
|
+
brief: "Severity for unused keys"
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
positional: {
|
|
819
|
+
kind: "tuple",
|
|
820
|
+
parameters: [{
|
|
821
|
+
brief: "Working directory",
|
|
822
|
+
parse: String,
|
|
823
|
+
placeholder: "path",
|
|
824
|
+
optional: true
|
|
825
|
+
}]
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
docs: { brief: "Lint i18n keys in your project" }
|
|
829
|
+
});
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region src/command/removeUnused.ts
|
|
832
|
+
const removeUnusedCommand = buildCommand({
|
|
833
|
+
async func(flags, path) {
|
|
834
|
+
const config = await loadVueI18nLintConfig({
|
|
835
|
+
path,
|
|
836
|
+
localePattern: flags.localePattern,
|
|
837
|
+
srcPattern: flags.srcPattern,
|
|
838
|
+
ignorePatterns: flags.ignorePatterns,
|
|
839
|
+
ignoreKeys: flags.ignoreKeys,
|
|
840
|
+
ignoreUnusedKeys: flags.ignoreUnusedKeys
|
|
841
|
+
});
|
|
842
|
+
const { localeFiles, sourceFiles, parseErrors } = await collectFiles(config, this.process);
|
|
843
|
+
const { unused } = processFiles(localeFiles, sourceFiles, defu({ checks: {
|
|
844
|
+
missingKeys: { severity: "off" },
|
|
845
|
+
unusedKeys: { severity: "error" }
|
|
846
|
+
} }, config));
|
|
847
|
+
const unusedSet = new Set(unused?.filter((it) => it.files.some((file) => file.scope === "global")).map((it) => it.key) ?? []);
|
|
848
|
+
if (parseErrors > 0) writeLine(this.process.stdout);
|
|
849
|
+
if (unusedSet.size === 0) {
|
|
850
|
+
writeLine(this.process.stdout, "No unused keys found.");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (!flags.dryRun) await Promise.all(localeFiles.map(async (localeFile) => {
|
|
854
|
+
const stringifier = stringifiers[extname(localeFile.file)];
|
|
855
|
+
if (!stringifier) return;
|
|
856
|
+
removeKeysFromData(localeFile.rawData, unusedSet);
|
|
857
|
+
await writeFile(localeFile.file, stringifier(localeFile.rawData), "utf-8");
|
|
858
|
+
}));
|
|
859
|
+
writeLine(this.process.stdout, flags.dryRun ? `Would remove ${unusedSet.size} unused key${unusedSet.size === 1 ? "" : "s"}.` : `Removed ${unusedSet.size} unused key${unusedSet.size === 1 ? "" : "s"}.`);
|
|
860
|
+
},
|
|
861
|
+
parameters: {
|
|
862
|
+
flags: {
|
|
863
|
+
dryRun: {
|
|
864
|
+
kind: "boolean",
|
|
865
|
+
optional: true,
|
|
866
|
+
brief: "Print count without modifying files"
|
|
867
|
+
},
|
|
868
|
+
localePattern: {
|
|
869
|
+
kind: "parsed",
|
|
870
|
+
parse: String,
|
|
871
|
+
optional: true,
|
|
872
|
+
brief: "Glob pattern for i18n locale files"
|
|
873
|
+
},
|
|
874
|
+
srcPattern: {
|
|
875
|
+
kind: "parsed",
|
|
876
|
+
parse: String,
|
|
877
|
+
optional: true,
|
|
878
|
+
brief: "Glob pattern for source files"
|
|
879
|
+
},
|
|
880
|
+
ignorePatterns: {
|
|
881
|
+
kind: "parsed",
|
|
882
|
+
parse: String,
|
|
883
|
+
optional: true,
|
|
884
|
+
variadic: ",",
|
|
885
|
+
brief: "Comma-separated glob patterns to ignore"
|
|
886
|
+
},
|
|
887
|
+
ignoreKeys: {
|
|
888
|
+
kind: "parsed",
|
|
889
|
+
parse: String,
|
|
890
|
+
optional: true,
|
|
891
|
+
variadic: ",",
|
|
892
|
+
brief: "Comma-separated keys to ignore"
|
|
893
|
+
},
|
|
894
|
+
ignoreUnusedKeys: {
|
|
895
|
+
kind: "parsed",
|
|
896
|
+
parse: String,
|
|
897
|
+
optional: true,
|
|
898
|
+
variadic: ",",
|
|
899
|
+
brief: "Comma-separated keys to ignore in the unused keys check"
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
positional: {
|
|
903
|
+
kind: "tuple",
|
|
904
|
+
parameters: [{
|
|
905
|
+
brief: "Working directory",
|
|
906
|
+
parse: String,
|
|
907
|
+
placeholder: "path",
|
|
908
|
+
optional: true
|
|
909
|
+
}]
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
docs: { brief: "Remove unused i18n keys from locale files" }
|
|
913
|
+
});
|
|
914
|
+
const stringifiers = {
|
|
915
|
+
".json": (data) => stringifyJSON(data, { indent: 2 }),
|
|
916
|
+
".jsonc": (data) => stringifyJSONC(data, { indent: 2 }),
|
|
917
|
+
".json5": (data) => stringifyJSON5(data, { indent: 2 }),
|
|
918
|
+
".yaml": (data) => stringifyYAML(data, { indent: 2 }),
|
|
919
|
+
".yml": (data) => stringifyYAML(data, { indent: 2 })
|
|
920
|
+
};
|
|
921
|
+
function removeKeysFromData(data, keys, prefix = "") {
|
|
922
|
+
if (typeof data !== "object" || data == null) return false;
|
|
923
|
+
if (isPlainObject(data)) {
|
|
924
|
+
for (const key of Object.keys(data)) {
|
|
925
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
926
|
+
if (keys.has(fullKey) || removeKeysFromData(data[key], keys, fullKey)) delete data[key];
|
|
927
|
+
}
|
|
928
|
+
return Object.keys(data).length === 0;
|
|
929
|
+
}
|
|
930
|
+
if (Array.isArray(data)) {
|
|
931
|
+
for (let i = data.length - 1; i >= 0; i--) {
|
|
932
|
+
const fullKey = prefix ? `${prefix}.${i}` : String(i);
|
|
933
|
+
if (keys.has(fullKey) || removeKeysFromData(data[i], keys, fullKey)) data.splice(i, 1);
|
|
934
|
+
}
|
|
935
|
+
return data.length === 0;
|
|
936
|
+
}
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region src/cli.ts
|
|
941
|
+
await run(buildApplication(buildRouteMap({
|
|
942
|
+
routes: {
|
|
943
|
+
lint: lintCommand,
|
|
944
|
+
init: initCommand,
|
|
945
|
+
removeUnused: removeUnusedCommand
|
|
946
|
+
},
|
|
947
|
+
defaultCommand: "lint",
|
|
948
|
+
docs: { brief: "Fast and accurate linting for Vue i18n." }
|
|
949
|
+
}), {
|
|
950
|
+
name: "vue-i18n-lint",
|
|
951
|
+
scanner: { caseStyle: "allow-kebab-for-camel" },
|
|
952
|
+
documentation: { caseStyle: "convert-camel-to-kebab" },
|
|
953
|
+
localization: { text: {
|
|
954
|
+
...text_en,
|
|
955
|
+
formatException: formatErrorMessage
|
|
956
|
+
} }
|
|
957
|
+
}), process.argv.slice(2), { process: {
|
|
958
|
+
stdout: process.stdout,
|
|
959
|
+
stderr: process.stderr,
|
|
960
|
+
env: process.env
|
|
961
|
+
} });
|
|
962
|
+
//#endregion
|
|
963
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/config/schema.d.ts
|
|
4
|
+
declare const configSchema: z.ZodObject<{
|
|
5
|
+
path: z.ZodString;
|
|
6
|
+
format: z.ZodDefault<z.ZodEnum<{
|
|
7
|
+
json: "json";
|
|
8
|
+
text: "text";
|
|
9
|
+
toon: "toon";
|
|
10
|
+
}>>;
|
|
11
|
+
localePattern: z.ZodDefault<z.ZodString>;
|
|
12
|
+
srcPattern: z.ZodDefault<z.ZodString>;
|
|
13
|
+
ignorePatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
14
|
+
ignoreKeys: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
15
|
+
checks: z.ZodDefault<z.ZodObject<{
|
|
16
|
+
missingKeys: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
|
|
17
|
+
severity: z.ZodDefault<z.ZodEnum<{
|
|
18
|
+
error: "error";
|
|
19
|
+
off: "off";
|
|
20
|
+
warning: "warning";
|
|
21
|
+
}>>;
|
|
22
|
+
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
23
|
+
}, z.core.$strip>, z.ZodPipe<z.ZodLiteral<false>, z.ZodTransform<{
|
|
24
|
+
severity: "off";
|
|
25
|
+
ignore: string[];
|
|
26
|
+
}, false>>]>>;
|
|
27
|
+
unusedKeys: z.ZodDefault<z.ZodUnion<readonly [z.ZodObject<{
|
|
28
|
+
severity: z.ZodDefault<z.ZodEnum<{
|
|
29
|
+
error: "error";
|
|
30
|
+
off: "off";
|
|
31
|
+
warning: "warning";
|
|
32
|
+
}>>;
|
|
33
|
+
ignore: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
34
|
+
}, z.core.$strip>, z.ZodPipe<z.ZodLiteral<false>, z.ZodTransform<{
|
|
35
|
+
severity: "off";
|
|
36
|
+
ignore: string[];
|
|
37
|
+
}, false>>]>>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
type ConfigInput = z.input<typeof configSchema>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/index.d.ts
|
|
43
|
+
type VueI18nLintConfig = Omit<ConfigInput, "path">;
|
|
44
|
+
declare const defineConfig: import("c12").DefineConfig<VueI18nLintConfig, import("c12").ConfigLayerMeta>;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { VueI18nLintConfig, defineConfig };
|
package/dist/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-i18n-lint",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Fast and accurate linting for Vue i18n.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"i18n",
|
|
7
|
+
"lint",
|
|
8
|
+
"vue"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Ruben Gees",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "rubengees/vue-i18n-lint"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"vue-i18n-lint": "./dist/cli.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "dist/index.mjs",
|
|
24
|
+
"types": "dist/index.d.mts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@babel/code-frame": "^8.0.0",
|
|
30
|
+
"@stricli/core": "^1.2.8",
|
|
31
|
+
"@toon-format/toon": "^2.3.0",
|
|
32
|
+
"@vue/compiler-core": "^3.5.39",
|
|
33
|
+
"@vue/compiler-sfc": "^3.5.39",
|
|
34
|
+
"c12": "4.0.0-beta.5",
|
|
35
|
+
"confbox": "^0.2.4",
|
|
36
|
+
"defu": "^6.1.7",
|
|
37
|
+
"globby": "^16.2.0",
|
|
38
|
+
"jiti": "^2.7.0",
|
|
39
|
+
"oxc-parser": "^0.137.0",
|
|
40
|
+
"regexp.escape": "^2.0.1",
|
|
41
|
+
"table": "^6.9.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@tsconfig/node22": "22.0.5",
|
|
46
|
+
"@types/babel__code-frame": "7.27.0",
|
|
47
|
+
"@types/node": "22.19.21",
|
|
48
|
+
"@types/regexp.escape": "2.0.0",
|
|
49
|
+
"@vitest/coverage-v8": "4.1.9",
|
|
50
|
+
"husky": "9.1.7",
|
|
51
|
+
"lint-staged": "17.0.8",
|
|
52
|
+
"oxfmt": "0.56.0",
|
|
53
|
+
"oxlint": "1.71.0",
|
|
54
|
+
"oxlint-tsgolint": "0.23.0",
|
|
55
|
+
"tsdown": "0.22.3",
|
|
56
|
+
"typescript": "7.0.1-rc",
|
|
57
|
+
"vitest": "4.1.9"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=22"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"dev": "vitest",
|
|
64
|
+
"start": "jiti src/cli.ts",
|
|
65
|
+
"build": "tsdown",
|
|
66
|
+
"lint": "oxlint",
|
|
67
|
+
"format": "oxfmt",
|
|
68
|
+
"format:check": "oxfmt --check",
|
|
69
|
+
"check": "tsc --noEmit",
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"bench": "vitest --run bench",
|
|
72
|
+
"verify": "pnpm format:check && pnpm lint && pnpm check && pnpm test"
|
|
73
|
+
}
|
|
74
|
+
}
|