@x18n/cli 0.0.13 → 0.0.15

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.13";
18
+ var version = "0.0.15";
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", {
@@ -617,7 +718,7 @@ function ejectPlugin(data) {
617
718
  const imports = ["definePlugin", ...data.detect ? ["definePreset"] : []];
618
719
  lines.push(`import { ${imports.join(", ")} } from '@x18n/core'`);
619
720
  if (data.placeholders) lines.push(`import { placeholderFromShape } from '@x18n/builtins/placeholders'`);
620
- if (data.scopes) lines.push(`import { regexScopes } from '@x18n/builtins/scanners'`);
721
+ if (data.scopes) lines.push(`import regexScopes from '@x18n/builtins/scanners/regex-scopes'`);
621
722
  lines.push("", `const plugin = definePlugin({`, ...fields, `})`, "");
622
723
  const plugins = `[plugin${data.scopes ? `, regexScopes(${literal(data.scopes.regex)})` : ""}]`;
623
724
  if (data.detect) {
@@ -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);
@@ -1 +1,44 @@
1
- export * from "@x18n/builtins/presets";
1
+ import { cargoI18n } from "@x18n/builtins/presets/cargo-i18n";
2
+ import { chromeExtension } from "@x18n/builtins/presets/chrome-extension";
3
+ import { dotnetResx } from "@x18n/builtins/presets/dotnet-resx";
4
+ import { easyLocalization } from "@x18n/builtins/presets/easy-localization";
5
+ import { emberIntl } from "@x18n/builtins/presets/ember-intl";
6
+ import { fluentReact } from "@x18n/builtins/presets/fluent-react";
7
+ import { fluentVue } from "@x18n/builtins/presets/fluent-vue";
8
+ import { flutter } from "@x18n/builtins/presets/flutter";
9
+ import { flutterTranslate } from "@x18n/builtins/presets/flutter-translate";
10
+ import { general } from "@x18n/builtins/presets/general";
11
+ import { globalize } from "@x18n/builtins/presets/globalize";
12
+ import { gravityUiI18n } from "@x18n/builtins/presets/gravity-ui-i18n";
13
+ import { i18nTag } from "@x18n/builtins/presets/i18n-tag";
14
+ import { i18next } from "@x18n/builtins/presets/i18next";
15
+ import { i18nextShopify } from "@x18n/builtins/presets/i18next-shopify";
16
+ import { i18nice } from "@x18n/builtins/presets/i18nice";
17
+ import { jekyll } from "@x18n/builtins/presets/jekyll";
18
+ import { laravel } from "@x18n/builtins/presets/laravel";
19
+ import { laravelVueI18n } from "@x18n/builtins/presets/laravel-vue-i18n";
20
+ import { lingui } from "@x18n/builtins/presets/lingui";
21
+ import { nextInternational } from "@x18n/builtins/presets/next-international";
22
+ import { nextIntl } from "@x18n/builtins/presets/next-intl";
23
+ import { nextTranslate } from "@x18n/builtins/presets/next-translate";
24
+ import { ngxTranslate } from "@x18n/builtins/presets/ngx-translate";
25
+ import { nuxtI18n } from "@x18n/builtins/presets/nuxt-i18n";
26
+ import { nuxtI18nMicro } from "@x18n/builtins/presets/nuxt-i18n-micro";
27
+ import { paraglide } from "@x18n/builtins/presets/paraglide";
28
+ import { phpGettext } from "@x18n/builtins/presets/php-gettext";
29
+ import { phpJoomla } from "@x18n/builtins/presets/php-joomla";
30
+ import { polyglot } from "@x18n/builtins/presets/polyglot";
31
+ import { pythonGettext } from "@x18n/builtins/presets/python-gettext";
32
+ import { reactI18next } from "@x18n/builtins/presets/react-i18next";
33
+ import { reactIntl } from "@x18n/builtins/presets/react-intl";
34
+ import { rubyI18nJs } from "@x18n/builtins/presets/ruby-i18n-js";
35
+ import { railsLazyKey, rubyRails } from "@x18n/builtins/presets/ruby-rails";
36
+ import { solidPrimitivesI18n } from "@x18n/builtins/presets/solid-primitives-i18n";
37
+ import { svelteI18n } from "@x18n/builtins/presets/svelte-i18n";
38
+ import { svelteI18next } from "@x18n/builtins/presets/svelte-i18next";
39
+ import { transloco } from "@x18n/builtins/presets/transloco";
40
+ import { typesafeI18n } from "@x18n/builtins/presets/typesafe-i18n";
41
+ import { ui5 } from "@x18n/builtins/presets/ui5";
42
+ import { vscodeExtension } from "@x18n/builtins/presets/vscode-extension";
43
+ import { vueI18n } from "@x18n/builtins/presets/vue-i18n";
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
@@ -1,2 +1,44 @@
1
- export * from "@x18n/builtins/presets";
2
- export {};
1
+ import { cargoI18n } from "@x18n/builtins/presets/cargo-i18n";
2
+ import { chromeExtension } from "@x18n/builtins/presets/chrome-extension";
3
+ import { dotnetResx } from "@x18n/builtins/presets/dotnet-resx";
4
+ import { easyLocalization } from "@x18n/builtins/presets/easy-localization";
5
+ import { emberIntl } from "@x18n/builtins/presets/ember-intl";
6
+ import { fluentReact } from "@x18n/builtins/presets/fluent-react";
7
+ import { fluentVue } from "@x18n/builtins/presets/fluent-vue";
8
+ import { flutter } from "@x18n/builtins/presets/flutter";
9
+ import { flutterTranslate } from "@x18n/builtins/presets/flutter-translate";
10
+ import { general } from "@x18n/builtins/presets/general";
11
+ import { globalize } from "@x18n/builtins/presets/globalize";
12
+ import { gravityUiI18n } from "@x18n/builtins/presets/gravity-ui-i18n";
13
+ import { i18nTag } from "@x18n/builtins/presets/i18n-tag";
14
+ import { i18next } from "@x18n/builtins/presets/i18next";
15
+ import { i18nextShopify } from "@x18n/builtins/presets/i18next-shopify";
16
+ import { i18nice } from "@x18n/builtins/presets/i18nice";
17
+ import { jekyll } from "@x18n/builtins/presets/jekyll";
18
+ import { laravel } from "@x18n/builtins/presets/laravel";
19
+ import { laravelVueI18n } from "@x18n/builtins/presets/laravel-vue-i18n";
20
+ import { lingui } from "@x18n/builtins/presets/lingui";
21
+ import { nextInternational } from "@x18n/builtins/presets/next-international";
22
+ import { nextIntl } from "@x18n/builtins/presets/next-intl";
23
+ import { nextTranslate } from "@x18n/builtins/presets/next-translate";
24
+ import { ngxTranslate } from "@x18n/builtins/presets/ngx-translate";
25
+ import { nuxtI18n } from "@x18n/builtins/presets/nuxt-i18n";
26
+ import { nuxtI18nMicro } from "@x18n/builtins/presets/nuxt-i18n-micro";
27
+ import { paraglide } from "@x18n/builtins/presets/paraglide";
28
+ import { phpGettext } from "@x18n/builtins/presets/php-gettext";
29
+ import { phpJoomla } from "@x18n/builtins/presets/php-joomla";
30
+ import { polyglot } from "@x18n/builtins/presets/polyglot";
31
+ import { pythonGettext } from "@x18n/builtins/presets/python-gettext";
32
+ import { reactI18next } from "@x18n/builtins/presets/react-i18next";
33
+ import { reactIntl } from "@x18n/builtins/presets/react-intl";
34
+ import { rubyI18nJs } from "@x18n/builtins/presets/ruby-i18n-js";
35
+ import { railsLazyKey, rubyRails } from "@x18n/builtins/presets/ruby-rails";
36
+ import { solidPrimitivesI18n } from "@x18n/builtins/presets/solid-primitives-i18n";
37
+ import { svelteI18n } from "@x18n/builtins/presets/svelte-i18n";
38
+ import { svelteI18next } from "@x18n/builtins/presets/svelte-i18next";
39
+ import { transloco } from "@x18n/builtins/presets/transloco";
40
+ import { typesafeI18n } from "@x18n/builtins/presets/typesafe-i18n";
41
+ import { ui5 } from "@x18n/builtins/presets/ui5";
42
+ import { vscodeExtension } from "@x18n/builtins/presets/vscode-extension";
43
+ import { vueI18n } from "@x18n/builtins/presets/vue-i18n";
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.13",
4
+ "version": "0.0.15",
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.13",
42
- "@x18n/core": "0.0.13",
43
- "@x18n/node": "0.0.13",
44
- "@x18n/ui": "0.0.13",
41
+ "@x18n/builtins": "0.0.15",
42
+ "@x18n/core": "0.0.15",
43
+ "@x18n/node": "0.0.15",
44
+ "@x18n/ui": "0.0.15",
45
45
  "cac": "^7.1.0",
46
46
  "devframe": "^1.2.6",
47
47
  "nostics": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  ---
2
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 missing or unused keys, translating missing messages, reviewing translations, extracting hard-coded strings, keeping a glossary of terms.
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
4
  ---
5
5
 
6
6
  # x18n
@@ -23,7 +23,7 @@ Batch the questions into one message. Routine work needs no question: adding a k
23
23
  ## Rules
24
24
 
25
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.
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
27
  - Keep every placeholder of the source message (`{name}`, `{{count}}`, `%{name}`, `:name`, `%s`, `@:key`).
28
28
  - Pass `--json` and read the envelope `{ "x18n": 2, "command", "data", "diagnostics", "errors" }`.
29
29
  - Run renames and deletes with `--dry-run` first, and read the plan.
@@ -85,9 +85,16 @@ import { defineConfig } from '@x18n/core'
85
85
 
86
86
  export default defineConfig({
87
87
  glossary: [
88
- { term: 'Workspace', translations: { de: 'Arbeitsbereich', fr: 'Espace de travail' } },
88
+ {
89
+ term: 'Workspace',
90
+ translations: { de: 'Arbeitsbereich', fr: 'Espace de travail' }
91
+ },
89
92
  { term: 'x18n', doNotTranslate: true },
90
- { term: 'Save', translations: { de: 'Speichern' }, note: 'The button label, not "rescue"' },
93
+ {
94
+ term: 'Save',
95
+ translations: { de: 'Speichern' },
96
+ note: 'The button label, not "rescue"'
97
+ },
91
98
  ],
92
99
  checks: { glossary: 'warning' },
93
100
  })
@@ -101,15 +108,27 @@ Maintain it while you work:
101
108
 
102
109
  ## Loops
103
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
+
104
122
  ### Add a key while coding
105
123
 
106
124
  ```sh
125
+ x18n search "Settings" --exact --json
107
126
  x18n get settings.title --json
108
127
  x18n set settings.title --value "Settings" --json
109
128
  x18n keys --missing de --json --limit 50
110
129
  ```
111
130
 
112
- `x18n set` without `--locale` writes the source locale. Then use the key in code, for example `t('settings.title')`.
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')`.
113
132
 
114
133
  ### Translate missing messages
115
134
 
@@ -132,7 +151,14 @@ x18n check --only placeholder-mismatch,invalid-syntax,glossary --locale de --jso
132
151
  x18n prompt review --locale de --limit 50
133
152
  ```
134
153
 
135
- Fix wrong values with `x18n set <key> --locale de --value <text> --json`, and list what you changed and why.
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.
136
162
 
137
163
  ### Clean up
138
164
 
@@ -11,11 +11,11 @@ Use this when a project has i18n-ally settings: `i18n-ally.*`, legacy `vue-i18n-
11
11
  - `languageTagSystem` is dropped: x18n uses locale codes exactly as they appear in file names.
12
12
  - A custom framework with `monopoly: true` turns the built-in presets off (`presets: []`).
13
13
  - Executed JS/TS locale files become read-only (`execute: true`).
14
- - Reviews move to `.x18n/reviews.yml`, which x18n reads once reviews ship.
14
+ - Reviews are converted to `.x18n/reviews.yml`; key descriptions and `review.removeCommentOnResolved` are dropped.
15
15
  - The config format: `x18n.config.ts` by default, or `--format yml` / `--format json` for projects without Node.js tooling.
16
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
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
18
+ - `.x18n/custom.x18n-plugin.yml`, when there are custom regexes, a custom framework, refactor templates or `localeCountryMap` flags
19
19
  - editor settings in `.vscode/settings.json`: `x18n.displayLocale`, `x18n.annotationInPlace`, `x18n.annotations`, `x18n.annotationMaxLength`, `x18n.showFlags`
20
20
  - `.x18n/reviews.yml`, when `.vscode/i18n-ally-reviews.yml` or `.vscode/i18n-ally-next-reviews.yml` exists
21
21