@verbaly/compiler 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,10 +15,11 @@ The compiler behind [Verbaly](https://github.com/AronSoto/verbaly): AST extracti
15
15
 
16
16
  > Most projects don't install this directly — [`@verbaly/vite`](https://www.npmjs.com/package/@verbaly/vite) wraps it with zero config. Reach for it when scripting extraction/checks yourself.
17
17
 
18
- ## CLI
18
+ ## 🧰 CLI
19
19
 
20
- ```bash
20
+ ```
21
21
  npx verbaly init # scaffold config + locale catalogs (detects your bundler)
22
+ npx verbaly doctor # diagnose the setup (config, catalogs, plugin, types, keys)
22
23
  npx verbaly extract # sync catalogs + types
23
24
  npx verbaly check # exit 1 if anything is missing (CI)
24
25
  npx verbaly extract --prune # drop orphaned keys
@@ -29,30 +30,30 @@ npx verbaly render # pre-fill data-verbaly HTML per locale (SSG — kill
29
30
 
30
31
  Reads `verbaly.config.{js,mjs,ts,mts,json}` (TS configs need `esbuild` installed). Generates `locales/<locale>.json` (flat, portable — no proprietary format) and `verbaly.d.ts` with params typed per key.
31
32
 
32
- ## Machine translation
33
+ ## 🤖 Machine translation
33
34
 
34
35
  `verbaly translate` fills the `""` holes `check` reports. The default provider uses Claude via the official SDK — install it as a dev dependency (translation is a build-time step, not an app runtime dependency): `pnpm add -D @anthropic-ai/sdk` (or `npm i -D`), plus `ANTHROPIC_API_KEY`. Default model is `claude-sonnet-5` (balanced quality/cost); override with `translate.model` in config or `--model <id>`. Placeholders, variants and tags are validated after translation — anything not preserved verbatim stays `""` so `check` keeps failing. Plug your own provider in `verbaly.config.ts`:
35
36
 
36
- ```ts
37
+ ```
37
38
  translate: {
38
39
  provider: async ({ sourceLocale, targetLocale, messages }) => ({ ...translated });
39
40
  }
40
41
  ```
41
42
 
42
- ## Static rendering (SSG)
43
+ ## 📄 Static rendering (SSG)
43
44
 
44
45
  `verbaly render` walks your built site (`dist/` by default, `--site <path>` to change) and pre-fills every `data-verbaly` element **per locale** using the real runtime — plurals, `Intl` formatting, `data-verbaly-args`, attribute translation and `data-verbaly-rich` (same whitelist, XSS-safe). The source locale is filled in place; every other locale is mirrored to `dist/<locale>/…` with `<html lang>` set. Static HTML ships already translated — **no flash of untranslated content** — and the runtime attributes stay put, so client-side locale switching keeps working.
45
46
 
46
47
  Named links in rich messages render as real `<a>` elements — hrefs come from config or markup, never from messages (`javascript:` blocked):
47
48
 
48
- ```ts
49
+ ```
49
50
  // verbaly.config.ts
50
51
  render: { links: { docs: { href: '/docs', target: '_blank', rel: 'noopener' } } }
51
52
  ```
52
53
 
53
54
  Per-element `data-verbaly-links='{"repo":"https://…"}'` merges over the config map.
54
55
 
55
- ## Pseudo-localization
56
+ ## 🔍 Pseudo-localization
56
57
 
57
58
  `verbaly pseudo` fills a QA catalog (`en-XA` by default, `--locale <id>` to change) from the source: accented letters, `⟦…⟧` markers and ~33% length padding reveal hardcoded strings, clipped layouts and concatenation bugs. Params, variant blocks and tags survive verbatim — the same structural validation as `translate`.
58
59
 
@@ -36,9 +36,9 @@ function batchFormat(request) {
36
36
  }
37
37
  };
38
38
  }
