@x18n/cli 0.0.14 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as out, c as runPlan, d as useColor, f as wireDiagnostics, i as openWorkspace, l as singleProject, n as displayPath, o as printJson, r as exitCodeOf, s as readInput, t as cwd, u as stdinArg } from "./context-j5IMF1tl.mjs";
2
2
  import { createRequire } from "node:module";
3
- import { PluginFileSchema, errors, isJsonObject, isX18nError, parseMessageBatch, placeholderProblems, renderPrompt, toErrorInfo, toWireRange, validate } from "@x18n/core";
3
+ import { PluginFileSchema, errors, isJsonObject, isX18nError, parseMessageBatch, placeholderProblems, renderExplanation, renderPrompt, toErrorInfo, toWireRange, validate } from "@x18n/core";
4
4
  import process from "node:process";
5
5
  import { styleText } from "node:util";
6
6
  import cac from "cac";
@@ -15,7 +15,7 @@ import { existsSync } from "node:fs";
15
15
  import { namespaceFromPath } from "@x18n/builtins/finders";
16
16
  import { parse, stringify } from "yaml";
17
17
  //#region package.json
18
- var version = "0.0.14";
18
+ var version = "0.1.0";
19
19
  //#endregion
20
20
  //#region src/commands/change.ts
21
21
  async function project(options) {
@@ -104,13 +104,13 @@ async function format(options) {
104
104
  }
105
105
  //#endregion
106
106
  //#region src/commands/check.ts
107
- function list(value) {
107
+ function list$1(value) {
108
108
  return value === void 0 ? [] : [value].flat().flatMap((item) => String(item).split(",")).map((item) => item.trim()).filter(Boolean);
109
109
  }
110
110
  function filterDiagnostics(diagnostics, options) {
111
- const locales = list(options.locale);
112
- const only = list(options.only);
113
- const skip = list(options.skip);
111
+ const locales = list$1(options.locale);
112
+ const only = list$1(options.only);
113
+ const skip = list$1(options.skip);
114
114
  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" ? {
115
115
  ...d,
116
116
  severity: "error"
@@ -138,14 +138,14 @@ function reportInputs(workspace, options) {
138
138
  function needsHistory(loaded, options) {
139
139
  const setting = loaded.project.config.checks["stale-translation"];
140
140
  const level = typeof setting === "string" ? setting : setting?.[0];
141
- const only = list(options.only);
142
- return level !== "off" && (only.length === 0 || only.includes("stale-translation")) && !list(options.skip).includes("stale-translation");
141
+ const only = list$1(options.only);
142
+ return level !== "off" && (only.length === 0 || only.includes("stale-translation")) && !list$1(options.skip).includes("stale-translation");
143
143
  }
144
144
  async function report(workspace, options) {
145
145
  const inputs = reportInputs(workspace, options);
146
- const reporterIds = list(options.reporter);
146
+ const reporterIds = list$1(options.reporter);
147
147
  const ids = reporterIds.length ? reporterIds : [options.json ? "json" : "pretty"];
148
- const outputs = list(options.output);
148
+ const outputs = list$1(options.output);
149
149
  const available = workspace.projects[0]?.project.reporters ?? /* @__PURE__ */ new Map();
150
150
  const roots = new Map(workspace.projects.map((p) => [p.name, p.root]));
151
151
  for (const [i, id] of ids.entries()) {
@@ -201,7 +201,7 @@ async function readPlan(file) {
201
201
  return data.items;
202
202
  }
203
203
  /** One question per string in a TTY: extract with the proposed key, a typed key, or skip. */
204
- async function review(candidates, root) {
204
+ async function review$1(candidates, root) {
205
205
  const rl = createInterface({
206
206
  input: process.stdin,
207
207
  output: process.stderr
@@ -258,7 +258,7 @@ async function extract(files, options) {
258
258
  out(`${candidates.length} strings. Apply with \`x18n extract --yes\`, or review with \`x18n extract --plan plan.json\`.`);
259
259
  return;
260
260
  }
261
- const chosen = interactive ? await review(candidates, loaded.root) : candidates;
261
+ const chosen = interactive ? await review$1(candidates, loaded.root) : candidates;
262
262
  if (chosen.length === 0) {
263
263
  out("Nothing to extract.");
264
264
  return;
@@ -368,6 +368,39 @@ function locate(loaded, path, start, end) {
368
368
  column: range.column
369
369
  };
370
370
  }
371
+ async function search(query, options) {
372
+ const workspace = await openWorkspace(options);
373
+ const projects = workspace.projects.map((loaded) => {
374
+ const { project } = loaded;
375
+ const { items, total, nextCursor } = page(project.search(String(query), { exact: options.exact }), options);
376
+ return {
377
+ name: loaded.name,
378
+ total,
379
+ nextCursor,
380
+ matches: items.map((m) => ({
381
+ key: m.key,
382
+ locale: m.locale,
383
+ value: m.value,
384
+ usages: project.usages({ key: m.key }).map((u) => ({
385
+ possible: u.possible,
386
+ ...locate(loaded, u.usage.path, u.usage.range.start, u.usage.range.end)
387
+ }))
388
+ }))
389
+ };
390
+ });
391
+ if (options.json) {
392
+ printJson("search", { projects });
393
+ return;
394
+ }
395
+ for (const p of projects) {
396
+ if (workspace.projects.length > 1) out(`# ${p.name}`);
397
+ for (const m of p.matches) {
398
+ out(`${m.key} ${m.locale} ${JSON.stringify(m.value)}`);
399
+ for (const u of m.usages) out(` ${u.file}:${u.line}:${u.column}${u.possible ? " (possible)" : ""}`);
400
+ }
401
+ if (p.nextCursor) out(`… ${p.total - Number(p.nextCursor)} more (--cursor ${p.nextCursor})`);
402
+ }
403
+ }
371
404
  async function get(input, options) {
372
405
  const workspace = await openWorkspace(options);
373
406
  const projects = workspace.projects.flatMap((loaded) => {
@@ -390,7 +423,8 @@ async function get(input, options) {
390
423
  raw: u.usage.raw,
391
424
  ...locate(loaded, u.usage.path, u.usage.range.start, u.usage.range.end)
392
425
  })),
393
- diagnostics: wireDiagnostics(loaded, info.diagnostics)
426
+ diagnostics: wireDiagnostics(loaded, info.diagnostics),
427
+ reviews: info.reviews
394
428
  }];
395
429
  });
396
430
  if (options.json) {
@@ -405,6 +439,7 @@ async function get(input, options) {
405
439
  out(` ${p.usages.length} usages`);
406
440
  for (const u of p.usages) out(` ${u.file}:${u.line}:${u.column}${u.possible ? " (possible)" : ""}`);
407
441
  for (const d of p.diagnostics) out(` ${d.severity} ${d.code}: ${d.message}`);
442
+ for (const thread of p.reviews) out(` review ${thread.locale}: ${thread.state ?? "resolved"}, ${thread.comments.length} comment${thread.comments.length === 1 ? "" : "s"}`);
408
443
  }
409
444
  }
410
445
  async function usages(input, options) {
@@ -458,25 +493,7 @@ async function doctor(options) {
458
493
  printJson("doctor", { projects });
459
494
  return;
460
495
  }
461
- for (const e of projects) {
462
- out(`# ${e.name} (${e.root})`);
463
- out(`config: ${e.configFile ?? "none (detection only)"}`);
464
- out(`presets:`);
465
- const applied = e.presets.filter((p) => p.reason === "detected" || p.reason === "listed");
466
- for (const p of applied) out(` ✓ ${p.name} (${p.reason})`);
467
- if (applied.length === 0) out(" none");
468
- if (e.presets.length > applied.length) out(` ${e.presets.length - applied.length} others not applied (--json lists them)`);
469
- out("finders:");
470
- for (const f of e.finders) out(` ${f.id}: ${f.resources.length} resources${f.resources.length ? `\n ${f.resources.slice(0, 20).join("\n ")}` : ""}`);
471
- out("formats:");
472
- for (const f of e.formats.filter((f) => f.resources > 0)) out(` ${f.id} (${f.extensions.join(", ")}): ${f.resources} resources`);
473
- out("scanners:");
474
- for (const s of e.scanners) out(` ${s.id} (${s.extensions.join(", ") || "none"}): ${s.files} files`);
475
- if (e.conflicts.length) out(`conflicts:\n${e.conflicts.map((c) => ` .${c.extension}: ${c.winner} wins over ${c.loser}`).join("\n")}`);
476
- if (e.errors.length) out(`errors:\n${e.errors.map((err) => ` ${err.name} ${err.why}`).join("\n")}`);
477
- out("config origins:");
478
- for (const [key, origin] of Object.entries(e.config.origins)) out(` ${key}: ${origin}`);
479
- }
496
+ for (const e of projects) out(renderExplanation(e, e.configFile));
480
497
  }
481
498
  //#endregion
482
499
  //#region src/commands/migrate.ts
@@ -526,10 +543,94 @@ async function prompt(kind, options) {
526
543
  else out(renderPrompt(data));
527
544
  }
528
545
  //#endregion
546
+ //#region src/commands/review.ts
547
+ const commentTypes = {
548
+ "comment": "comment",
549
+ "approve": "approve",
550
+ "request-change": "request-change",
551
+ "suggest": "suggestion"
552
+ };
553
+ /** `Ada <ada@example.com>`, `Ada` or `ada@example.com`. */
554
+ function parseAuthor(input) {
555
+ const text = input.trim();
556
+ const open = text.lastIndexOf("<");
557
+ if (open >= 0 && text.endsWith(">")) {
558
+ const name = text.slice(0, open).trim();
559
+ return {
560
+ ...name && { name },
561
+ email: text.slice(open + 1, -1).trim()
562
+ };
563
+ }
564
+ return text.includes("@") ? { email: text } : { name: text };
565
+ }
566
+ function required(value, flag) {
567
+ if (value === void 0 || value === "") throw errors.X18N4008({ reason: `pass ${flag}` });
568
+ return String(value);
569
+ }
570
+ async function list(target, options) {
571
+ const workspace = await openWorkspace(options);
572
+ const locale = options.locale === void 0 ? void 0 : String([options.locale].flat()[0]);
573
+ const projects = workspace.projects.map(({ name, project }) => ({
574
+ name,
575
+ threads: project.reviews({
576
+ key: target === void 0 ? void 0 : project.parseKey(target),
577
+ locale,
578
+ open: options.open
579
+ })
580
+ }));
581
+ if (options.json) {
582
+ printJson("review list", { projects });
583
+ return;
584
+ }
585
+ const threads = projects.flatMap((p) => p.threads);
586
+ if (threads.length === 0) out(options.open ? "No open reviews." : "No reviews.");
587
+ for (const thread of threads) {
588
+ out(`${thread.key} ${thread.locale}${thread.state ? ` ${thread.state.replace("-", " ")}` : ""}`);
589
+ for (const c of thread.comments) {
590
+ const flags = [c.resolved && "resolved", c.stale && "stale"].filter(Boolean).join(", ");
591
+ out(` ${c.id} ${c.type.padEnd(14)} ${c.author?.name ?? c.author?.email ?? "-"} ${c.time.slice(0, 10)}${flags ? ` (${flags})` : ""}`);
592
+ if (c.text) out(` ${c.text}`);
593
+ if (c.suggestion !== void 0) out(` suggests ${JSON.stringify(c.suggestion)}`);
594
+ }
595
+ }
596
+ }
597
+ /** `x18n review <action> [target]`: list, comment, approve, request-change, suggest, resolve, apply-suggestion, mark-fresh. */
598
+ async function review(action, target, options) {
599
+ if (action === "list") return list(target, options);
600
+ const loaded = singleProject(await openWorkspace(options));
601
+ const { project } = loaded;
602
+ const locales = options.locale === void 0 ? void 0 : [options.locale].flat().map(String);
603
+ let op;
604
+ const type = commentTypes[action];
605
+ if (type) {
606
+ const locale = locales?.[0];
607
+ if (locale === void 0) throw errors.X18N4008({ reason: "pass --locale <locale>" });
608
+ op = {
609
+ type: "review",
610
+ key: project.parseKey(required(target, "<key>")),
611
+ locale,
612
+ review: type,
613
+ text: type === "request-change" ? required(options.text, "--text <text>") : options.text === void 0 ? void 0 : String(options.text),
614
+ suggestion: type === "suggestion" ? required(options.value, "--value <value>") : void 0,
615
+ author: options.author === void 0 ? await project.reviewAuthor() : parseAuthor(options.author)
616
+ };
617
+ } else if (action === "resolve" || action === "apply-suggestion") op = {
618
+ type: action === "resolve" ? "resolve-review" : "apply-suggestion",
619
+ id: required(target, "<id>")
620
+ };
621
+ else if (action === "mark-fresh") op = {
622
+ type: "mark-fresh",
623
+ key: project.parseKey(required(target, "<key>")),
624
+ locales
625
+ };
626
+ else throw errors.X18N4008({ reason: `unknown review action "${action}" (list, comment, approve, request-change, suggest, resolve, apply-suggestion, mark-fresh)` });
627
+ await runPlan(`review ${action}`, loaded, project.plan([op]), options);
628
+ }
629
+ //#endregion
529
630
  //#region src/commands/setup.ts
530
631
  async function schema(name, options) {
531
632
  const which = name ?? "config";
532
- if (which !== "config" && which !== "plugin") throw errors.X18N0001({ issue: `unknown schema "${which}" (config, plugin)` });
633
+ if (which !== "config" && which !== "plugin" && which !== "reviews") throw errors.X18N0001({ issue: `unknown schema "${which}" (config, plugin, reviews)` });
533
634
  const require = createRequire(import.meta.url);
534
635
  const text = await readFile(require.resolve(`@x18n/core/schema/${which}.json`), "utf8");
535
636
  if (options.json) printJson("schema", {
@@ -676,10 +777,11 @@ function createCli() {
676
777
  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");
677
778
  cli.command("", "Show the status and the main commands").action(run("status", async (options) => {
678
779
  await status(options);
679
- 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");
780
+ if (!options.json) process.stdout.write("\nCommands: check, keys, search, get, usages, set, apply, rename, delete, fill, prompt translate, ui. Run `x18n --help` for all.\n");
680
781
  }));
681
782
  cli.command("status", "Locales with coverage, resources, keys and diagnostics").action(run("status", status));
682
783
  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));
784
+ cli.command("search <query>", "Find keys by their value in any locale, with their usages").option("--exact", "Only values containing the query, instead of a fuzzy match").option("--limit <n>", "Page size").option("--cursor <cursor>", "Start of the page").action(run("search", search));
683
785
  cli.command("get <key>", "Values, resources, usages and diagnostics of one key").action(run("get", get));
684
786
  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));
685
787
  cli.command("locales", "Locale codes as found, with display names and resources").action(run("locales", locales));
@@ -693,12 +795,13 @@ function createCli() {
693
795
  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));
694
796
  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));
695
797
  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").option("--stale", "Also translate stale translations (needs git history)").action(run("prompt", prompt));
798
+ cli.command("review <action> [target]", "Reviews: `list [key]`, `comment|approve|request-change|suggest <key>`, `resolve|apply-suggestion <id>`, `mark-fresh <key>`").option("--locale <locale>", "Locale of the message (repeatable for mark-fresh)").option("--text <text>", "Comment text").option("--value <value>", "Suggested value (suggest)").option("--author <author>", "Author as `Name <email>` (default: review.author, then git user)").option("--open", "Only unresolved reviews (list)").option("--dry-run", "Print the plan instead of writing").action(run("review", review));
696
799
  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)));
697
800
  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)));
