@x18n/cli 0.0.0 → 0.0.3
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/dist/cli.mjs +25 -8
- package/package.json +5 -5
- package/skills/x18n/SKILL.md +70 -27
- package/skills/x18n/references/migrate-i18n-ally.md +49 -0
package/dist/cli.mjs
CHANGED
|
@@ -9,9 +9,10 @@ import { createInterface } from "node:readline/promises";
|
|
|
9
9
|
import { configFileNames, findConfigFile, loadWorkspace, migrateI18nAlly } from "@x18n/node";
|
|
10
10
|
import { extractItems, findCandidates, toPlanFile } from "@x18n/builtins/extractors";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
|
+
import { namespaceFromPath } from "@x18n/builtins/finders";
|
|
12
13
|
import { parse, stringify } from "yaml";
|
|
13
14
|
//#region package.json
|
|
14
|
-
var version = "0.0.
|
|
15
|
+
var version = "0.0.3";
|
|
15
16
|
//#endregion
|
|
16
17
|
//#region src/commands/change.ts
|
|
17
18
|
async function project(options) {
|
|
@@ -295,9 +296,8 @@ async function status(options) {
|
|
|
295
296
|
out(`${loaded.name} (${displayPath(loaded.root, "")})`);
|
|
296
297
|
out(` ${project.resources.length} resources, ${project.keys().length} keys, ${project.usages().length} usages`);
|
|
297
298
|
for (const c of project.analyze().coverage) {
|
|
298
|
-
const info = project.localeInfo(c.locale);
|
|
299
299
|
const label = `${c.locale}${c.locale === project.sourceLocale ? " (source)" : ""}`;
|
|
300
|
-
out(` ${
|
|
300
|
+
out(` ${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
301
|
}
|
|
302
302
|
out(` ${summary.errors} errors, ${summary.warnings} warnings`);
|
|
303
303
|
}
|
|
@@ -429,7 +429,7 @@ async function locales(options) {
|
|
|
429
429
|
printJson("locales", { projects });
|
|
430
430
|
return;
|
|
431
431
|
}
|
|
432
|
-
for (const p of projects) for (const l of p.locales) out(`${l.
|
|
432
|
+
for (const p of projects) for (const l of p.locales) out(`${l.code.padEnd(10)} ${(l.displayName ?? "").padEnd(28)} ${l.resources.join(", ")}`);
|
|
433
433
|
}
|
|
434
434
|
async function doctor(options) {
|
|
435
435
|
const projects = (await openWorkspace(options)).projects.map((loaded) => ({
|
|
@@ -445,7 +445,10 @@ async function doctor(options) {
|
|
|
445
445
|
out(`# ${e.name} (${e.root})`);
|
|
446
446
|
out(`config: ${e.configFile ?? "none (detection only)"}`);
|
|
447
447
|
out(`presets:`);
|
|
448
|
-
|
|
448
|
+
const applied = e.presets.filter((p) => p.reason === "detected" || p.reason === "listed");
|
|
449
|
+
for (const p of applied) out(` ✓ ${p.name} (${p.reason})`);
|
|
450
|
+
if (applied.length === 0) out(" none");
|
|
451
|
+
if (e.presets.length > applied.length) out(` ${e.presets.length - applied.length} others not applied (--json lists them)`);
|
|
449
452
|
out("finders:");
|
|
450
453
|
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
454
|
out("formats:");
|
|
@@ -513,6 +516,21 @@ async function schema(name, options) {
|
|
|
513
516
|
});
|
|
514
517
|
else out(text);
|
|
515
518
|
}
|
|
519
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
520
|
+
/** The folders and file name that name `namespace`, in the style of `namespace.fromPath`. */
|
|
521
|
+
function namespaceSpan(parts, namespace, fromPath) {
|
|
522
|
+
for (let start = 0; start < parts.length; start++) for (let end = start + 1; end <= parts.length; end++) if (namespaceFromPath(parts.slice(start, end).join("/"), fromPath) === namespace) return {
|
|
523
|
+
start,
|
|
524
|
+
end
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** The resource pattern that matches a found file: `public/locales/en/common.json` → `public/locales/{locale}/{namespaces}.{ext}`. */
|
|
528
|
+
function patternOf(resource, fromPath) {
|
|
529
|
+
const parts = resource.path.replace(/\.[^./]+$/, "").split("/");
|
|
530
|
+
const span = resource.namespace === void 0 ? void 0 : namespaceSpan(parts, resource.namespace, fromPath);
|
|
531
|
+
if (span) parts.splice(span.start, span.end - span.start, "{namespaces}");
|
|
532
|
+
return `${parts.join("/")}.{ext}`.replace(new RegExp(`(^|[/._-])${escapeRegExp(resource.locale)}(?=[/._-])`), "$1{locale}");
|
|
533
|
+
}
|
|
516
534
|
async function init(options) {
|
|
517
535
|
const root = resolve(options.root ?? cwd());
|
|
518
536
|
const existing = findConfigFile(root);
|
|
@@ -521,9 +539,8 @@ async function init(options) {
|
|
|
521
539
|
cwd: root,
|
|
522
540
|
cache: false
|
|
523
541
|
})).projects[0];
|
|
524
|
-
const
|
|
525
|
-
const
|
|
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}")))];
|
|
542
|
+
const detected = project.explain().presets.filter((p) => p.reason === "detected" && p.name !== "general").map((p) => p.name);
|
|
543
|
+
const resources = [...new Set(project.resources.filter((r) => !r.block).map((r) => patternOf(r, project.config.namespace.fromPath)))];
|
|
527
544
|
const config = {
|
|
528
545
|
sourceLocale: project.sourceLocale,
|
|
529
546
|
resources: resources.map((pattern) => ({ pattern }))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x18n/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
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.
|
|
42
|
-
"@x18n/core": "0.0.
|
|
43
|
-
"@x18n/node": "0.0.
|
|
44
|
-
"@x18n/ui": "0.0.
|
|
41
|
+
"@x18n/builtins": "0.0.3",
|
|
42
|
+
"@x18n/core": "0.0.3",
|
|
43
|
+
"@x18n/node": "0.0.3",
|
|
44
|
+
"@x18n/ui": "0.0.3",
|
|
45
45
|
"cac": "^7.1.0",
|
|
46
46
|
"devframe": "^1.2.5",
|
|
47
47
|
"yaml": "^2.9.1"
|
package/skills/x18n/SKILL.md
CHANGED
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: x18n
|
|
3
|
-
description:
|
|
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
|
|
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
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Pass `--json` and read the envelope `{ "x18n": 1, "command", "data", "diagnostics", "errors" }`.
|
|
16
|
-
-
|
|
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
|
-
##
|
|
32
|
+
## Set up x18n
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
137
|
+
## Output
|
|
97
138
|
|
|
98
|
-
|
|
139
|
+
Exit codes:
|
|
99
140
|
|
|
100
|
-
|
|
101
|
-
x18n
|
|
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
|
-
|
|
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>.
|