@x18n/cli 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/cli",
3
3
  "type": "module",
4
- "version": "0.0.0",
4
+ "version": "0.0.2",
5
5
  "description": "Manage and view i18n files of any project, for humans and agents",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -38,10 +38,10 @@
38
38
  "node": ">=22"
39
39
  },
40
40
  "dependencies": {
41
- "@x18n/builtins": "0.0.0",
42
- "@x18n/core": "0.0.0",
43
- "@x18n/node": "0.0.0",
44
- "@x18n/ui": "0.0.0",
41
+ "@x18n/builtins": "0.0.2",
42
+ "@x18n/core": "0.0.2",
43
+ "@x18n/node": "0.0.2",
44
+ "@x18n/ui": "0.0.2",
45
45
  "cac": "^7.1.0",
46
46
  "devframe": "^1.2.5",
47
47
  "yaml": "^2.9.1"
@@ -1,27 +1,67 @@
1
1
  ---
2
2
  name: x18n
3
- description: Maintain the i18n files of a project with the x18n CLI - find missing, unused and broken keys, add and rename keys, translate missing messages, and extract hard-coded strings. Use when the task touches locale files (JSON, YAML, Vue i18n blocks) or translation keys in code, instead of editing locale files by hand.
3
+ description: Set up and run x18n (`@x18n/cli`) to check and manage a project's translations. Use when the task touches locale files or translation keys - adding, renaming or deleting keys, finding missing or unused keys, translating missing messages, extracting hard-coded strings, keeping a glossary of terms - or when setting up x18n or migrating from i18n-ally.
4
4
  ---
5
5
 
6
6
  # x18n
7
7
 
8
- x18n reads the project's i18n setup (locale files, translate calls in code) and changes it safely. Use the `x18n` CLI instead of editing locale files directly: it keeps formatting, key style, file choice and placeholders correct, and every command has stable JSON output.
8
+ x18n reads a project's i18n setup (locale files, translate calls in code) and changes it safely. Work through the `x18n` CLI from `@x18n/cli`: it keeps formatting, key style, file choice and placeholders correct, and every command prints a stable JSON envelope with `--json`.
9
+
10
+ ## Decisions belong to the user
11
+
12
+ You do the legwork; the user makes the **decisions** that shape the product. Before you act on one of these, stop and ask, with your recommendation and the facts you found:
13
+
14
+ - the source locale, and which locales the project supports
15
+ - key naming and structure: namespaces, nesting, the style of new keys
16
+ - a glossary term and its translation per locale, and terms that must stay untranslated (brand and product names)
17
+ - tone and formality per locale (German *du* or *Sie*, French *tu* or *vous*)
18
+ - deleting keys that look unused, since keys built at runtime look unused too
19
+ - renames that touch many files or move keys between namespaces
20
+ - turning on checks, and making warnings fail CI with `--strict`
21
+
22
+ Batch the questions into one message. Routine work needs no question: adding a key you use, translating a missing message, fixing a placeholder.
9
23
 
10
24
  ## Rules
11
25
 
12
- - Do not edit locale files by hand. Use `x18n set`, `x18n apply`, `x18n rename` and `x18n delete`.
13
- - Before using a key in code, check whether it exists: `x18n get <key> --json`.
14
- - Never drop or rename placeholders (`{name}`, `{{count}}`, `%s`, `@:key`).
15
- - Pass `--json` and read the envelope `{ "x18n": 1, "command", "data", "diagnostics", "errors" }`. Never parse the human output.
16
- - Prefer `--dry-run` first for renames and deletes, and read the plan.
26
+ - Change locale files through `x18n set`, `x18n apply`, `x18n rename` and `x18n delete`. The config file (`x18n.config.ts`) is the one i18n file you edit by hand.
27
+ - Check a key before you use it in code: `x18n get <key> --json`. Reuse an existing key with the same meaning.
28
+ - Keep every placeholder of the source message (`{name}`, `{{count}}`, `%{name}`, `:name`, `%s`, `@:key`).
29
+ - Pass `--json` and read the envelope `{ "x18n": 1, "command", "data", "diagnostics", "errors" }`.
30
+ - Run renames and deletes with `--dry-run` first, and read the plan.
17
31
 
18
- ## Exit codes
32
+ ## Set up x18n
19
33
 
20
- - `0` success
21
- - `1` `x18n check` found errors, or `x18n apply` rejected values
22
- - `2` usage or config error
23
- - `3` a decision is needed: re-run with `--yes` to accept the defaults
24
- - `4` files changed during the command: run it again
34
+ 1. Run `npx @x18n/cli doctor` in the project root. Without a config, x18n uses the project's i18n-ally settings if it has any, and detects the i18n library and the locale files. Check the detected presets, the locale files and the source locale. **Done when** the doctor output names the library the project uses and lists every locale file.
35
+ 2. If the project has i18n-ally settings (`i18n-ally.*` keys in `.vscode/settings.json`), follow [references/migrate-i18n-ally.md](references/migrate-i18n-ally.md) instead of step 3.
36
+ 3. If detection is wrong or incomplete, run `x18n init` to write `x18n.config.ts`, then fix `resources`, `sourceLocale` or `presets` in it. `x18n init --format yml` writes YAML for projects without Node.js tooling. **Done when** `x18n doctor` shows the right result.
37
+ 4. Install the CLI as a dev dependency with the project's package manager (`pnpm add -D @x18n/cli`), and add a script such as `"i18n:check": "x18n check"`. Ask the user before you add it to CI.
38
+ 5. Run `x18n check --json` and report the summary to the user. **Done when** the user has the counts of errors and warnings per check, and has decided what to fix first.
39
+
40
+ Commands below use `x18n`; without the dev dependency, run them as `npx @x18n/cli <command>`.
41
+
42
+ ## Glossary (terms table)
43
+
44
+ The `glossary` in the config is the project's terms table. Translate prompts include it, and the `glossary` check reports messages that translate a term differently.
45
+
46
+ ```ts
47
+ // x18n.config.ts
48
+ import { defineConfig } from '@x18n/core'
49
+
50
+ export default defineConfig({
51
+ glossary: [
52
+ { term: 'Workspace', translations: { de: 'Arbeitsbereich', fr: 'Espace de travail' } },
53
+ { term: 'x18n', doNotTranslate: true },
54
+ { term: 'Save', translations: { de: 'Speichern' }, note: 'The button label, not "rescue"' },
55
+ ],
56
+ checks: { glossary: 'warning' },
57
+ })
58
+ ```
59
+
60
+ Maintain it while you work:
61
+
62
+ - Before you translate, read the current entries: `x18n doctor --json` lists them under `config.glossary`.
63
+ - When you meet a term that recurs across messages or has a product meaning, propose an entry to the user, with a translation for each locale.
64
+ - Add the entries the user approves, then run `x18n check --only glossary --json` and fix the messages it reports.
25
65
 
26
66
  ## Loops
27
67
 
@@ -48,14 +88,14 @@ x18n keys --missing de --json --limit 50
48
88
  x18n prompt translate --to de --limit 50
49
89
  ```
50
90
 
51
- Follow the prompt: write `{ "de": { "<key>": "<translation>" } }` to a file, then:
91
+ Follow the prompt. It holds the rules, the glossary, examples of existing translations and the code around each usage. Write `{ "de": { "<key>": "<translation>" } }` to a file, then:
52
92
 
53
93
  ```sh
54
94
  x18n apply translations.json --json
55
- x18n check --only placeholder-mismatch,invalid-syntax --locale de --json
95
+ x18n check --only placeholder-mismatch,invalid-syntax,glossary --locale de --json
56
96
  ```
57
97
 
58
- `x18n apply` refuses values whose placeholders differ from the source and lists them in `data.rejected`; fix them and apply again. When the prompt reports more keys, continue with `x18n prompt translate --to de --cursor <nextCursor>`.
98
+ `x18n apply` refuses values whose placeholders differ from the source and lists them in `data.rejected`. Fix them and apply again. When the prompt reports more keys, continue with `x18n prompt translate --to de --cursor <nextCursor>`.
59
99
 
60
100
  ### Review translations
61
101
 
@@ -63,16 +103,17 @@ x18n check --only placeholder-mismatch,invalid-syntax --locale de --json
63
103
  x18n prompt review --locale de --limit 50
64
104
  ```
65
105
 
66
- Fix wrong values with `x18n set <key> --locale de --value <text> --json` and list what you changed.
106
+ Fix wrong values with `x18n set <key> --locale de --value <text> --json`, and list what you changed and why.
67
107
 
68
108
  ### Clean up
69
109
 
70
110
  ```sh
71
111
  x18n keys --unused --json
72
112
  x18n delete old.key --dry-run --json
73
- x18n delete old.key --json
74
113
  ```
75
114
 
115
+ Show the user the unused keys and delete only the ones they confirm: `x18n delete old.key --json`.
116
+
76
117
  ### Rename keys
77
118
 
78
119
  ```sh
@@ -81,7 +122,7 @@ x18n rename home.title home.heading --json
81
122
  x18n rename pages screens --json
82
123
  ```
83
124
 
84
- A rename updates every locale and every usage in code. Renaming a parent (`pages`) moves every key under it. A rename to an existing key is refused (exit code 1).
125
+ A rename updates every locale and every usage in code. Renaming a parent (`pages`) moves every key under it. A rename onto an existing key is refused (exit code 1).
85
126
 
86
127
  ### Extract hard-coded strings
87
128
 
@@ -91,16 +132,18 @@ x18n extract --plan extract-plan.json
91
132
  x18n extract --apply extract-plan.json --json
92
133
  ```
93
134
 
94
- Edit the keys in the plan file before applying it.
135
+ Edit the keys in the plan file before you apply it. The proposed keys follow the project's style; ask the user when there is none yet.
95
136
 
96
- ## Paging
137
+ ## Output
97
138
 
98
- List commands take `--limit` and return `nextCursor`; pass it as `--cursor` to get the next page:
139
+ Exit codes:
99
140
 
100
- ```sh
101
- x18n keys --json --limit 100 --cursor 100
102
- ```
141
+ - `0` success
142
+ - `1` `x18n check` found errors, or `x18n apply` rejected values
143
+ - `2` usage or config error
144
+ - `3` a decision is needed: re-run with `--yes` to accept the defaults
145
+ - `4` files changed during the command: run it again
103
146
 
104
- ## Monorepos
147
+ List commands take `--limit` and return `nextCursor`; pass it as `--cursor` to get the next page: `x18n keys --json --limit 100 --cursor 100`.
105
148
 
106
- When `x18n status --json` lists several projects, pass `--project <name>` to write commands.
149
+ When `x18n status --json` lists several projects (a monorepo), pass `--project <name>` to write commands.
@@ -0,0 +1,49 @@
1
+ # Migrate from i18n-ally
2
+
3
+ Use this when a project has i18n-ally settings: `i18n-ally.*`, legacy `vue-i18n-ally.*` or i18n-ally-next `i18n-ally-next.*` keys in `.vscode/settings.json` or a `*.code-workspace` file, or a `.vscode/i18n-ally-custom-framework.yml` (i18n-ally-next: `i18n-ally-next-custom-framework.yml`). `x18n migrate i18n-ally` converts them. It never deletes i18n-ally files.
4
+
5
+ ## Steps
6
+
7
+ 1. Record the baseline before you change anything: `npx @x18n/cli status --json` and `npx @x18n/cli keys --unused --json`. x18n already reads the i18n-ally settings without a config, so this is what the project gets today. **Done when** you have the locale list, the key count and the unused keys.
8
+ 2. Preview the migration: `x18n migrate i18n-ally --dry-run`. It prints the files it would write and notes about settings it dropped. **Done when** you have read every note.
9
+ 3. Ask the user about each **decision** the notes raise, in one message. The usual ones:
10
+ - `translate.*` settings (machine translation engines) are dropped. x18n writes translate prompts instead (`x18n prompt translate --to <locale>`).
11
+ - `languageTagSystem` is dropped: x18n uses locale codes exactly as they appear in file names.
12
+ - A custom framework with `monopoly: true` turns the built-in presets off (`presets: []`).
13
+ - Executed JS/TS locale files become read-only (`execute: true`).
14
+ - Reviews move to `.x18n/reviews.yml`, which x18n reads once reviews ship.
15
+ - The config format: `x18n.config.ts` by default, or `--format yml` / `--format json` for projects without Node.js tooling.
16
+ 4. Write the files: `x18n migrate i18n-ally --json` (add `--format yml` if the user chose YAML; `--force` replaces an existing x18n config). It writes:
17
+ - `x18n.config.ts` (or `.yml`, `.json`) from the project settings
18
+ - `x18n/custom.x18n-plugin.yml`, when there are custom regexes, a custom framework, refactor templates or `localeCountryMap` flags
19
+ - editor settings in `.vscode/settings.json`: `x18n.displayLocale`, `x18n.inPlace`, `x18n.annotations`, `x18n.annotationMaxLength`
20
+ - `.x18n/reviews.yml`, when `.vscode/i18n-ally-reviews.yml` or `.vscode/i18n-ally-next-reviews.yml` exists
21
+
22
+ **Done when** the command exits with code 0 and `data.files` lists the files above that apply.
23
+ 5. Verify: run `x18n doctor --json`, `x18n status --json` and `x18n keys --unused --json`, and compare them with the baseline of step 1. The locales and keys must match. The unused keys may shrink, because `keysInUse` and the derived-key rules now apply. **Done when** every difference is explained to the user.
24
+ 6. Clean up, after the user agrees: the command's `data.removable` lists the i18n-ally settings that x18n no longer needs. Remove them from `.vscode/settings.json`, delete the custom framework file if listed, and suggest the VS Code extension `antfu.x18n` in place of `lokalise.i18n-ally` or `lydanne.i18n-ally-next`.
25
+
26
+ In VS Code, the extension offers the same migration when it finds i18n-ally settings and no x18n config: the command **x18n: Migrate from i18n-ally** previews the files and writes them on confirmation.
27
+
28
+ ## What maps where
29
+
30
+ | i18n-ally | x18n |
31
+ | --- | --- |
32
+ | `localesPaths`, `pathMatcher`, `dirStructure`, `namespace` | `resources[].pattern`, with the same `{locale}`, `{namespace}`, `{namespaces}`, `{ext}` tokens |
33
+ | `enabledFrameworks` | `presets`: every i18n-ally framework has a preset (`vue` → `vue-i18n`, `react` → `react-intl`, `ember` → `ember-intl`, `chrome-ext` → `chrome-extension`); `general` and `custom` are dropped |
34
+ | `sourceLanguage` | `sourceLocale` |
35
+ | `displayLanguage` | VS Code setting `x18n.displayLocale` |
36
+ | `ignoredLocales` | `ignoreLocales` |
37
+ | `keystyle`, `disablePathParsing` | `keyStyle` (`disablePathParsing` becomes `flat`) |
38
+ | `defaultNamespace` | `namespace.default` |
39
+ | `enabledParsers`, `parsers.extendFileExtensions` | `formats` |
40
+ | `keysInUse`, `usage.derivedKeyRules`, `usage.scanningIgnore` | `usage.keysInUse`, `usage.derivedKeys`, `usage.exclude` |
41
+ | `regex.usageMatch`, `regex.usageMatchAppend` | `scanners.regex` in the generated YAML plugin |
42
+ | `refactor.templates` | `extract.templates` |
43
+ | `extract.*` | `extract.*`, same meaning |
44
+ | `sortKeys`, `sortCompare`, `indent`, `tabStyle`, `keepFulfilled` | `write.sort`, `write.indent`, `write.keepFilled` |
45
+ | `annotations`, `annotationInPlace`, `annotationMaxLength` | VS Code settings `x18n.annotations`, `x18n.inPlace`, `x18n.annotationMaxLength` |
46
+ | `localeCountryMap` | `localeInfo` flags in the generated YAML plugin |
47
+ | custom framework `languageIds`, `usageMatchRegex`, `scopeRangeRegex`, `refactorTemplates`, `monopoly` | the generated YAML plugin: `scanners.regex`, `scopes.regex`, `extract.templates`; `monopoly` sets `presets: []` |
48
+
49
+ The full map is on the docs page <https://x18n.antfu.dev/migration>.
package/dist/cli.d.mts DELETED
@@ -1,4 +0,0 @@
1
- import { CAC } from "cac";
2
- //#region src/cli.d.ts
3
- export declare function createCli(): CAC;
4
- //#endregion
package/dist/cli.mjs DELETED
@@ -1,672 +0,0 @@
1
- import { a as openWorkspace, c as readInput, d as stdinArg, f as useColor, i as exitCodeOf, l as runPlan, n as displayPath, o as out, p as wireDiagnostics, r as errorInfo, s as printJson, t as cwd, u as singleProject } from "./context-Za_ea1Nk.mjs";
2
- import { createRequire } from "node:module";
3
- import { PluginFileSchema, X18nError, isJsonObject, parseMessageBatch, placeholderProblems, renderPrompt, toWireRange, validate } from "@x18n/core";
4
- import process from "node:process";
5
- import cac from "cac";
6
- import { mkdir, readFile, writeFile } from "node:fs/promises";
7
- import { dirname, join, resolve } from "node:path";
8
- import { createInterface } from "node:readline/promises";
9
- import { configFileNames, findConfigFile, loadWorkspace, migrateI18nAlly } from "@x18n/node";
10
- import { extractItems, findCandidates, toPlanFile } from "@x18n/builtins/extractors";
11
- import { existsSync } from "node:fs";
12
- import { parse, stringify } from "yaml";
13
- //#region package.json
14
- var version = "0.0.0";
15
- //#endregion
16
- //#region src/commands/change.ts
17
- async function project(options) {
18
- return singleProject(await openWorkspace(options));
19
- }
20
- async function set(key, options) {
21
- if (options.value === void 0) throw new X18nError("X18N4008", "pass --value <text> (or --value - to read stdin)");
22
- const loaded = await project(options);
23
- const value = options.value === "\0stdin" ? (await readInput(stdinArg)).replace(/\r?\n$/, "") : String(options.value);
24
- const locale = options.locale ?? loaded.project.sourceLocale;
25
- await runPlan("set", loaded, loaded.project.plan([{
26
- type: "set",
27
- key: loaded.project.parseKey(key),
28
- locale,
29
- value
30
- }]), options);
31
- }
32
- async function apply(source, options) {
33
- const loaded = await project(options);
34
- const p = loaded.project;
35
- let data;
36
- try {
37
- data = JSON.parse(await readInput(source));
38
- } catch (error) {
39
- throw new X18nError("X18N4008", `${source} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
40
- }
41
- const ops = parseMessageBatch(p, data);
42
- const rejected = placeholderProblems(p, ops);
43
- if (rejected.length > 0 && !options.force) {
44
- if (options.json) {
45
- printJson("apply", {
46
- applied: false,
47
- rejected
48
- }, [], [new X18nError("X18N4007", `${rejected.length} values`).toJSON()]);
49
- return 1;
50
- }
51
- for (const r of rejected) out(`rejected ${r.locale} ${r.key}: ${[r.missing.length ? `missing ${r.missing.join(", ")}` : "", r.extra.length ? `unexpected ${r.extra.join(", ")}` : ""].filter(Boolean).join("; ")}`);
52
- out(`${rejected.length} values have placeholder problems; nothing was written. Fix them or pass --force.`);
53
- return 1;
54
- }
55
- await runPlan("apply", loaded, p.plan(ops), options);
56
- return 0;
57
- }
58
- async function rename(from, to, options) {
59
- const loaded = await project(options);
60
- const p = loaded.project;
61
- const op = {
62
- type: "rename",
63
- from: p.parseKey(from),
64
- to: p.parseKey(to),
65
- usages: options.usages !== false
66
- };
67
- await runPlan("rename", loaded, p.plan([op]), options);
68
- }
69
- async function remove(key, options) {
70
- const loaded = await project(options);
71
- const locales = options.locale === void 0 ? "all" : [options.locale].flat();
72
- await runPlan("delete", loaded, loaded.project.plan([{
73
- type: "delete",
74
- key: loaded.project.parseKey(key),
75
- locales
76
- }]), options);
77
- }
78
- async function fill(options) {
79
- const loaded = await project(options);
80
- const p = loaded.project;
81
- const targets = options.locale === void 0 ? p.locales.filter((l) => l !== p.sourceLocale) : [options.locale].flat();
82
- const ops = [];
83
- for (const locale of targets) for (const key of p.keys({ missing: locale })) {
84
- if (p.message(key, locale) !== void 0) continue;
85
- ops.push({
86
- type: "set",
87
- key,
88
- locale,
89
- value: options.copySource ? p.message(key, p.sourceLocale)?.value ?? "" : ""
90
- });
91
- }
92
- await runPlan("fill", loaded, p.plan(ops), options);
93
- }
94
- async function format(options) {
95
- const loaded = await project(options);
96
- await runPlan("format", loaded, loaded.project.plan([{ type: "format" }]), options);
97
- }
98
- //#endregion
99
- //#region src/commands/check.ts
100
- function list(value) {
101
- return value === void 0 ? [] : [value].flat().flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
102
- }
103
- function filterDiagnostics(diagnostics, options) {
104
- const locales = list(options.locale);
105
- const only = list(options.only);
106
- const skip = list(options.skip);
107
- return diagnostics.filter((d) => locales.length === 0 || d.locale === void 0 || locales.includes(d.locale) || d.code === "missing-translation" && locales.some((l) => d.message.includes(l))).filter((d) => only.length === 0 || only.includes(d.code)).filter((d) => !skip.includes(d.code)).map((d) => options.strict && d.severity === "warning" ? {
108
- ...d,
109
- severity: "error"
110
- } : d);
111
- }
112
- function reportInputs(workspace, options) {
113
- return workspace.projects.map((loaded) => {
114
- const analysis = loaded.project.analyze();
115
- const diagnostics = filterDiagnostics(analysis.diagnostics, options);
116
- return {
117
- project: loaded.name,
118
- text: (path) => loaded.project.text(path),
119
- diagnostics,
120
- coverage: analysis.coverage,
121
- summary: {
122
- errors: diagnostics.filter((d) => d.severity === "error").length,
123
- warnings: diagnostics.filter((d) => d.severity === "warning").length,
124
- infos: diagnostics.filter((d) => d.severity === "info" || d.severity === "hint").length,
125
- files: new Set(diagnostics.flatMap((d) => d.location ? [d.location.path] : [])).size
126
- }
127
- };
128
- });
129
- }
130
- async function report(workspace, options) {
131
- const inputs = reportInputs(workspace, options);
132
- const reporterIds = list(options.reporter);
133
- const ids = reporterIds.length ? reporterIds : [options.json ? "json" : "pretty"];
134
- const outputs = list(options.output);
135
- const available = workspace.projects[0]?.project.reporters ?? /* @__PURE__ */ new Map();
136
- const roots = new Map(workspace.projects.map((p) => [p.name, p.root]));
137
- for (const [i, id] of ids.entries()) {
138
- const reporter = available.get(id);
139
- if (!reporter) throw new X18nError("X18N0001", `unknown reporter "${id}" (${[...available.keys()].join(", ")})`);
140
- const text = await reporter.report(inputs, {
141
- color: !outputs[i] && useColor(options) && id === "pretty",
142
- displayPath: (path, project) => displayPath(roots.get(project) ?? "", path)
143
- });
144
- if (outputs[i]) await writeFile(outputs[i], text);
145
- else if (text) process.stdout.write(text);
146
- }
147
- const errors = inputs.reduce((sum, input) => sum + input.summary.errors, 0);
148
- const warnings = inputs.reduce((sum, input) => sum + input.summary.warnings, 0);
149
- const tooManyWarnings = options.maxWarnings !== void 0 && warnings > Number(options.maxWarnings);
150
- return errors > 0 || tooManyWarnings ? 1 : 0;
151
- }
152
- async function check(options) {
153
- const workspace = await openWorkspace(options);
154
- const code = await report(workspace, options);
155
- if (!options.watch) return code;
156
- const rerun = async () => {
157
- if (process.stdout.isTTY) process.stdout.write("\x1Bc");
158
- out(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] File change detected. Checking...\n`);
159
- await report(workspace, options);
160
- };
161
- for (const loaded of workspace.projects) {
162
- loaded.project.watch();
163
- loaded.project.onChange(() => void rerun());
164
- }
165
- out("\nWatching for file changes.");
166
- return new Promise(() => {});
167
- }
168
- //#endregion
169
- //#region src/commands/extract.ts
170
- function isPlanItem(value) {
171
- if (!isJsonObject(value) || !isJsonObject(value.range)) return false;
172
- return [
173
- "path",
174
- "original",
175
- "key",
176
- "value",
177
- "replacement"
178
- ].every((field) => typeof value[field] === "string") && typeof value.range.start === "number" && typeof value.range.end === "number" && Array.isArray(value.imports) && value.imports.every((line) => typeof line === "string");
179
- }
180
- async function readPlan(file) {
181
- const data = JSON.parse(await readFile(file, "utf8"));
182
- if (!isJsonObject(data) || !Array.isArray(data.items) || !data.items.every(isPlanItem)) throw new X18nError("X18N4008", `${file} is not an extraction plan`);
183
- return data.items;
184
- }
185
- /** One question per string in a TTY: extract with the proposed key, a typed key, or skip. */
186
- async function review(candidates, root) {
187
- const rl = createInterface({
188
- input: process.stdin,
189
- output: process.stderr
190
- });
191
- const picked = [];
192
- try {
193
- for (const candidate of candidates) {
194
- process.stderr.write(`\n${displayPath(root, candidate.path)}:${candidate.line} ${JSON.stringify(candidate.text)}\n → ${candidate.replacement}\n`);
195
- const reply = (await rl.question(`Extract as "${candidate.key}"? [Y/n/key/q] `)).trim();
196
- if (reply === "q") break;
197
- if (reply === "n") continue;
198
- picked.push(reply && reply !== "y" ? {
199
- ...candidate,
200
- key: reply,
201
- replacement: candidate.replacement.replace(candidate.key, reply)
202
- } : candidate);
203
- }
204
- } finally {
205
- rl.close();
206
- }
207
- return picked;
208
- }
209
- async function extract(files, options) {
210
- const loaded = singleProject(await openWorkspace(options));
211
- const { project } = loaded;
212
- if (options.apply) {
213
- const items = extractItems(project, await readPlan(options.apply));
214
- await runPlan("extract", loaded, project.plan([{
215
- type: "extract",
216
- items
217
- }]), options);
218
- return;
219
- }
220
- const candidates = findCandidates(project, { files });
221
- if (options.plan) {
222
- await writeFile(options.plan, `${JSON.stringify(toPlanFile(project, candidates), null, 2)}\n`);
223
- if (options.json) printJson("extract", {
224
- plan: options.plan,
225
- items: candidates.length
226
- });
227
- else out(`Wrote ${candidates.length} strings to ${options.plan}. Edit keys and replacements, then run \`x18n extract --apply ${options.plan}\`.`);
228
- return;
229
- }
230
- const interactive = !options.list && !options.yes && !options.json && process.stdin.isTTY;
231
- if (options.list || !interactive && !options.yes) {
232
- if (options.json) {
233
- printJson("extract", { items: candidates.map((c) => ({
234
- ...c,
235
- file: displayPath(loaded.root, c.path)
236
- })) });
237
- return;
238
- }
239
- for (const c of candidates) out(`${displayPath(loaded.root, c.path)}:${c.line}\t${c.kind}\t${JSON.stringify(c.text)}\t→ ${c.key}${c.reused ? " (existing)" : ""}`);
240
- out(`${candidates.length} strings. Apply with \`x18n extract --yes\`, or review with \`x18n extract --plan plan.json\`.`);
241
- return;
242
- }
243
- const chosen = interactive ? await review(candidates, loaded.root) : candidates;
244
- if (chosen.length === 0) {
245
- out("Nothing to extract.");
246
- return;
247
- }
248
- await runPlan("extract", loaded, project.plan([{
249
- type: "extract",
250
- items: extractItems(project, chosen)
251
- }]), options);
252
- }
253
- //#endregion
254
- //#region src/commands/look.ts
255
- function percent(coverage) {
256
- return coverage.total === 0 ? 100 : Math.floor(coverage.translated / coverage.total * 1e3) / 10;
257
- }
258
- function summaryOf(loaded) {
259
- const diagnostics = loaded.project.analyze().diagnostics;
260
- return {
261
- errors: diagnostics.filter((d) => d.severity === "error").length,
262
- warnings: diagnostics.filter((d) => d.severity === "warning").length,
263
- infos: diagnostics.filter((d) => d.severity === "info" || d.severity === "hint").length
264
- };
265
- }
266
- function statusData(loaded) {
267
- const { project } = loaded;
268
- return {
269
- name: loaded.name,
270
- root: displayPath(loaded.root, ""),
271
- sourceLocale: project.sourceLocale,
272
- locales: project.analyze().coverage.map((c) => ({
273
- locale: c.locale,
274
- total: c.total,
275
- translated: c.translated,
276
- missing: c.missing.length,
277
- empty: c.empty.length,
278
- percent: percent(c)
279
- })),
280
- resources: project.resources.length,
281
- keys: project.keys().length,
282
- usages: project.usages().length,
283
- summary: summaryOf(loaded)
284
- };
285
- }
286
- async function status(options) {
287
- const workspace = await openWorkspace(options);
288
- if (options.json) {
289
- printJson("status", { projects: workspace.projects.map(statusData) });
290
- return;
291
- }
292
- for (const loaded of workspace.projects) {
293
- const { project } = loaded;
294
- const summary = summaryOf(loaded);
295
- out(`${loaded.name} (${displayPath(loaded.root, "")})`);
296
- out(` ${project.resources.length} resources, ${project.keys().length} keys, ${project.usages().length} usages`);
297
- for (const c of project.analyze().coverage) {
298
- const info = project.localeInfo(c.locale);
299
- const label = `${c.locale}${c.locale === project.sourceLocale ? " (source)" : ""}`;
300
- out(` ${info.flag ?? " "} ${label.padEnd(16)} ${String(percent(c)).padStart(5)}% ${c.translated}/${c.total}${c.missing.length ? `, ${c.missing.length} missing` : ""}${c.empty.length ? `, ${c.empty.length} empty` : ""}`);
301
- }
302
- out(` ${summary.errors} errors, ${summary.warnings} warnings`);
303
- }
304
- }
305
- /** One page of a list: `--limit` items from `--cursor`, plus the cursor of the next page. */
306
- function page(items, options) {
307
- const start = Number(options.cursor ?? 0) || 0;
308
- const end = options.limit ? start + Number(options.limit) : items.length;
309
- return {
310
- items: items.slice(start, end),
311
- total: items.length,
312
- nextCursor: end < items.length ? String(end) : void 0
313
- };
314
- }
315
- async function keys(glob, options) {
316
- const workspace = await openWorkspace(options);
317
- const projects = workspace.projects.map((loaded) => {
318
- const { project } = loaded;
319
- const { items, total, nextCursor } = page(project.keys({
320
- glob,
321
- missing: options.missing,
322
- unused: options.unused,
323
- namespace: options.namespace
324
- }), options);
325
- const values = (key) => Object.fromEntries(project.locales.map((locale) => [locale, project.message(key, locale)?.value]));
326
- return {
327
- name: loaded.name,
328
- total,
329
- nextCursor,
330
- keys: items.map((key) => options.values ? {
331
- key,
332
- values: values(key)
333
- } : { key })
334
- };
335
- });
336
- if (options.json) {
337
- printJson("keys", { projects });
338
- return;
339
- }
340
- for (const p of projects) {
341
- if (workspace.projects.length > 1) out(`# ${p.name}`);
342
- for (const item of p.keys) out("values" in item ? `${item.key}\t${JSON.stringify(item.values)}` : item.key);
343
- if (p.nextCursor) out(`… ${p.total - Number(p.nextCursor)} more (--cursor ${p.nextCursor})`);
344
- }
345
- }
346
- function locate(loaded, path, start, end) {
347
- const range = toWireRange(loaded.project.text(path), start, end);
348
- return {
349
- file: displayPath(loaded.root, path),
350
- line: range.line,
351
- column: range.column
352
- };
353
- }
354
- async function get(input, options) {
355
- const workspace = await openWorkspace(options);
356
- const projects = workspace.projects.flatMap((loaded) => {
357
- const { project } = loaded;
358
- const info = project.get(project.parseKey(input));
359
- if (!info) return [];
360
- const resourceOf = (id) => project.resources.find((r) => r.id === id)?.path ?? id;
361
- return [{
362
- name: loaded.name,
363
- key: info.key,
364
- namespace: info.namespace,
365
- messages: info.messages.map((m) => ({
366
- locale: m.locale,
367
- value: m.value,
368
- ...locate(loaded, resourceOf(m.resource), m.range?.key.start ?? 0, m.range?.key.end ?? 0)
369
- })),
370
- missing: info.missing,
371
- usages: info.usages.map((u) => ({
372
- possible: u.possible,
373
- raw: u.usage.raw,
374
- ...locate(loaded, u.usage.path, u.usage.range.start, u.usage.range.end)
375
- })),
376
- diagnostics: wireDiagnostics(loaded, info.diagnostics)
377
- }];
378
- });
379
- if (options.json) {
380
- printJson("get", { projects });
381
- return;
382
- }
383
- if (projects.length === 0) out(`"${input}" is not defined.`);
384
- for (const p of projects) {
385
- out(`${p.key}${workspace.projects.length > 1 ? ` (${p.name})` : ""}`);
386
- for (const m of p.messages) out(` ${m.locale.padEnd(8)} ${JSON.stringify(m.value)} ${m.file}:${m.line}:${m.column}`);
387
- if (p.missing.length) out(` missing in ${p.missing.join(", ")}`);
388
- out(` ${p.usages.length} usages`);
389
- for (const u of p.usages) out(` ${u.file}:${u.line}:${u.column}${u.possible ? " (possible)" : ""}`);
390
- for (const d of p.diagnostics) out(` ${d.severity} ${d.code}: ${d.message}`);
391
- }
392
- }
393
- async function usages(input, options) {
394
- const projects = (await openWorkspace(options)).projects.map((loaded) => {
395
- const { project } = loaded;
396
- const { items, total, nextCursor } = page(project.usages({
397
- key: input === void 0 ? void 0 : project.parseKey(input),
398
- path: options.file
399
- }), options);
400
- return {
401
- name: loaded.name,
402
- total,
403
- nextCursor,
404
- usages: items.map((m) => ({
405
- key: m.usage.key,
406
- raw: m.usage.raw,
407
- possible: m.possible,
408
- dynamic: m.usage.dynamic,
409
- ...locate(loaded, m.usage.path, m.usage.range.start, m.usage.range.end)
410
- }))
411
- };
412
- });
413
- if (options.json) {
414
- printJson("usages", { projects });
415
- return;
416
- }
417
- for (const p of projects) for (const u of p.usages) out(`${u.file}:${u.line}:${u.column}\t${u.key ?? u.raw}${u.possible ? " (possible)" : ""}`);
418
- }
419
- async function locales(options) {
420
- const projects = (await openWorkspace(options)).projects.map((loaded) => ({
421
- name: loaded.name,
422
- sourceLocale: loaded.project.sourceLocale,
423
- locales: loaded.project.locales.map((code) => ({
424
- ...loaded.project.localeInfo(code),
425
- resources: loaded.project.resources.filter((r) => r.locale === code).map((r) => displayPath(loaded.root, r.path))
426
- }))
427
- }));
428
- if (options.json) {
429
- printJson("locales", { projects });
430
- return;
431
- }
432
- for (const p of projects) for (const l of p.locales) out(`${l.flag ?? " "} ${l.code.padEnd(10)} ${(l.displayName ?? "").padEnd(28)} ${l.resources.join(", ")}`);
433
- }
434
- async function doctor(options) {
435
- const projects = (await openWorkspace(options)).projects.map((loaded) => ({
436
- ...loaded.project.explain(),
437
- root: displayPath(loaded.root, ""),
438
- configFile: loaded.configFile && displayPath(loaded.configFile, "")
439
- }));
440
- if (options.json) {
441
- printJson("doctor", { projects });
442
- return;
443
- }
444
- for (const e of projects) {
445
- out(`# ${e.name} (${e.root})`);
446
- out(`config: ${e.configFile ?? "none (detection only)"}`);
447
- out(`presets:`);
448
- for (const p of e.presets) out(` ${p.reason === "detected" || p.reason === "listed" ? "✓" : "·"} ${p.name} (${p.reason})`);
449
- out("finders:");
450
- for (const f of e.finders) out(` ${f.id}: ${f.resources.length} resources${f.resources.length ? `\n ${f.resources.slice(0, 20).join("\n ")}` : ""}`);
451
- out("formats:");
452
- for (const f of e.formats.filter((f) => f.resources > 0)) out(` ${f.id} (${f.extensions.join(", ")}): ${f.resources} resources`);
453
- out("scanners:");
454
- for (const s of e.scanners) out(` ${s.id} (${s.extensions.join(", ") || "none"}): ${s.files} files`);
455
- if (e.conflicts.length) out(`conflicts:\n${e.conflicts.map((c) => ` .${c.extension}: ${c.winner} wins over ${c.loser}`).join("\n")}`);
456
- if (e.errors.length) out(`errors:\n${e.errors.map((err) => ` ${err.code} ${err.message}`).join("\n")}`);
457
- out("config origins:");
458
- for (const [key, origin] of Object.entries(e.config.origins)) out(` ${key}: ${origin}`);
459
- }
460
- }
461
- //#endregion
462
- //#region src/commands/migrate.ts
463
- async function migrate(source, options) {
464
- if (source !== "i18n-ally") throw new X18nError("X18N0001", `unknown migration source "${source}" (i18n-ally)`);
465
- const root = resolve(options.root ?? cwd());
466
- const result = await migrateI18nAlly(root, { format: options.format });
467
- const existing = findConfigFile(root);
468
- if (existing && !options.force && !options.dryRun && result.files.length > 0) throw new X18nError("X18N0001", `${existing} already exists (pass --force to replace it)`);
469
- if (!options.dryRun) for (const file of result.files) {
470
- const path = join(root, file.path);
471
- await mkdir(dirname(path), { recursive: true });
472
- if (file.path === ".x18n/reviews.yml" && existsSync(path)) continue;
473
- await writeFile(path, file.content);
474
- }
475
- if (options.json) {
476
- printJson("migrate", {
477
- ...result,
478
- written: !options.dryRun
479
- });
480
- return;
481
- }
482
- for (const file of result.files) out(options.dryRun ? `--- ${file.path}\n${file.content}` : `wrote ${file.path}`);
483
- for (const note of result.notes) out(`note: ${note}`);
484
- if (result.removable.length) out(`These i18n-ally settings can be removed now:\n${result.removable.map((key) => ` ${key}`).join("\n")}`);
485
- }
486
- //#endregion
487
- //#region src/commands/prompt.ts
488
- async function prompt(kind, options) {
489
- if (kind !== "translate" && kind !== "review") throw new X18nError("X18N0001", `unknown prompt "${kind}" (translate, review)`);
490
- const locale = kind === "translate" ? options.to : options.locale ?? options.to;
491
- if (!locale) throw new X18nError("X18N0001", kind === "translate" ? "pass --to <locale>" : "pass --locale <locale>");
492
- const { project } = singleProject(await openWorkspace(options));
493
- const data = project.prompt(kind, locale, {
494
- keys: options.keys,
495
- namespace: options.namespace,
496
- limit: options.limit === void 0 ? void 0 : Number(options.limit),
497
- cursor: options.cursor,
498
- context: options.context
499
- });
500
- if (options.json || options.format === "json") printJson(`prompt ${kind}`, data);
501
- else out(renderPrompt(data));
502
- }
503
- //#endregion
504
- //#region src/commands/setup.ts
505
- async function schema(name, options) {
506
- const which = name ?? "config";
507
- if (which !== "config" && which !== "plugin") throw new X18nError("X18N0001", `unknown schema "${which}" (config, plugin)`);
508
- const require = createRequire(import.meta.url);
509
- const text = await readFile(require.resolve(`@x18n/core/schema/${which}.json`), "utf8");
510
- if (options.json) printJson("schema", {
511
- name: which,
512
- schema: JSON.parse(text)
513
- });
514
- else out(text);
515
- }
516
- async function init(options) {
517
- const root = resolve(options.root ?? cwd());
518
- const existing = findConfigFile(root);
519
- if (existing && !options.force) throw new X18nError("X18N0001", `${existing} already exists (pass --force to replace it)`);
520
- const { project } = (await loadWorkspace({
521
- cwd: root,
522
- cache: false
523
- })).projects[0];
524
- const explanation = project.explain();
525
- const detected = explanation.presets.filter((p) => p.reason === "detected" && p.name !== "general").map((p) => p.name);
526
- const resources = explanation.config.resources.length > 0 ? explanation.config.resources.map((r) => r.pattern) : [...new Set(project.resources.map((r) => r.path.replace(new RegExp(`(^|/)${r.locale}(?=[./])`), "$1{locale}").replace(/\.[^.]+$/, ".{ext}")))];
527
- const config = {
528
- sourceLocale: project.sourceLocale,
529
- resources: resources.map((pattern) => ({ pattern }))
530
- };
531
- const format = options.format ?? "ts";
532
- let file;
533
- let content;
534
- if (format === "yml" || format === "yaml") {
535
- file = join(root, "x18n.config.yml");
536
- content = `# yaml-language-server: $schema=https://x18n.antfu.dev/schema/config.json\n${stringify(config)}`;
537
- } else if (format === "json") {
538
- file = join(root, "x18n.config.json");
539
- content = `${JSON.stringify({
540
- $schema: "https://x18n.antfu.dev/schema/config.json",
541
- ...config
542
- }, null, 2)}\n`;
543
- } else {
544
- file = join(root, "x18n.config.ts");
545
- const body = JSON.stringify(config, null, 2).replace(/"(\w+)":/g, "$1:").replace(/"/g, "'");
546
- content = `import { defineConfig } from '@x18n/core'\n\n// Detected presets: ${detected.join(", ") || "none"}. They apply automatically.\nexport default defineConfig(${body})\n`;
547
- }
548
- if (!configFileNames.some((name) => file.endsWith(name))) throw new X18nError("X18N0001", `unknown format "${format}" (ts, yml, json)`);
549
- await writeFile(file, content);
550
- if (options.json) printJson("init", {
551
- file,
552
- presets: detected,
553
- config
554
- });
555
- else out(`Wrote ${file}\nDetected presets: ${detected.join(", ") || "none"}\nLocales: ${project.locales.join(", ")}`);
556
- }
557
- const literal = (value) => JSON.stringify(value, null, 2).replace(/"([a-z_$][\w$]*)":/gi, "$1:");
558
- /** The TS plugin equivalent of a YAML plugin, using the same built-in factories. */
559
- function ejectPlugin(data) {
560
- const lines = [];
561
- const configLines = [];
562
- if (data.config) configLines.push(` Object.assign(config, ${literal(data.config).replace(/\n/g, "\n ")})`);
563
- if (data.scanners?.calls || data.scanners?.regex) {
564
- configLines.push(` config.usage = {`, ` ...config.usage,`);
565
- if (data.scanners.calls) configLines.push(` calls: [...config.usage.calls, ...${literal(data.scanners.calls).replace(/\n/g, "\n ")}],`);
566
- if (data.scanners.regex) configLines.push(` regex: [...config.usage.regex, ...${literal(data.scanners.regex).replace(/\n/g, "\n ")}],`);
567
- configLines.push(` }`);
568
- }
569
- const fields = [` name: ${literal(data.name)},`];
570
- if (configLines.length) fields.push(` config(config) {`, ...configLines, ` },`);
571
- if (data.derivedKeys) fields.push(` derivedKeys: ${literal(data.derivedKeys)},`);
572
- if (data.placeholders) fields.push(` placeholders: ${literal(data.placeholders)}.map(placeholderFromShape),`);
573
- if (data.normalizeKey) {
574
- const rules = data.normalizeKey.map((rule) => `.replace(new RegExp(${literal(rule.replace.pattern)}, 'g'), ${literal(rule.replace.with)})`).join("");
575
- fields.push(` normalizeKey: raw => raw${rules},`);
576
- }
577
- if (data.localeInfo) fields.push(` localeInfo: code => (${literal(data.localeInfo)} as Record<string, { flag?: string, displayName?: string }>)[code],`);
578
- const imports = ["definePlugin", ...data.detect ? ["definePreset"] : []];
579
- lines.push(`import { ${imports.join(", ")} } from '@x18n/core'`);
580
- if (data.placeholders) lines.push(`import { placeholderFromShape } from '@x18n/builtins/placeholders'`);
581
- if (data.scopes) lines.push(`import { regexScopes } from '@x18n/builtins/scanners'`);
582
- lines.push("", `const plugin = definePlugin({`, ...fields, `})`, "");
583
- const plugins = `[plugin${data.scopes ? `, regexScopes(${literal(data.scopes.regex)})` : ""}]`;
584
- if (data.detect) {
585
- const any = data.detect.dependencies?.any;
586
- lines.push(`export default definePreset({`, ` name: ${literal(data.name)},`, ...data.exclusive ? [` exclusive: true,`] : [], ` detect: ctx => ${any ? `ctx.hasAnyDependency(${literal(any)})` : "true"},`, ` plugins: ${plugins},`, `})`);
587
- } else lines.push(data.scopes ? `export default ${plugins}` : "export default plugin");
588
- return `${lines.join("\n")}\n`;
589
- }
590
- async function eject(file, options) {
591
- const path = resolve(cwd(), file);
592
- if (!existsSync(path)) throw new X18nError("X18N1001", path);
593
- const result = validate(PluginFileSchema, parse(await readFile(path, "utf8")));
594
- if (!result.success) throw new X18nError("X18N1004", result.issues.map((i) => `${i.path}: ${i.message}`).join("; "));
595
- const code = ejectPlugin(result.output);
596
- if (options.json) printJson("plugin eject", {
597
- file: path,
598
- code
599
- });
600
- else out(code);
601
- }
602
- //#endregion
603
- //#region src/cli.ts
604
- const uiCommands = () => import("./ui-Bp3xyqr_.mjs");
605
- /** Runs a command and turns errors into the JSON envelope or a printed hint, with the matching exit code. */
606
- function run(command, action) {
607
- return async (...args) => {
608
- const options = args.at(-1);
609
- try {
610
- const code = await action(...args);
611
- process.exitCode = code ?? 0;
612
- } catch (error) {
613
- const info = errorInfo(error);
614
- if (options.json) printJson(command, null, [], [info]);
615
- else {
616
- process.stderr.write(`error ${info.code}: ${info.message}\n`);
617
- if (info.hint) process.stderr.write(` ${info.hint}\n`);
618
- if (!(error instanceof Error && "code" in error) && error instanceof Error && process.env.DEBUG) process.stderr.write(`${error.stack}\n`);
619
- }
620
- process.exitCode = exitCodeOf(error);
621
- }
622
- };
623
- }
624
- function createCli() {
625
- const cli = cac("x18n");
626
- cli.option("--root <dir>", "Project root (default: nearest config upward from the cwd)").option("--config <file>", "Explicit config file").option("--project <name>", "Pick one project in a monorepo").option("--json", "Print the JSON contract instead of human output").option("--no-cache", "Ignore the persistent cache").option("--yes", "Accept default answers for decisions");
627
- cli.command("", "Show the status and the main commands").action(run("status", async (options) => {
628
- await status(options);
629
- if (!options.json) process.stdout.write("\nCommands: check, keys, get, usages, set, apply, rename, delete, fill, prompt translate, ui. Run `x18n --help` for all.\n");
630
- }));
631
- cli.command("status", "Locales with coverage, resources, keys and diagnostics").action(run("status", status));
632
- cli.command("keys [glob]", "List keys").option("--missing <locale>", "Only keys missing or empty in a locale").option("--unused", "Only keys without usages").option("--namespace <ns>", "Only keys of a namespace").option("--values", "Include the value per locale").option("--limit <n>", "Page size").option("--cursor <cursor>", "Start of the page (from `nextCursor`)").action(run("keys", keys));
633
- cli.command("get <key>", "Values, resources, usages and diagnostics of one key").action(run("get", get));
634
- cli.command("usages [key]", "Usages of a key, or all usages").option("--file <path>", "Only usages in a file").option("--limit <n>", "Page size").option("--cursor <cursor>", "Start of the page").action(run("usages", usages));
635
- cli.command("locales", "Locale codes as found, with display names and resources").action(run("locales", locales));
636
- cli.command("doctor", "How the project was understood: presets, finders, formats, scanners, config origins").action(run("doctor", doctor));
637
- cli.command("check", "Report invalid, untranslated and unused keys, like `tsc --noEmit`").option("--watch", "Re-check on change").option("--strict", "Turn every warning into an error").option("--locale <locale>", "Only report for these locales (repeatable)").option("--only <checks>", "Only these check ids (comma separated)").option("--skip <checks>", "Skip these check ids (comma separated)").option("--max-warnings <n>", "Fail when there are more warnings").option("--reporter <id>", "pretty, json, markdown, github or sarif (repeatable)").option("--output <file>", "Write the matching reporter output to a file (repeatable)").action(run("check", check));
638
- cli.command("set <key>", "Set one value").option("--locale <locale>", "Locale (default: source locale)").option("--value <value>", "The value; `-` reads stdin").option("--dry-run", "Print the plan instead of writing").action(run("set", set));
639
- cli.command("apply <file>", "Batch set from JSON `{ \"<locale>\": { \"<key>\": \"<value>\" } }`; `-` reads stdin").option("--force", "Write values with placeholder problems").option("--dry-run", "Print the plan instead of writing").action(run("apply", apply));
640
- cli.command("rename <from> <to>", "Rename a key (or every key under a parent) in all locales and usages").option("--no-usages", "Keep code unchanged").option("--dry-run", "Print the plan instead of writing").action(run("rename", rename));
641
- cli.command("delete <key>", "Delete a key (or every key under a parent) in all locales").option("--locale <locale>", "Only in these locales (repeatable)").option("--dry-run", "Print the plan instead of writing").action(run("delete", remove));
642
- cli.command("fill", "Add missing keys with empty values").option("--locale <locale>", "Only these locales (repeatable)").option("--copy-source", "Use the source value instead of an empty string").option("--dry-run", "Print the plan instead of writing").action(run("fill", fill));
643
- cli.command("format", "Re-write resources with the configured sort and indent").option("--dry-run", "Print the plan instead of writing").action(run("format", format));
644
- cli.command("extract [...files]", "Find hard-coded strings and turn them into keys").option("--list", "Print candidates and proposed keys").option("--plan <file>", "Write the extraction plan to a file to edit").option("--apply <file>", "Apply an (edited) extraction plan").option("--dry-run", "Print the plan instead of writing").action(run("extract", extract));
645
- cli.command("prompt <kind>", "Print a prompt for an agent or LLM: `prompt translate --to <locale>` or `prompt review --locale <locale>`").option("--to <locale>", "Target locale (translate)").option("--locale <locale>", "Locale to review").option("--keys <glob>", "Only keys matching a glob").option("--namespace <ns>", "Only keys of a namespace").option("--limit <n>", "Messages per prompt (default 50)").option("--cursor <cursor>", "Continue from `nextCursor`").option("--format <format>", "markdown (default) or json").option("--no-context", "Leave out code around usages").action(run("prompt", prompt));
646
- cli.command("ui", "Start the UI: browse and edit messages in the browser, with HTTP MCP at /__mcp").option("--port <port>", "Port (default: a free one from 9999)").option("--host <host>", "Bind host (default: localhost)").option("--no-open", "Do not open the browser").option("--no-auth", "Skip authentication (CI and e2e only)").action(run("ui", async (options) => (await uiCommands()).ui(options)));
647
- cli.command("build", "Write a static, read-only report of the UI").option("--out-dir <dir>", "Output directory (default: .x18n/report)").action(run("build", async (options) => (await uiCommands()).build(options)));
648
- cli.command("mcp", "Serve the MCP tools over stdio").action(run("mcp", async (options) => (await uiCommands()).mcp(options)));
649
- cli.command("init", "Detect the setup and write x18n.config.ts").option("--format <format>", "ts (default), yml or json").option("--force", "Replace an existing config").action(run("init", init));
650
- cli.command("migrate <source>", "Convert i18n-ally settings: `migrate i18n-ally`").option("--format <format>", "ts (default), yml or json").option("--dry-run", "Print the files instead of writing").option("--force", "Replace an existing config").action(run("migrate", migrate));
651
- cli.command("schema [name]", "Print a JSON Schema: config (default) or plugin").action(run("schema", schema));
652
- cli.command("plugin <action> <file>", "Plugin tools: `plugin eject <file.yml>` prints the TS equivalent").action(run("plugin eject", async (action, file, options) => {
653
- if (action !== "eject") throw new Error(`unknown plugin action "${action}"`);
654
- await eject(file, options);
655
- }));
656
- cli.help();
657
- cli.version(version);
658
- return cli;
659
- }
660
- process.stdout.on("error", (error) => {
661
- if (error.code === "EPIPE") process.exit(0);
662
- throw error;
663
- });
664
- const argv = process.argv.map((arg) => arg === "-" ? stdinArg : arg);
665
- try {
666
- createCli().parse(argv);
667
- } catch (error) {
668
- process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`);
669
- process.exitCode = 2;
670
- }
671
- //#endregion
672
- export { createCli };
@@ -1,148 +0,0 @@
1
- import { X18nError, applyTextEdits, envelope, isX18nError, toWireDiagnostic } from "@x18n/core";
2
- import process from "node:process";
3
- import { Buffer } from "node:buffer";
4
- import { readFile } from "node:fs/promises";
5
- import { join, relative } from "node:path";
6
- import { createInterface } from "node:readline/promises";
7
- import { loadWorkspace, stderrLogger } from "@x18n/node";
8
- //#region src/context.ts
9
- const cwd = () => process.cwd();
10
- function useColor(options) {
11
- if (options.json || process.env.NO_COLOR) return false;
12
- if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
13
- return process.stdout.isTTY === true;
14
- }
15
- async function openWorkspace(options) {
16
- return loadWorkspace({
17
- cwd: options.root ?? cwd(),
18
- configFile: options.config,
19
- upward: options.root === void 0,
20
- project: options.project,
21
- cache: options.cache !== false,
22
- logger: stderrLogger
23
- });
24
- }
25
- /** Write commands work on one project; a monorepo needs `--project`. */
26
- function singleProject(workspace) {
27
- if (workspace.projects.length !== 1) throw new X18nError("X18N0003", `pick one with --project (${workspace.projects.map((p) => p.name).join(", ")})`);
28
- return workspace.projects[0];
29
- }
30
- function displayPath(root, path) {
31
- return relative(cwd(), join(root, path)).replace(/\\/g, "/") || ".";
32
- }
33
- function out(text) {
34
- process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
35
- }
36
- function printJson(command, data, diagnostics = [], errors = []) {
37
- const value = envelope(command, data, diagnostics, errors);
38
- out(JSON.stringify(value, null, 2));
39
- }
40
- function wireDiagnostics(loaded, diagnostics) {
41
- return diagnostics.map((d) => toWireDiagnostic(loaded.name, d, (path) => loaded.project.text(path)));
42
- }
43
- /** Exit code for an error: 2 usage or config, 3 decision needed, 4 stale plan, 1 otherwise. */
44
- function exitCodeOf(error) {
45
- if (!isX18nError(error)) return 2;
46
- if (error.code === "X18N4003") return 3;
47
- if (error.code === "X18N4004") return 4;
48
- return /^X18N[01]/.test(error.code) ? 2 : 1;
49
- }
50
- function errorInfo(error) {
51
- if (isX18nError(error)) return error.toJSON();
52
- return {
53
- code: "X18N0002",
54
- message: error instanceof Error ? error.message : String(error),
55
- hint: "",
56
- docs: ""
57
- };
58
- }
59
- /** Stands for a lone `-` argument (stdin). */
60
- const stdinArg = "\0stdin";
61
- async function readInput(source) {
62
- if (source !== "-" && source !== "\0stdin") return readFile(source, "utf8");
63
- const chunks = [];
64
- for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
65
- return Buffer.concat(chunks).toString("utf8");
66
- }
67
- /** Answers for the plan's decisions: defaults with `--yes`, a prompt in a TTY, else an error. */
68
- async function answer(plan, options) {
69
- const answers = {};
70
- if (plan.decisions.length === 0) return answers;
71
- if (options.yes) {
72
- for (const decision of plan.decisions) answers[decision.id] = decision.default;
73
- return answers;
74
- }
75
- if (options.json || !process.stdin.isTTY) throw new X18nError("X18N4003", plan.decisions.map((d) => `${d.message} (${d.options.map((o) => o.id).join(" | ")})`).join(" "), plan.decisions.map((d) => d.id));
76
- const rl = createInterface({
77
- input: process.stdin,
78
- output: process.stderr
79
- });
80
- try {
81
- for (const decision of plan.decisions) {
82
- process.stderr.write(`${decision.message}\n${decision.options.map((o, i) => ` ${i + 1}) ${o.label}`).join("\n")}\n`);
83
- const reply = await rl.question(`Choose [1]: `);
84
- const option = decision.options[Number(reply || "1") - 1] ?? decision.options[0];
85
- answers[decision.id] = option.id;
86
- }
87
- } finally {
88
- rl.close();
89
- }
90
- return answers;
91
- }
92
- /** Changes as a small line diff, for `--dry-run` and the summary after a write. */
93
- function formatPlan(project, root, plan) {
94
- const lines = [];
95
- for (const change of plan.changes) {
96
- const path = displayPath(root, change.path);
97
- if (change.type === "create") {
98
- lines.push(`create ${path}`, ...change.content.split("\n").filter(Boolean).map((line) => `+ ${line}`));
99
- continue;
100
- }
101
- if (change.type === "delete") {
102
- lines.push(`delete ${path}`);
103
- continue;
104
- }
105
- const before = project.text(change.path) ?? "";
106
- const after = applyTextEdits(before, change.edits);
107
- const a = before.split("\n");
108
- const b = after.split("\n");
109
- let start = 0;
110
- while (start < a.length && start < b.length && a[start] === b[start]) start++;
111
- let endA = a.length;
112
- let endB = b.length;
113
- while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
114
- endA--;
115
- endB--;
116
- }
117
- lines.push(`edit ${path}`, ...a.slice(start, endA).map((line) => `- ${line}`), ...b.slice(start, endB).map((line) => `+ ${line}`));
118
- }
119
- for (const warning of plan.warnings) lines.push(`warning: ${warning}`);
120
- return lines.length ? lines.join("\n") : "No changes.";
121
- }
122
- function planData(root, plan, applied) {
123
- return {
124
- applied,
125
- keys: plan.keys,
126
- warnings: plan.warnings,
127
- decisions: plan.decisions,
128
- changes: plan.changes.map((change) => ({
129
- ...change,
130
- file: displayPath(root, change.path)
131
- }))
132
- };
133
- }
134
- /** Plans, asks, then applies (or prints with `--dry-run`). */
135
- async function runPlan(command, loaded, plan, options) {
136
- if (options.dryRun) {
137
- if (options.json) printJson(command, planData(loaded.root, plan, false));
138
- else out(formatPlan(loaded.project, loaded.root, plan));
139
- return;
140
- }
141
- const answers = await answer(plan, options);
142
- const preview = formatPlan(loaded.project, loaded.root, plan);
143
- await loaded.project.apply(plan, { answers });
144
- if (options.json) printJson(command, planData(loaded.root, plan, true));
145
- else out(preview);
146
- }
147
- //#endregion
148
- export { openWorkspace as a, readInput as c, stdinArg as d, useColor as f, exitCodeOf as i, runPlan as l, displayPath as n, out as o, wireDiagnostics as p, errorInfo as r, printJson as s, cwd as t, singleProject as u };
package/dist/index.d.mts DELETED
@@ -1,2 +0,0 @@
1
- import { Check, Format, LocaleFinder, Plugin, Preset, Reporter, Scanner, UserConfig, defineConfig, definePlugin, definePreset } from "@x18n/core";
2
- export { type Check, type Format, type LocaleFinder, type Plugin, type Preset, type Reporter, type Scanner, type UserConfig, defineConfig, definePlugin, definePreset };
package/dist/index.mjs DELETED
@@ -1 +0,0 @@
1
- export { defineConfig, definePlugin, definePreset } from "@x18n/core";
@@ -1 +0,0 @@
1
- export * from "@x18n/builtins/presets";
package/dist/presets.mjs DELETED
@@ -1,2 +0,0 @@
1
- export * from "@x18n/builtins/presets";
2
- export {};
@@ -1,74 +0,0 @@
1
- import { a as openWorkspace, o as out, s as printJson, t as cwd } from "./context-Za_ea1Nk.mjs";
2
- import process from "node:process";
3
- import { join, resolve } from "node:path";
4
- import { createX18nDevframe } from "@x18n/ui";
5
- import { createBuild } from "devframe/adapters/build";
6
- import { createDevServer } from "devframe/adapters/dev";
7
- import { createMcpServer } from "devframe/adapters/mcp";
8
- import { buildOtpAuthUrl, getTempAuthCodeInfo } from "devframe/node/auth";
9
- import { randomToken } from "devframe/utils/crypto-token";
10
- import { launchEditor } from "devframe/utils/launch-editor";
11
- //#region src/commands/ui.ts
12
- /** Starts the UI dev server and keeps running until interrupted. */
13
- async function ui(options) {
14
- const workspace = await openWorkspace(options);
15
- workspace.projects.forEach((p) => p.project.watch());
16
- const editor = process.env.LAUNCH_EDITOR || process.env.VISUAL || process.env.EDITOR;
17
- const definition = createX18nDevframe(workspace.projects, { openInEditor: editor ? async ({ project, path, line, column }) => launchEditor(`${join(project.root, path)}:${line}:${column}`, editor) : void 0 });
18
- const mcpToken = randomToken();
19
- const auth = options.auth !== false;
20
- if (!auth) process.stderr.write("warning: --no-auth lets anything that reaches the port edit your files. Use it only in CI and e2e tests.\n");
21
- const server = await createDevServer(definition, {
22
- host: options.host ?? "localhost",
23
- ...options.port === void 0 ? {} : { port: Number(options.port) },
24
- auth,
25
- openBrowser: options.open !== false,
26
- mcp: { authorization: mcpToken }
27
- });
28
- const url = `${server.origin}/`;
29
- const code = auth ? getTempAuthCodeInfo().code : void 0;
30
- const mcp = {
31
- url: `${url}__mcp`,
32
- token: mcpToken
33
- };
34
- if (options.json) printJson("ui", {
35
- url,
36
- code,
37
- link: code && buildOtpAuthUrl(url, code),
38
- mcp
39
- });
40
- else {
41
- out(`x18n UI at ${url}`);
42
- if (code) out(` auth code ${code}\n or open ${buildOtpAuthUrl(url, code)}`);
43
- out(` MCP ${mcp.url}\n MCP token Authorization: Bearer ${mcp.token}`);
44
- }
45
- const stop = () => {
46
- server.close().finally(() => process.exit(0));
47
- };
48
- process.once("SIGINT", stop);
49
- process.once("SIGTERM", stop);
50
- }
51
- async function build(options) {
52
- const workspace = await openWorkspace(options);
53
- const outDir = resolve(cwd(), options.outDir ?? ".x18n/report");
54
- const log = console.log;
55
- console.log = console.error;
56
- try {
57
- await createBuild(createX18nDevframe(workspace.projects), { outDir });
58
- } finally {
59
- console.log = log;
60
- }
61
- if (options.json) printJson("build", { outDir });
62
- else out(`Wrote the report to ${outDir}`);
63
- }
64
- /** MCP over stdio: stdout is the transport, so nothing else may print there. */
65
- async function mcp(options) {
66
- const workspace = await openWorkspace(options);
67
- workspace.projects.forEach((p) => p.project.watch());
68
- await createMcpServer(createX18nDevframe(workspace.projects), {
69
- transport: "stdio",
70
- serverName: "x18n"
71
- });
72
- }
73
- //#endregion
74
- export { build, mcp, ui };