39
- async function loadSdk() {
39
+ async function loadSdk(load = () => import("@anthropic-ai/sdk")) {
40
40
  try {
41
- return (await import("@anthropic-ai/sdk")).default;
41
+ return (await load()).default;
42
42
  } catch (error) {
43
43
  if (isModuleNotFound(error, "@anthropic-ai/sdk")) throw new Error("[verbaly] the claude translate provider needs @anthropic-ai/sdk — install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY", { cause: error });
44
44
  throw error;
@@ -50,4 +50,4 @@ function isModuleNotFound(error, name) {
50
50
  //#endregion
51
51
  export { claudeProvider };
52
52
 
53
- //# sourceMappingURL=claude-BhP-eK5E.js.map
53
+ //# sourceMappingURL=claude-Bi_Ex2hm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claude-BhP-eK5E.js","names":[],"sources":["../src/providers/claude.ts"],"sourcesContent":["import type { TranslateProvider, TranslateRequest } from '../translate';\n\nexport interface ClaudeProviderOptions {\n model?: string;\n apiKey?: string;\n maxTokens?: number;\n}\n\n// balanced default for translation\nconst DEFAULT_MODEL = 'claude-sonnet-5';\n\nconst SYSTEM = `You translate UI strings for the Verbaly i18n library.\nRules:\n- Translate only the human-readable text, naturally for the target locale.\n- Preserve verbatim: placeholders like {name}, format specs like {price:currency/EUR}, variant blocks like {count | one: ... | other: # ...} (translate only the text inside each variant, keep keys and # as-is), ICU syntax, named tags like <em>...</em> and escapes {{ }} || ##.\n- Keys are opaque identifiers: return exactly the same keys, never translate or rename them.`;\n\nexport function claudeProvider(options: ClaudeProviderOptions = {}): TranslateProvider {\n return async (request: TranslateRequest) => {\n const Anthropic = await loadSdk();\n const client = new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});\n const response = await client.messages.create({\n model: options.model ?? DEFAULT_MODEL,\n max_tokens: options.maxTokens ?? 16000,\n thinking: { type: 'disabled' },\n system: SYSTEM,\n messages: [{ role: 'user', content: buildPrompt(request) }],\n output_config: { format: batchFormat(request) },\n });\n const text = response.content.find((block) => block.type === 'text')?.text ?? '{}';\n return JSON.parse(text) as Record<string, string>;\n };\n}\n\nexport function buildPrompt(request: TranslateRequest): string {\n return (\n `Translate each value from \"${request.sourceLocale}\" to \"${request.targetLocale}\". ` +\n `Return a JSON object with the same keys and translated values.\\n\\n` +\n JSON.stringify(request.messages, null, 2)\n );\n}\n\nexport function batchFormat(request: TranslateRequest) {\n const keys = Object.keys(request.messages);\n return {\n type: 'json_schema' as const,\n schema: {\n type: 'object',\n properties: Object.fromEntries(keys.map((key) => [key, { type: 'string' }])),\n required: keys,\n additionalProperties: false,\n },\n };\n}\n\nasync function loadSdk(): Promise<typeof import('@anthropic-ai/sdk').default> {\n try {\n const mod = await import('@anthropic-ai/sdk');\n return mod.default;\n } catch (error) {\n if (isModuleNotFound(error, '@anthropic-ai/sdk')) {\n throw new Error(\n '[verbaly] the claude translate provider needs @anthropic-ai/sdk — install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY',\n { cause: error },\n );\n }\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown, name: string): boolean {\n return (\n error instanceof Error &&\n (error as { code?: string }).code === 'ERR_MODULE_NOT_FOUND' &&\n error.message.includes(name)\n );\n}\n"],"mappings":";AASA,MAAM,gBAAgB;AAEtB,MAAM,SAAS;;;;;AAMf,SAAgB,eAAe,UAAiC,CAAC,GAAsB;CACrF,OAAO,OAAO,YAA8B;EAW1C,MAAM,QAAO,MARU,KADJ,OADK,QAAQ,IACH,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAChD,CAAC,CAAC,SAAS,OAAO;GAC5C,OAAO,QAAQ,SAAS;GACxB,YAAY,QAAQ,aAAa;GACjC,UAAU,EAAE,MAAM,WAAW;GAC7B,QAAQ;GACR,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS,YAAY,OAAO;GAAE,CAAC;GAC1D,eAAe,EAAE,QAAQ,YAAY,OAAO,EAAE;EAChD,CAAC,EAAA,CACqB,QAAQ,MAAM,UAAU,MAAM,SAAS,MAAM,CAAC,EAAE,QAAQ;EAC9E,OAAO,KAAK,MAAM,IAAI;CACxB;AACF;AAEA,SAAgB,YAAY,SAAmC;CAC7D,OACE,8BAA8B,QAAQ,aAAa,QAAQ,QAAQ,aAAa,yEAEhF,KAAK,UAAU,QAAQ,UAAU,MAAM,CAAC;AAE5C;AAEA,SAAgB,YAAY,SAA2B;CACrD,MAAM,OAAO,OAAO,KAAK,QAAQ,QAAQ;CACzC,OAAO;EACL,MAAM;EACN,QAAQ;GACN,MAAM;GACN,YAAY,OAAO,YAAY,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC;GAC3E,UAAU;GACV,sBAAsB;EACxB;CACF;AACF;AAEA,eAAe,UAA+D;CAC5E,IAAI;EAEF,QAAO,MADW,OAAO,qBAAA,CACd;CACb,SAAS,OAAO;EACd,IAAI,iBAAiB,OAAO,mBAAmB,GAC7C,MAAM,IAAI,MACR,oMACA,EAAE,OAAO,MAAM,CACjB;EAEF,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,OAAgB,MAAuB;CAC/D,OACE,iBAAiB,SAChB,MAA4B,SAAS,0BACtC,MAAM,QAAQ,SAAS,IAAI;AAE/B"}
1
+ {"version":3,"file":"claude-Bi_Ex2hm.js","names":[],"sources":["../src/providers/claude.ts"],"sourcesContent":["import type { TranslateProvider, TranslateRequest } from '../translate';\n\nexport interface ClaudeProviderOptions {\n model?: string;\n apiKey?: string;\n maxTokens?: number;\n}\n\n// balanced default for translation\nconst DEFAULT_MODEL = 'claude-sonnet-5';\n\nconst SYSTEM = `You translate UI strings for the Verbaly i18n library.\nRules:\n- Translate only the human-readable text, naturally for the target locale.\n- Preserve verbatim: placeholders like {name}, format specs like {price:currency/EUR}, variant blocks like {count | one: ... | other: # ...} (translate only the text inside each variant, keep keys and # as-is), ICU syntax, named tags like <em>...</em> and escapes {{ }} || ##.\n- Keys are opaque identifiers: return exactly the same keys, never translate or rename them.`;\n\nexport function claudeProvider(options: ClaudeProviderOptions = {}): TranslateProvider {\n return async (request: TranslateRequest) => {\n const Anthropic = await loadSdk();\n const client = new Anthropic(options.apiKey ? { apiKey: options.apiKey } : {});\n const response = await client.messages.create({\n model: options.model ?? DEFAULT_MODEL,\n max_tokens: options.maxTokens ?? 16000,\n thinking: { type: 'disabled' },\n system: SYSTEM,\n messages: [{ role: 'user', content: buildPrompt(request) }],\n output_config: { format: batchFormat(request) },\n });\n const text = response.content.find((block) => block.type === 'text')?.text ?? '{}';\n return JSON.parse(text) as Record<string, string>;\n };\n}\n\nexport function buildPrompt(request: TranslateRequest): string {\n return (\n `Translate each value from \"${request.sourceLocale}\" to \"${request.targetLocale}\". ` +\n `Return a JSON object with the same keys and translated values.\\n\\n` +\n JSON.stringify(request.messages, null, 2)\n );\n}\n\nexport function batchFormat(request: TranslateRequest) {\n const keys = Object.keys(request.messages);\n return {\n type: 'json_schema' as const,\n schema: {\n type: 'object',\n properties: Object.fromEntries(keys.map((key) => [key, { type: 'string' }])),\n required: keys,\n additionalProperties: false,\n },\n };\n}\n\ntype SdkModule = { default: typeof import('@anthropic-ai/sdk').default };\n\n// injectable for tests\nexport async function loadSdk(\n load: () => Promise<SdkModule> = () => import('@anthropic-ai/sdk'),\n): Promise<SdkModule['default']> {\n try {\n const mod = await load();\n return mod.default;\n } catch (error) {\n if (isModuleNotFound(error, '@anthropic-ai/sdk')) {\n throw new Error(\n '[verbaly] the claude translate provider needs @anthropic-ai/sdk — install it as a dev dependency (e.g. `pnpm add -D @anthropic-ai/sdk` / `npm i -D @anthropic-ai/sdk`) and set ANTHROPIC_API_KEY',\n { cause: error },\n );\n }\n throw error;\n }\n}\n\nfunction isModuleNotFound(error: unknown, name: string): boolean {\n return (\n error instanceof Error &&\n (error as { code?: string }).code === 'ERR_MODULE_NOT_FOUND' &&\n error.message.includes(name)\n );\n}\n"],"mappings":";AASA,MAAM,gBAAgB;AAEtB,MAAM,SAAS;;;;;AAMf,SAAgB,eAAe,UAAiC,CAAC,GAAsB;CACrF,OAAO,OAAO,YAA8B;EAW1C,MAAM,QAAO,MARU,KADJ,OADK,QAAQ,IACH,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC,CAChD,CAAC,CAAC,SAAS,OAAO;GAC5C,OAAO,QAAQ,SAAS;GACxB,YAAY,QAAQ,aAAa;GACjC,UAAU,EAAE,MAAM,WAAW;GAC7B,QAAQ;GACR,UAAU,CAAC;IAAE,MAAM;IAAQ,SAAS,YAAY,OAAO;GAAE,CAAC;GAC1D,eAAe,EAAE,QAAQ,YAAY,OAAO,EAAE;EAChD,CAAC,EAAA,CACqB,QAAQ,MAAM,UAAU,MAAM,SAAS,MAAM,CAAC,EAAE,QAAQ;EAC9E,OAAO,KAAK,MAAM,IAAI;CACxB;AACF;AAEA,SAAgB,YAAY,SAAmC;CAC7D,OACE,8BAA8B,QAAQ,aAAa,QAAQ,QAAQ,aAAa,yEAEhF,KAAK,UAAU,QAAQ,UAAU,MAAM,CAAC;AAE5C;AAEA,SAAgB,YAAY,SAA2B;CACrD,MAAM,OAAO,OAAO,KAAK,QAAQ,QAAQ;CACzC,OAAO;EACL,MAAM;EACN,QAAQ;GACN,MAAM;GACN,YAAY,OAAO,YAAY,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,MAAM,SAAS,CAAC,CAAC,CAAC;GAC3E,UAAU;GACV,sBAAsB;EACxB;CACF;AACF;AAKA,eAAsB,QACpB,aAAuC,OAAO,sBACf;CAC/B,IAAI;EAEF,QAAO,MADW,KAAK,EAAA,CACZ;CACb,SAAS,OAAO;EACd,IAAI,iBAAiB,OAAO,mBAAmB,GAC7C,MAAM,IAAI,MACR,oMACA,EAAE,OAAO,MAAM,CACjB;EAEF,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,OAAgB,MAAuB;CAC/D,OACE,iBAAiB,SAChB,MAA4B,SAAS,0BACtC,MAAM,QAAQ,SAAS,IAAI;AAE/B"}
package/dist/cli.js CHANGED
@@ -200,6 +200,16 @@ function resolveConfig(config = {}) {
200
200
  render: config.render ?? {}
201
201
  };
202
202
  }
203
+ const CONFIG_FILES = [
204
+ "verbaly.config.js",
205
+ "verbaly.config.mjs",
206
+ "verbaly.config.ts",
207
+ "verbaly.config.mts",
208
+ "verbaly.config.json"
209
+ ];
210
+ function findConfigFile(root) {
211
+ return CONFIG_FILES.find((name) => existsSync(join(root, name)));
212
+ }
203
213
  async function loadConfigFile(root) {
204
214
  for (const name of ["verbaly.config.js", "verbaly.config.mjs"]) {
205
215
  const path = join(root, name);
@@ -578,13 +588,6 @@ function pruneCatalogs(cfg, catalogs, registry) {
578
588
  }
579
589
  //#endregion
580
590
  //#region src/init.ts
581
- const CONFIG_NAMES = [
582
- "verbaly.config.js",
583
- "verbaly.config.mjs",
584
- "verbaly.config.ts",
585
- "verbaly.config.mts",
586
- "verbaly.config.json"
587
- ];
588
591
  const BUNDLERS = [
589
592
  "vite",
590
593
  "webpack",
@@ -618,7 +621,7 @@ function init(options = {}) {
618
621
  const root = options.root ?? process.cwd();
619
622
  const created = [];
620
623
  const skipped = [];
621
- const existing = CONFIG_NAMES.find((name) => existsSync(join(root, name)));
624
+ const existing = findConfigFile(root);
622
625
  const typescript = existsSync(join(root, "tsconfig.json"));
623
626
  const configFile = existing ?? (typescript ? "verbaly.config.ts" : "verbaly.config.mjs");
624
627
  if (existing) skipped.push(existing);
@@ -651,6 +654,111 @@ function init(options = {}) {
651
654
  };
652
655
  }
653
656
  //#endregion
657
+ //#region src/doctor.ts
658
+ const PREVIEW = 5;
659
+ async function doctor(cfg) {
660
+ const entries = [];
661
+ const ok = (check, message) => entries.push({
662
+ level: "ok",
663
+ check,
664
+ message
665
+ });
666
+ const warn = (check, message, fix) => entries.push({
667
+ level: "warn",
668
+ check,
669
+ message,
670
+ fix
671
+ });
672
+ const error = (check, message, fix) => entries.push({
673
+ level: "error",
674
+ check,
675
+ message,
676
+ fix
677
+ });
678
+ const rel = (path) => relative(cfg.root, path).replaceAll("\\", "/");
679
+ const configFile = findConfigFile(cfg.root);
680
+ if (configFile) ok("config", `${configFile} found`);
681
+ else warn("config", "no config file — running on defaults", "run `npx verbaly init`");
682
+ const catalogs = {};
683
+ let catalogsHealthy = true;
684
+ if (!existsSync(cfg.dir)) {
685
+ catalogsHealthy = false;
686
+ error("catalogs", `catalogs directory ${rel(cfg.dir)}/ does not exist`, "run `npx verbaly init` to scaffold it");
687
+ } else {
688
+ for (const locale of cfg.locales) {
689
+ const file = join(cfg.dir, `${locale}.json`);
690
+ if (!existsSync(file)) {
691
+ catalogsHealthy = false;
692
+ error(`locale ${locale}`, `${rel(file)} is missing`, "run `npx verbaly extract` to create it");
693
+ continue;
694
+ }
695
+ try {
696
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
697
+ const bad = Object.entries(parsed).find(([, value]) => typeof value !== "string");
698
+ if (bad) {
699
+ catalogsHealthy = false;
700
+ error(`locale ${locale}`, `${rel(file)} has a non-string value at "${bad[0]}"`, "catalogs are flat key → string JSON; fix the value");
701
+ } else catalogs[locale] = parsed;
702
+ } catch {
703
+ catalogsHealthy = false;
704
+ error(`locale ${locale}`, `${rel(file)} is not valid JSON`, "repair the file (or delete it and run `npx verbaly extract`)");
705
+ }
706
+ }
707
+ if (catalogsHealthy) ok("catalogs", `${cfg.locales.length} locales (${cfg.locales.join(", ")}) in ${rel(cfg.dir)}/`);
708
+ }
709
+ const source = catalogs[cfg.sourceLocale];
710
+ if (source && Object.keys(source).length === 0) warn("source", `source catalog ${cfg.sourceLocale}.json is empty`, "write your first t`…` message and run `npx verbaly extract`");
711
+ const deps = readDeps(cfg.root);
712
+ const bundler = detectBundler(cfg.root);
713
+ const wired = deps["@verbaly/vite"] || deps["@verbaly/unplugin"];
714
+ if (!bundler) ok("plugin", "no bundler detected — CLI flow (extract/check) applies");
715
+ else if (wired) ok("plugin", `${deps["@verbaly/vite"] ? "@verbaly/vite" : "@verbaly/unplugin"} installed for ${bundler}`);
716
+ else if (bundler === "vite") warn("plugin", "vite detected but @verbaly/vite is not installed", "pnpm add -D @verbaly/vite and add verbaly() to the plugins in vite.config");
717
+ else warn("plugin", `${bundler} detected but @verbaly/unplugin is not installed`, `pnpm add -D @verbaly/unplugin and add the verbaly ${bundler} plugin to your build config`);
718
+ if (source) {
719
+ const dtsPath = join(cfg.root, "verbaly.d.ts");
720
+ if (!existsSync(dtsPath)) warn("types", "verbaly.d.ts has not been generated", "run `npx verbaly extract`");
721
+ else if (readFileSync(dtsPath, "utf8") !== generateDts(new Map(Object.entries(source)))) warn("types", "verbaly.d.ts is stale", "run `npx verbaly extract`");
722
+ else ok("types", "verbaly.d.ts is up to date");
723
+ }
724
+ const registry = await extractProject(cfg);
725
+ if (source) {
726
+ const extracted = registry.messages();
727
+ const used = registry.usedKeys();
728
+ const orphans = Object.keys(source).filter((key) => !extracted.has(key) && !used.has(key));
729
+ if (orphans.length > 0) warn("orphans", `${orphans.length} catalog ${orphans.length === 1 ? "key is" : "keys are"} no longer referenced (${preview(orphans)})`, "run `npx verbaly extract --prune` to drop them");
730
+ else ok("orphans", "no orphan keys");
731
+ }
732
+ if (catalogsHealthy) {
733
+ const result = check(cfg, catalogs, registry);
734
+ if (result.unknown.length > 0) error("keys", `${result.unknown.length} unknown ${result.unknown.length === 1 ? "key" : "keys"} used in code (${preview(result.unknown.map((u) => u.key))})`, "fix the key or add it to the catalogs — `npx verbaly check` for details");
735
+ if (result.missing.length > 0) {
736
+ const locales = [...new Set(result.missing.map((m) => m.locale))];
737
+ warn("translations", `${result.missing.length} missing ${result.missing.length === 1 ? "translation" : "translations"} (${locales.join(", ")})`, "run `npx verbaly translate` or fill the catalogs — `npx verbaly check` for details");
738
+ }
739
+ if (result.ok) ok("translations", "all translations complete");
740
+ }
741
+ return {
742
+ ok: entries.every((entry) => entry.level !== "error"),
743
+ entries
744
+ };
745
+ }
746
+ function preview(keys) {
747
+ const head = keys.slice(0, PREVIEW).join(", ");
748
+ return keys.length > PREVIEW ? `${head}, …` : head;
749
+ }
750
+ function readDeps(root) {
751
+ try {
752
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
753
+ return {
754
+ ...pkg.dependencies,
755
+ ...pkg.devDependencies
756
+ };
757
+ } catch {
758
+ return {};
759
+ }
760
+ }
761
+ //#endregion
654
762
  //#region src/pseudo.ts
655
763
  const PSEUDO_LOCALE = "en-XA";
656
764
  const ACCENTS = {
@@ -1041,6 +1149,7 @@ const HELP = `verbaly — i18n compiler
1041
1149
 
1042
1150
  Usage:
1043
1151
  verbaly init scaffold config + locale catalogs (detects your bundler)
1152
+ verbaly doctor diagnose the setup (config, catalogs, plugin, types, keys)
1044
1153
  verbaly extract scan sources, update catalogs and types
1045
1154
  verbaly check verify translations are complete (CI)
1046
1155
  verbaly translate fill missing translations via a provider (default: claude)
@@ -1104,6 +1213,28 @@ async function main() {
1104
1213
  sourceLocale: values.source,
1105
1214
  locales: values.locales?.split(",")
1106
1215
  });
1216
+ if (command === "doctor") {
1217
+ const result = await doctor(cfg);
1218
+ const icon = {
1219
+ ok: "✓",
1220
+ warn: "⚠",
1221
+ error: "✗"
1222
+ };
1223
+ console.log(`[verbaly] doctor — ${result.entries.length} checks`);
1224
+ for (const entry of result.entries) {
1225
+ const line = ` ${icon[entry.level]} ${entry.check}: ${entry.message}`;
1226
+ if (entry.level === "error") console.error(line);
1227
+ else if (entry.level === "warn") console.warn(line);
1228
+ else console.log(line);
1229
+ if (entry.fix) console.log(` fix: ${entry.fix}`);
1230
+ }
1231
+ if (result.ok) console.log("[verbaly] setup looks healthy ✓");
1232
+ else {
1233
+ console.error("[verbaly] doctor found problems");
1234
+ process.exitCode = 1;
1235
+ }
1236
+ return;
1237
+ }
1107
1238
  if (command === "extract") {
1108
1239
  const registry = await extractProject(cfg);
1109
1240
  const catalogs = loadCatalogs(cfg);
@@ -1177,7 +1308,7 @@ async function main() {
1177
1308
  async function resolveProvider(cfg, model) {
1178
1309
  const configured = cfg.translate.provider;
1179
1310
  if (typeof configured === "function") return configured;
1180
- const { claudeProvider } = await import("./claude-BhP-eK5E.js");
1311
+ const { claudeProvider } = await import("./claude-Bi_Ex2hm.js");
1181
1312
  return claudeProvider({ model: model ?? cfg.translate.model });
1182
1313
  }
1183
1314
  main().catch((error) => {