698
801
  cli.command("mcp", "Serve the MCP tools over stdio").action(run("mcp", async (options) => (await uiCommands()).mcp(options)));
699
802
  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));
700
803
  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));
701
- cli.command("schema [name]", "Print a JSON Schema: config (default) or plugin").action(run("schema", schema));
804
+ cli.command("schema [name]", "Print a JSON Schema: config (default), plugin or reviews").action(run("schema", schema));
702
805
  cli.command("plugin <action> <file>", "Plugin tools: `plugin eject <file.yml>` prints the TS equivalent").action(run("plugin eject", async (action, file, options) => {
703
806
  if (action !== "eject") throw new Error(`unknown plugin action "${action}"`);
704
807
  await eject(file, options);
@@ -10,7 +10,6 @@ import { flutterTranslate } from "@x18n/builtins/presets/flutter-translate";
10
10
  import { general } from "@x18n/builtins/presets/general";
11
11
  import { globalize } from "@x18n/builtins/presets/globalize";
12
12
  import { gravityUiI18n } from "@x18n/builtins/presets/gravity-ui-i18n";
13
- import { i18nJs } from "@x18n/builtins/presets/i18n-js";
14
13
  import { i18nTag } from "@x18n/builtins/presets/i18n-tag";
15
14
  import { i18next } from "@x18n/builtins/presets/i18next";
16
15
  import { i18nextShopify } from "@x18n/builtins/presets/i18next-shopify";
@@ -32,6 +31,7 @@ import { polyglot } from "@x18n/builtins/presets/polyglot";
32
31
  import { pythonGettext } from "@x18n/builtins/presets/python-gettext";
33
32
  import { reactI18next } from "@x18n/builtins/presets/react-i18next";
34
33
  import { reactIntl } from "@x18n/builtins/presets/react-intl";
34
+ import { rubyI18nJs } from "@x18n/builtins/presets/ruby-i18n-js";
35
35
  import { railsLazyKey, rubyRails } from "@x18n/builtins/presets/ruby-rails";
36
36
  import { solidPrimitivesI18n } from "@x18n/builtins/presets/solid-primitives-i18n";
37
37
  import { svelteI18n } from "@x18n/builtins/presets/svelte-i18n";
@@ -41,4 +41,4 @@ import { typesafeI18n } from "@x18n/builtins/presets/typesafe-i18n";
41
41
  import { ui5 } from "@x18n/builtins/presets/ui5";
42
42
  import { vscodeExtension } from "@x18n/builtins/presets/vscode-extension";
43
43
  import { vueI18n } from "@x18n/builtins/presets/vue-i18n";
44
- export { cargoI18n, chromeExtension, dotnetResx, easyLocalization, emberIntl, fluentReact, fluentVue, flutter, flutterTranslate, general, globalize, gravityUiI18n, i18nJs, i18nTag, i18next, i18nextShopify, i18nice, jekyll, laravel, laravelVueI18n, lingui, nextInternational, nextIntl, nextTranslate, ngxTranslate, nuxtI18n, nuxtI18nMicro, paraglide, phpGettext, phpJoomla, polyglot, pythonGettext, railsLazyKey, reactI18next, reactIntl, rubyRails, solidPrimitivesI18n, svelteI18n, svelteI18next, transloco, typesafeI18n, ui5, vscodeExtension, vueI18n };
44
+ export { cargoI18n, chromeExtension, dotnetResx, easyLocalization, emberIntl, fluentReact, fluentVue, flutter, flutterTranslate, general, globalize, gravityUiI18n, i18nTag, i18next, i18nextShopify, i18nice, jekyll, laravel, laravelVueI18n, lingui, nextInternational, nextIntl, nextTranslate, ngxTranslate, nuxtI18n, nuxtI18nMicro, paraglide, phpGettext, phpJoomla, polyglot, pythonGettext, railsLazyKey, reactI18next, reactIntl, rubyI18nJs, rubyRails, solidPrimitivesI18n, svelteI18n, svelteI18next, transloco, typesafeI18n, ui5, vscodeExtension, vueI18n };
package/dist/presets.mjs CHANGED
@@ -10,7 +10,6 @@ import { flutterTranslate } from "@x18n/builtins/presets/flutter-translate";
10
10
  import { general } from "@x18n/builtins/presets/general";
11
11
  import { globalize } from "@x18n/builtins/presets/globalize";
12
12
  import { gravityUiI18n } from "@x18n/builtins/presets/gravity-ui-i18n";
13
- import { i18nJs } from "@x18n/builtins/presets/i18n-js";
14
13
  import { i18nTag } from "@x18n/builtins/presets/i18n-tag";
15
14
  import { i18next } from "@x18n/builtins/presets/i18next";
16
15
  import { i18nextShopify } from "@x18n/builtins/presets/i18next-shopify";
@@ -32,6 +31,7 @@ import { polyglot } from "@x18n/builtins/presets/polyglot";
32
31
  import { pythonGettext } from "@x18n/builtins/presets/python-gettext";
33
32
  import { reactI18next } from "@x18n/builtins/presets/react-i18next";
34
33
  import { reactIntl } from "@x18n/builtins/presets/react-intl";
34
+ import { rubyI18nJs } from "@x18n/builtins/presets/ruby-i18n-js";
35
35
  import { railsLazyKey, rubyRails } from "@x18n/builtins/presets/ruby-rails";
36
36
  import { solidPrimitivesI18n } from "@x18n/builtins/presets/solid-primitives-i18n";
37
37
  import { svelteI18n } from "@x18n/builtins/presets/svelte-i18n";
@@ -41,4 +41,4 @@ import { typesafeI18n } from "@x18n/builtins/presets/typesafe-i18n";
41
41
  import { ui5 } from "@x18n/builtins/presets/ui5";
42
42
  import { vscodeExtension } from "@x18n/builtins/presets/vscode-extension";
43
43
  import { vueI18n } from "@x18n/builtins/presets/vue-i18n";
44
- export { cargoI18n, chromeExtension, dotnetResx, easyLocalization, emberIntl, fluentReact, fluentVue, flutter, flutterTranslate, general, globalize, gravityUiI18n, i18nJs, i18nTag, i18next, i18nextShopify, i18nice, jekyll, laravel, laravelVueI18n, lingui, nextInternational, nextIntl, nextTranslate, ngxTranslate, nuxtI18n, nuxtI18nMicro, paraglide, phpGettext, phpJoomla, polyglot, pythonGettext, railsLazyKey, reactI18next, reactIntl, rubyRails, solidPrimitivesI18n, svelteI18n, svelteI18next, transloco, typesafeI18n, ui5, vscodeExtension, vueI18n };
44
+ export { cargoI18n, chromeExtension, dotnetResx, easyLocalization, emberIntl, fluentReact, fluentVue, flutter, flutterTranslate, general, globalize, gravityUiI18n, i18nTag, i18next, i18nextShopify, i18nice, jekyll, laravel, laravelVueI18n, lingui, nextInternational, nextIntl, nextTranslate, ngxTranslate, nuxtI18n, nuxtI18nMicro, paraglide, phpGettext, phpJoomla, polyglot, pythonGettext, railsLazyKey, reactI18next, reactIntl, rubyI18nJs, rubyRails, solidPrimitivesI18n, svelteI18n, svelteI18next, transloco, typesafeI18n, ui5, vscodeExtension, vueI18n };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x18n/cli",
3
3
  "type": "module",
4
- "version": "0.0.14",
4
+ "version": "0.1.0",
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.14",
42
- "@x18n/core": "0.0.14",
43
- "@x18n/node": "0.0.14",
44
- "@x18n/ui": "0.0.14",
41
+ "@x18n/builtins": "0.1.0",
42
+ "@x18n/core": "0.1.0",
43
+ "@x18n/node": "0.1.0",
44
+ "@x18n/ui": "0.1.0",
45
45
  "cac": "^7.1.0",
46
46
  "devframe": "^1.2.6",
47
47
  "nostics": "^1.3.0",
@@ -0,0 +1,214 @@
1
+ ---
2
+ name: x18n
3
+ description: Manage a project's translations with x18n (`@x18n/cli`). Use when the task touches locale files or translation keys - adding, renaming or deleting keys, finding the key behind a text shown in the app, finding missing or unused keys, translating missing messages, reviewing translations, extracting hard-coded strings, keeping a glossary of terms.
4
+ ---
5
+
6
+ # x18n
7
+
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
+
21
+ Batch the questions into one message. Routine work needs no question: adding a key you use, translating a missing message, fixing a placeholder.
22
+
23
+ ## Rules
24
+
25
+ - 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.
26
+ - Check a key before you use it in code: `x18n get <key> --json`. Reuse an existing key with the same meaning: `x18n search "<text>" --json` finds keys whose value already says it.
27
+ - Keep every placeholder of the source message (`{name}`, `{{count}}`, `%{name}`, `:name`, `%s`, `@:key`).
28
+ - Pass `--json` and read the envelope `{ "x18n": 2, "command", "data", "diagnostics", "errors" }`.
29
+ - Run renames and deletes with `--dry-run` first, and read the plan.
30
+ - Write every key as a full string literal. Do not build keys at runtime (see below).
31
+
32
+ ## Avoid composed keys
33
+
34
+ Do not build a key from parts, such as `` t(`foo.bar.${name}`) `` or `t('foo.bar.' + name)`. x18n and the compiler cannot see the full key, so:
35
+
36
+ - `x18n keys --unused` reports the key as unused, and `x18n delete` can remove a message that the code needs
37
+ - `x18n rename` does not update the usage
38
+ - `x18n check` cannot find a missing message until the user hits it at runtime
39
+ - translators cannot see the full key in context
40
+
41
+ Write each key out in full and map the value to it:
42
+
43
+ ```ts
44
+ // Avoid
45
+ t(`status.${status}`)
46
+
47
+ // Prefer: a lookup of full literal keys
48
+ const labels = {
49
+ active: t('status.active'),
50
+ paused: t('status.paused'),
51
+ } as const
52
+ labels[status]
53
+ ```
54
+
55
+ ```ts
56
+ // Avoid
57
+ t(`plans.${plan}.title`)
58
+
59
+ // Prefer: a switch, or a record of functions when the call must be lazy
60
+ const planTitle = {
61
+ free: () => t('plans.free.title'),
62
+ pro: () => t('plans.pro.title'),
63
+ }
64
+ planTitle[plan]()
65
+ ```
66
+
67
+ When the variable part is only a word inside a sentence, use a placeholder instead of a key part: `t('greeting', { name })` with the message `Hello, {name}`. When the text differs by count, use the library's plural syntax, not `t(count === 1 ? 'item.one' : 'item.many')`.
68
+
69
+ If the user's code already composes keys, do not rewrite it unasked. Tell the user which keys are affected, and treat those keys as used when you clean up.
70
+
71
+ ## Set up and check
72
+
73
+ - To set up x18n, or migrate from i18n-ally, use the `x18n-setup` skill.
74
+ - To run `x18n check` and fix what it reports, use the `x18n-check` skill.
75
+
76
+ Commands below use `x18n`; without the dev dependency, run them as `npx @x18n/cli <command>`.
77
+
78
+ ## Glossary (terms table)
79
+
80
+ 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.
81
+
82
+ ```ts
83
+ // x18n.config.ts
84
+ import { defineConfig } from '@x18n/core'
85
+
86
+ export default defineConfig({
87
+ glossary: [
88
+ {
89
+ term: 'Workspace',
90
+ translations: { de: 'Arbeitsbereich', fr: 'Espace de travail' }
91
+ },
92
+ { term: 'x18n', doNotTranslate: true },
93
+ {
94
+ term: 'Save',
95
+ translations: { de: 'Speichern' },
96
+ note: 'The button label, not "rescue"'
97
+ },
98
+ ],
99
+ checks: { glossary: 'warning' },
100
+ })
101
+ ```
102
+
103
+ Maintain it while you work:
104
+
105
+ - Before you translate, read the current entries: `x18n doctor --json` lists them under `config.glossary`.
106
+ - 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.
107
+ - Add the entries the user approves, then run `x18n check --only glossary --json` and fix the messages it reports.
108
+
109
+ ## Loops
110
+
111
+ ### Find the key behind a text
112
+
113
+ When the user names a text they see in the app, and not a key, search the values:
114
+
115
+ ```sh
116
+ x18n search "Sign in" --json
117
+ x18n search "Anmelden" --exact --json
118
+ ```
119
+
120
+ The search covers every locale and ignores case. It is fuzzy by default, and values that contain the query rank first. `--exact` keeps only those. Each match in `data.projects[].matches` has `key`, `locale`, `value` and `usages` (`file`, `line`, `column`), so you can open the code that shows the text.
121
+
122
+ ### Add a key while coding
123
+
124
+ ```sh
125
+ x18n search "Settings" --exact --json
126
+ x18n get settings.title --json
127
+ x18n set settings.title --value "Settings" --json
128
+ x18n keys --missing de --json --limit 50
129
+ ```
130
+
131
+ Reuse a key that `x18n search` finds with the same meaning. `x18n set` without `--locale` writes the source locale. Then use the key in code, for example `t('settings.title')`.
132
+
133
+ ### Translate missing messages
134
+
135
+ ```sh
136
+ x18n prompt translate --to de --limit 50
137
+ ```
138
+
139
+ 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:
140
+
141
+ ```sh
142
+ x18n apply translations.json --json
143
+ x18n check --only placeholder-mismatch,invalid-syntax,glossary --locale de --json
144
+ ```
145
+
146
+ `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>`.
147
+
148
+ ### Review translations
149
+
150
+ ```sh
151
+ x18n prompt review --locale de --limit 50
152
+ ```
153
+
154
+ Do not change values yourself. For each wrong translation, leave a review for a human to accept, then list what you reviewed:
155
+
156
+ ```sh
157
+ x18n review suggest <key> --locale de --value <better text> --text <why> --json
158
+ x18n review request-change <key> --locale de --text <why> --json
159
+ ```
160
+
161
+ Keep every placeholder in a suggested value. `x18n review list --locale de --open --json` shows the open reviews. Only when the user asks, apply one with `x18n review apply-suggestion <id> --json`. When reviews are off (`review.enabled: false`, error X18N5003), fix values with `x18n set <key> --locale de --value <text> --json` instead.
162
+
163
+ ### Clean up
164
+
165
+ ```sh
166
+ x18n keys --unused --json
167
+ x18n delete old.key --dry-run --json
168
+ ```
169
+
170
+ Show the user the unused keys and delete only the ones they confirm: `x18n delete old.key --json`.
171
+
172
+ ### Rename keys
173
+
174
+ ```sh
175
+ x18n rename home.title home.heading --dry-run --json
176
+ x18n rename home.title home.heading --json
177
+ x18n rename pages screens --json
178
+ ```
179
+
180
+ 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).
181
+
182
+ ### Extract hard-coded strings
183
+
184
+ ```sh
185
+ x18n extract --list --json
186
+ x18n extract --plan extract-plan.json
187
+ x18n extract --apply extract-plan.json --json
188
+ ```
189
+
190
+ 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.
191
+
192
+ Tests, fixtures, mocks, stories, configs and type declarations are never scanned, in any language (`*.test.ts`, `*_test.go`, `test_*.py`, `*_spec.rb`, `*Test.java`, `vite.config.ts`, `tests/`, `spec/`, `e2e/`, ...). To keep strings that are not UI text out of the plan, drop them from the plan file, or add a control comment to the code:
193
+
194
+ | Comment | Skips |
195
+ | --- | --- |
196
+ | `x18n-ignore-file` | the whole file, wherever the comment is |
197
+ | `x18n-ignore-start` ... `x18n-ignore-end` | everything between them (to the end of the file when unclosed) |
198
+ | `x18n-ignore` | strings on its line and on the next line |
199
+
200
+ Use any comment syntax: `// x18n-ignore-file`, `<!-- x18n-ignore -->`. For strings that recur across files, use `extract.ignore` in the config instead.
201
+
202
+ ## Output
203
+
204
+ Exit codes:
205
+
206
+ - `0` success
207
+ - `1` `x18n check` found errors, or `x18n apply` rejected values
208
+ - `2` usage or config error
209
+ - `3` a decision is needed: re-run with `--yes` to accept the defaults
210
+ - `4` files changed during the command: run it again
211
+
212
+ List commands take `--limit` and return `nextCursor`; pass it as `--cursor` to get the next page: `x18n keys --json --limit 100 --cursor 100`.
213
+
214
+ When `x18n status --json` lists several projects (a monorepo), pass `--project <name>` to write commands.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: x18n-check
3
+ description: Run `x18n check` and fix what it reports - missing keys and translations, placeholder mismatches, invalid message syntax, unused keys, glossary violations. Use when the user asks to check, lint or fix translations, when `x18n check` fails locally or in CI, or before opening a PR that touches locale files or translation keys.
4
+ ---
5
+
6
+ # Check and fix translations
7
+
8
+ `x18n check` reports broken, untranslated and unused messages, like `tsc --noEmit` for i18n. Run it, fix the routine problems, and bring the rest to the user. Change locale files only through the `x18n` CLI (`set`, `apply`, `rename`, `delete`), never by hand. The `x18n` skill covers those commands.
9
+
10
+ If the project has no x18n setup yet, use the `x18n-setup` skill first.
11
+
12
+ ## Decisions belong to the user
13
+
14
+ Before you act on one of these, stop and ask, with your recommendation and the facts you found. Batch the questions into one message.
15
+
16
+ - deleting keys reported as unused, since keys built at runtime look unused too
17
+ - a glossary term and its translation per locale
18
+ - changing the severity of a check in the `checks` map of the config, such as `'unused-key': 'error'` or `glossary: 'warning'`
19
+ - making warnings fail CI with `--strict` or `--max-warnings <n>`
20
+
21
+ ## Loop
22
+
23
+ 1. Run `x18n check --json`. Read `data.projects[].summary` for the counts and `diagnostics` for the problems. Group the diagnostics by `code` and `severity`. **Done when** you know the count per check, errors first.
24
+ 2. Fix the errors, check by check, with the table below. Re-run only that check to confirm: `x18n check --only placeholder-mismatch --json`. **Done when** the summary shows no errors, or each remaining error waits on a user decision.
25
+ 3. Fix the warnings you can fix without a decision, such as missing translations. **Done when** the remaining warnings each need a decision.
26
+ 4. Ask the user about the remaining diagnostics in one message: list them by check, with your recommendation for each. Apply what they decide.
27
+ 5. Run `x18n check --json` again and report the result. **Done when** `x18n check` exits with code 0, or every remaining diagnostic is one the user chose to keep.
28
+
29
+ ## Fixes per check
30
+
31
+ | Check (`code`) | Fix |
32
+ | --- | --- |
33
+ | `missing-key` | A usage names a key the source locale lacks. If it is a typo, fix the usage to an existing key (`x18n keys` lists them). Else add it: `x18n set <key> --value <text> --json`. |
34
+ | `missing-translation`, `empty-translation` | Translate: `x18n prompt translate --to <locale> --limit 50`, write the result to a file, then `x18n apply <file> --json`. Follow `nextCursor` when the prompt reports more keys. |
35
+ | `placeholder-mismatch` | Rewrite the target value with every placeholder of the source: `x18n set <key> --locale <locale> --value <text> --json`. |
36
+ | `invalid-syntax` | Fix the message syntax (ICU braces, linked messages) with `x18n set`. When a linked message does not exist, fix the link or add that key. |
37
+ | `key-conflict` | A key is both a message and a parent, or is defined twice in one locale. Ask which one to keep, then `x18n rename` or `x18n delete` with `--dry-run` first. |
38
+ | `unused-key` | Show the list from `x18n keys --unused --json` and delete only the keys the user confirms: `x18n delete <key> --dry-run --json`, then without `--dry-run`. Keys built at runtime go in `usage.keysInUse` in the config instead. |
39
+ | `redundant-translation` | The message repeats what its fallback locale (`localeFallbacks`) already says. Delete it with `x18n delete <key> --locale <locale> --dry-run` first, then without `--dry-run`. |
40
+ | `glossary` | Rewrite the message with the term from the glossary (`x18n doctor --json` lists it under `config.glossary`), with `x18n set`. |
41
+ | `locale-code-conflict`, `dynamic-key`, `key-style`, `identical-to-source`, `hard-string` | Report them. Fix only when the user asks; `hard-string` is fixed with `x18n extract`, or silenced with an `x18n-ignore` control comment when the string is not UI text. |
42
+
43
+ After a batch of fixes, check only what you touched: `x18n check --only placeholder-mismatch,invalid-syntax,glossary --locale de --json`.
44
+
45
+ ## CI
46
+
47
+ `x18n check` exits with code 1 on errors, so CI fails on broken keys without more setup. Warnings do not fail it unless the project uses `--strict` or `--max-warnings <n>`. For pull request annotations, use `x18n check --reporter github --reporter pretty`. Change none of this without asking.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: x18n-setup
3
+ description: Set up x18n (`@x18n/cli`) in a project, or migrate it from i18n-ally. Use when a project has no x18n config yet, when x18n detects the wrong locale files or library, when the user asks to add, install or configure x18n, or when `.vscode/settings.json` has `i18n-ally.*` settings.
4
+ ---
5
+
6
+ # Set up x18n
7
+
8
+ x18n reads a project's i18n setup (locale files, translate calls in code) and often needs no config. Set it up by checking what it detects, and write a config only when detection needs help.
9
+
10
+ ## Decisions belong to the user
11
+
12
+ Before you act on one of these, stop and ask, with your recommendation and the facts you found. Batch the questions into one message.
13
+
14
+ - the source locale, and which locales the project supports
15
+ - the config format: `x18n.config.ts`, or YAML for projects without Node.js tooling
16
+ - what an i18n-ally migration drops (see the reference below)
17
+ - adding `x18n check` to CI or to a git hook
18
+
19
+ ## Steps
20
+
21
+ 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.
22
+ 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.
23
+ 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. **Done when** `x18n doctor` shows the right result.
24
+ 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.
25
+ 5. Run `x18n check --json` and report the summary to the user: errors and warnings per check (`code` of each diagnostic) and coverage per locale. Do not fix anything in this skill. **Done when** the user has the counts and knows that the `x18n-check` skill fixes them.
26
+
27
+ Without the dev dependency, run commands as `npx @x18n/cli <command>`. In a monorepo, `x18n status --json` lists every project; pass `--project <name>` to pick one.
@@ -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 are converted to `.x18n/reviews.yml`; key descriptions and `review.removeCommentOnResolved` are dropped.
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.annotationInPlace`, `x18n.annotations`, `x18n.annotationMaxLength`, `x18n.showFlags`
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`, `annotationInPlaceFullMatch`, `annotationMaxLength`, `showFlags`, `theme.annotation*` | VS Code settings with the same names under `x18n.*` |
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>.