@dialexjs/cli 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,27 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { B as listLocales, C as isStale, D as record, E as readLock, G as saveDictionaryFile, K as setString, L as getString, M as scanFiles, N as TODO_PREFIX, O as writeLock, P as copyLeaf, R as hasPath, T as lockPath, V as loadDictionaryFile, a as renderGenerated, d as resolveDialexConfig, f as logger, i as generateDictionaries, m as analyzeProject, o as runGenerate, r as toKey, t as loadProject, w as lockFor, x as LOCK_FILE, z as listLeaves } from "./project-B27699T2.mjs";
2
+ import { A as scanFiles, B as listLeaves, C as lockPath, E as writeLock, G as saveDictionaryFile, H as loadDictionaryFile, I as copyLeaf, K as TODO_PREFIX, L as setString, R as getString, S as lockFor, T as record, V as listLocales, a as generateDictionaries, d as resolveDialexConfig, f as logger, i as runGenerate, p as analyzeProject, r as toKey, s as renderGenerated, t as loadProject, w as readLock, x as isStale, y as LOCK_FILE, z as hasPath } from "./project-BTpZGNjh.mjs";
3
3
  import { Command } from "commander";
4
- import fs from "node:fs";
5
4
  import path from "node:path";
6
5
  import pc from "picocolors";
7
- import { fileURLToPath } from "node:url";
6
+ import fs from "node:fs";
8
7
  import { loadFile, parseModule, writeFile } from "magicast";
8
+ import { fileURLToPath } from "node:url";
9
9
  import { argumentSignature, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
10
10
  import * as p from "@clack/prompts";
11
11
  import { addNuxtModule } from "magicast/helpers";
12
12
  //#region package.json
13
- var version = "0.9.1";
13
+ var version = "0.9.2";
14
14
  //#endregion
15
- //#region src/commands/check.ts
16
- function addIssue(diag, issue) {
17
- const level = issue.level === "error" ? "error" : "warning";
18
- (level === "error" ? diag.errors : diag.warnings).push(issue.message);
19
- diag.annotations.push({
20
- level,
21
- message: issue.message,
22
- line: issue.range ? issue.range.start.line + 1 : void 0
23
- });
24
- }
15
+ //#region src/commands/check/fix.ts
25
16
  /**
26
17
  * Inserts keys that exist in one locale but not in another, copying the default-locale value
27
18
  * (strings get a `[TODO]` prefix). Returns how many keys were inserted.
@@ -49,9 +40,22 @@ async function applyFixes(files, configuredLocales, defaultLocale) {
49
40
  }
50
41
  return fixed;
51
42
  }
43
+ //#endregion
44
+ //#region src/commands/check/report.ts
45
+ function addIssue(diag, issue) {
46
+ const level = issue.level === "error" ? "error" : "warning";
47
+ (level === "error" ? diag.errors : diag.warnings).push(issue.message);
48
+ diag.annotations.push({
49
+ level,
50
+ message: issue.message,
51
+ line: issue.range ? issue.range.start.line + 1 : void 0
52
+ });
53
+ }
52
54
  function escapeAnnotation(value) {
53
55
  return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
54
56
  }
57
+ //#endregion
58
+ //#region src/commands/check/stale-generated.ts
55
59
  /**
56
60
  * What a formatter (Prettier, Biome, oxfmt) can change without changing the code: whitespace,
57
61
  * quote style, quoted object keys and trailing commas. Comparing this form keeps `check` quiet
@@ -108,6 +112,8 @@ function staleGeneratedFiles(root, options) {
108
112
  rewritten
109
113
  };
110
114
  }
115
+ //#endregion
116
+ //#region src/commands/check/index.ts
111
117
  async function runCheck(options = {}) {
112
118
  const root = options.cwd || process.cwd();
113
119
  const quiet = options.json === true;
@@ -300,7 +306,7 @@ ${units.join("\n")}
300
306
  return result;
301
307
  }
302
308
  //#endregion
303
- //#region src/commands/import.ts
309
+ //#region src/commands/import/formats.ts
304
310
  const unescapeXml = (value) => value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
305
311
  function parseCsv(text) {
306
312
  const rows = [];
@@ -383,6 +389,8 @@ function inferFormat(file) {
383
389
  if (ext === ".xlf" || ext === ".xliff") return "xliff";
384
390
  throw new Error(`Cannot infer format from "${ext}". Pass --format json|csv|xliff.`);
385
391
  }
392
+ //#endregion
393
+ //#region src/commands/import/icu-guard.ts
386
394
  /**
387
395
  * Why `value` must not replace the current text, if it is an ICU message that is invalid or uses
388
396
  * different arguments than the same key in another locale.
@@ -408,6 +416,8 @@ function icuProblem(df, defaultLocale, locale, keyPath, value) {
408
416
  }
409
417
  }
410
418
  }
419
+ //#endregion
420
+ //#region src/commands/import/index.ts
411
421
  async function runImport(options) {
412
422
  const root = options.cwd || process.cwd();
413
423
  const file = path.resolve(root, options.file);
@@ -627,6 +637,125 @@ function detectFramework(root) {
627
637
  };
628
638
  }
629
639
  //#endregion
640
+ //#region src/commands/init/ai-setup.ts
641
+ /** Writes `.agents/skills/dialex/SKILL.md`, merges `.mcp.json` and adds a note to AGENTS.md. */
642
+ function setupAiSupport(root) {
643
+ try {
644
+ const skillDir = path.join(root, ".agents/skills/dialex");
645
+ fs.mkdirSync(skillDir, { recursive: true });
646
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
647
+ name: dialex
648
+ description: Guide for internationalizing JavaScript and TypeScript apps with Dialex. Use when adding translations, creating content dictionaries, configuring locales, or using Dialex with frameworks like Next.js, React, Express, Hono, Fastify, SvelteKit, and Nuxt.
649
+ ---
650
+
651
+ # Dialex Internationalization
652
+
653
+ Dialex is a high-performance, type-safe internationalization toolchain for JavaScript and TypeScript.
654
+
655
+ ## Core Concepts
656
+ - Dictionaries are defined with \`defineDictionary("name", { ... })\` in \`*.content.ts\` files.
657
+ - Keys must match across all locales.
658
+ - ICU message syntax is supported for plurals and selects.
659
+ - Parity is validated at compile time with \`dialex check\`.
660
+
661
+ ## Useful Commands
662
+ - \`dialex check --json\`: Check dictionary parity and references.
663
+ - \`dialex check --fix\`: Automatically insert missing keys marked with [TODO].
664
+ - \`dialex generate\`: Rebuild standalone dictionary bundle and types.
665
+ - \`dialex translate --locale <locale>\`: Automatically translate missing keys.
666
+ `, "utf-8");
667
+ const mcpConfigPath = path.join(root, ".mcp.json");
668
+ let mcpConfig = {};
669
+ if (fs.existsSync(mcpConfigPath)) try {
670
+ mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
671
+ } catch {
672
+ mcpConfig = {};
673
+ }
674
+ mcpConfig.mcpServers = mcpConfig.mcpServers || {};
675
+ mcpConfig.mcpServers.dialex = mcpConfig.mcpServers.dialex || {
676
+ command: "npx",
677
+ args: ["@dialexjs/mcp"]
678
+ };
679
+ fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
680
+ const agentsMdPath = path.join(root, "AGENTS.md");
681
+ const dialexAgentSnippet = `
682
+ ## Internationalization (Dialex)
683
+
684
+ This project uses [Dialex](https://github.com/awaiden/dialex) for type-safe internationalization.
685
+ - Dictionaries are in \`*.content.ts\` files using \`defineDictionary\`.
686
+ - Run \`dialex check --json\` to verify parity.
687
+ - Run \`dialex check --fix\` to insert missing keys marked with \`[TODO]\`.
688
+ - Run \`dx generate\` (alias of \`dialex\`) or \`npm run dx:generate\` to compile dictionary bundles and update types.
689
+ `;
690
+ if (!fs.existsSync(agentsMdPath)) fs.writeFileSync(agentsMdPath, dialexAgentSnippet.trimStart(), "utf-8");
691
+ else {
692
+ const existing = fs.readFileSync(agentsMdPath, "utf-8");
693
+ if (!existing.includes("Dialex")) fs.writeFileSync(agentsMdPath, existing.trimEnd() + "\n" + dialexAgentSnippet, "utf-8");
694
+ }
695
+ logger.success("Configured AI assistant support (.agents/skills, .mcp.json, AGENTS.md)");
696
+ } catch (err) {
697
+ logger.warn(`Could not finish AI configuration: ${err.message || String(err)}`);
698
+ }
699
+ }
700
+ //#endregion
701
+ //#region src/commands/init/config-file.ts
702
+ /** Creates `dialex.config.ts`, or updates the locales of the one that exists. */
703
+ async function writeDialexConfig(root, framework, defaultLocale, localeList) {
704
+ const configPath = fs.existsSync(path.join(root, "dialex.config.ts")) ? path.join(root, "dialex.config.ts") : fs.existsSync(path.join(root, "i18n.config.ts")) ? path.join(root, "i18n.config.ts") : path.join(root, "dialex.config.ts");
705
+ const configFileName = path.basename(configPath);
706
+ const generatedOutput = framework === "react-router" ? "app/dialex.generated.ts" : framework === "adonis" ? "start/dialex.generated.ts" : void 0;
707
+ try {
708
+ if (fs.existsSync(configPath)) {
709
+ const mod = await loadFile(configPath);
710
+ if (mod && mod.exports.default) {
711
+ if (mod.exports.default.$args && mod.exports.default.$args[0]) {
712
+ mod.exports.default.$args[0].defaultLocale = defaultLocale;
713
+ mod.exports.default.$args[0].locales = localeList;
714
+ if (generatedOutput) mod.exports.default.$args[0].output = generatedOutput;
715
+ }
716
+ await writeFile(mod, configPath);
717
+ logger.success(`Updated ${pc.bold(configFileName)} via Magicast`);
718
+ }
719
+ } else {
720
+ const mod = parseModule(`import { defineConfig } from "dialexjs";
721
+
722
+ export default defineConfig({});
723
+ `);
724
+ mod.exports.default.$args[0].defaultLocale = defaultLocale;
725
+ mod.exports.default.$args[0].locales = localeList;
726
+ if (generatedOutput) mod.exports.default.$args[0].output = generatedOutput;
727
+ await writeFile(mod, configPath);
728
+ logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
729
+ }
730
+ } catch (err) {
731
+ logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
732
+ const fallback = `import { defineConfig } from "dialexjs";
733
+
734
+ export default defineConfig({
735
+ defaultLocale: "${defaultLocale}",
736
+ locales: [${localeList.map((l) => `"${l}"`).join(", ")}],${generatedOutput ? `\n output: "${generatedOutput}",` : ""}
737
+ });
738
+ `;
739
+ fs.writeFileSync(configPath, fallback, "utf-8");
740
+ }
741
+ }
742
+ /** Nuxt reads the generated file through its module; it has to be registered in the Nuxt config. */
743
+ async function registerNuxtModule(root) {
744
+ const nuxtConfig = [
745
+ "nuxt.config.ts",
746
+ "nuxt.config.js",
747
+ "nuxt.config.mjs"
748
+ ].map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
749
+ if (nuxtConfig) try {
750
+ const nMod = await loadFile(nuxtConfig);
751
+ addNuxtModule(nMod, "dialexjs/nuxt", "dialex", {});
752
+ await writeFile(nMod, nuxtConfig);
753
+ logger.success(`Registered dialexjs/nuxt in ${pc.bold(path.basename(nuxtConfig))} via Magicast`);
754
+ } catch {
755
+ logger.warn("Could not update the Nuxt config; add the dialexjs/nuxt module manually.");
756
+ }
757
+ }
758
+ //#endregion
630
759
  //#region src/utils/package-json.ts
631
760
  /** Version of this CLI, which the other Dialex packages share. Read from the nearest package.json. */
632
761
  function cliVersion() {
@@ -678,237 +807,9 @@ function addRequiredPackages(pkg, version = cliVersion()) {
678
807
  return added;
679
808
  }
680
809
  //#endregion
681
- //#region src/commands/init.ts
682
- async function runInit(options = {}) {
683
- const root = options.cwd || process.cwd();
684
- const detected = detectFramework(root);
685
- const isInteractive = !options.yes && process.stdin.isTTY;
686
- let framework = options.framework || detected.framework;
687
- let defaultLocale = options.defaultLocale || "en";
688
- let localesInput = options.locales || "en,tr";
689
- if (isInteractive) {
690
- p.intro(pc.bgCyan(pc.black(" dialex init ")));
691
- const frameworkOptions = [
692
- {
693
- value: "hono",
694
- label: "Hono",
695
- hint: detected.framework === "hono" ? "detected" : void 0
696
- },
697
- {
698
- value: "express",
699
- label: "Express",
700
- hint: detected.framework === "express" ? "detected" : void 0
701
- },
702
- {
703
- value: "fastify",
704
- label: "Fastify",
705
- hint: detected.framework === "fastify" ? "detected" : void 0
706
- },
707
- {
708
- value: "koa",
709
- label: "Koa",
710
- hint: detected.framework === "koa" ? "detected" : void 0
711
- },
712
- {
713
- value: "nestjs",
714
- label: "NestJS",
715
- hint: detected.framework === "nestjs" ? "detected" : void 0
716
- },
717
- {
718
- value: "next",
719
- label: "Next.js",
720
- hint: detected.framework === "next" ? "detected" : void 0
721
- },
722
- {
723
- value: "react",
724
- label: "React / Vite",
725
- hint: detected.framework === "react" ? "detected" : void 0
726
- },
727
- {
728
- value: "elysia",
729
- label: "Elysia",
730
- hint: detected.framework === "elysia" ? "detected" : void 0
731
- },
732
- {
733
- value: "sveltekit",
734
- label: "SvelteKit",
735
- hint: detected.framework === "sveltekit" ? "detected" : void 0
736
- },
737
- {
738
- value: "astro",
739
- label: "Astro",
740
- hint: detected.framework === "astro" ? "detected" : void 0
741
- },
742
- {
743
- value: "vue",
744
- label: "Vue / Vite",
745
- hint: detected.framework === "vue" ? "detected" : void 0
746
- },
747
- {
748
- value: "nuxt",
749
- label: "Nuxt",
750
- hint: detected.framework === "nuxt" ? "detected" : void 0
751
- },
752
- {
753
- value: "svelte",
754
- label: "Svelte / Vite",
755
- hint: detected.framework === "svelte" ? "detected" : void 0
756
- },
757
- {
758
- value: "solid",
759
- label: "Solid / Vite",
760
- hint: detected.framework === "solid" ? "detected" : void 0
761
- },
762
- {
763
- value: "react-router",
764
- label: "React Router",
765
- hint: detected.framework === "react-router" ? "detected" : void 0
766
- },
767
- {
768
- value: "react-native",
769
- label: "React Native / Expo",
770
- hint: detected.framework === "react-native" ? "detected" : void 0
771
- },
772
- {
773
- value: "preact",
774
- label: "Preact / Vite",
775
- hint: detected.framework === "preact" ? "detected" : void 0
776
- },
777
- {
778
- value: "qwik",
779
- label: "Qwik",
780
- hint: detected.framework === "qwik" ? "detected" : void 0
781
- },
782
- {
783
- value: "lit",
784
- label: "Lit / Web Components",
785
- hint: detected.framework === "lit" ? "detected" : void 0
786
- },
787
- {
788
- value: "hapi",
789
- label: "Hapi",
790
- hint: detected.framework === "hapi" ? "detected" : void 0
791
- },
792
- {
793
- value: "adonis",
794
- label: "AdonisJS",
795
- hint: detected.framework === "adonis" ? "detected" : void 0
796
- },
797
- {
798
- value: "trpc",
799
- label: "tRPC",
800
- hint: detected.framework === "trpc" ? "detected" : void 0
801
- }
802
- ];
803
- const selectedFramework = await p.select({
804
- message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
805
- options: frameworkOptions,
806
- initialValue: detected.framework
807
- });
808
- if (p.isCancel(selectedFramework)) {
809
- p.cancel("Operation cancelled.");
810
- return;
811
- }
812
- framework = selectedFramework;
813
- const inputDefault = await p.text({
814
- message: "What is your default locale?",
815
- initialValue: "en",
816
- validate: (v) => !v || !v.trim() ? "Default locale cannot be empty" : void 0
817
- });
818
- if (p.isCancel(inputDefault)) {
819
- p.cancel("Operation cancelled.");
820
- return;
821
- }
822
- defaultLocale = inputDefault.trim();
823
- const inputLocales = await p.text({
824
- message: "Comma-separated list of supported locales:",
825
- initialValue: `${defaultLocale},tr`,
826
- validate: (v) => !v || !v.trim() ? "Locales list cannot be empty" : void 0
827
- });
828
- if (p.isCancel(inputLocales)) {
829
- p.cancel("Operation cancelled.");
830
- return;
831
- }
832
- localesInput = inputLocales.trim();
833
- if (options.ai === void 0 && options.noAi === void 0) {
834
- const confirmAi = await p.confirm({
835
- message: "Configure AI agent support (Claude/Cursor skills, .mcp.json, AGENTS.md)?",
836
- initialValue: true
837
- });
838
- if (!p.isCancel(confirmAi)) options.ai = confirmAi;
839
- }
840
- } else logger.info(`Auto-detected framework: ${pc.bold(framework)} (${detected.matchedRule})`);
841
- const localeList = Array.from(/* @__PURE__ */ new Set([defaultLocale, ...localesInput.split(",").map((s) => s.trim()).filter(Boolean)]));
842
- const configPath = fs.existsSync(path.join(root, "dialex.config.ts")) ? path.join(root, "dialex.config.ts") : fs.existsSync(path.join(root, "i18n.config.ts")) ? path.join(root, "i18n.config.ts") : path.join(root, "dialex.config.ts");
843
- const configFileName = path.basename(configPath);
844
- const generatedOutput = framework === "react-router" ? "app/dialex.generated.ts" : framework === "adonis" ? "start/dialex.generated.ts" : void 0;
845
- try {
846
- if (fs.existsSync(configPath)) {
847
- const mod = await loadFile(configPath);
848
- if (mod && mod.exports.default) {
849
- if (mod.exports.default.$args && mod.exports.default.$args[0]) {
850
- mod.exports.default.$args[0].defaultLocale = defaultLocale;
851
- mod.exports.default.$args[0].locales = localeList;
852
- if (generatedOutput) mod.exports.default.$args[0].output = generatedOutput;
853
- }
854
- await writeFile(mod, configPath);
855
- logger.success(`Updated ${pc.bold(configFileName)} via Magicast`);
856
- }
857
- } else {
858
- const mod = parseModule(`import { defineConfig } from "dialexjs";
859
-
860
- export default defineConfig({});
861
- `);
862
- mod.exports.default.$args[0].defaultLocale = defaultLocale;
863
- mod.exports.default.$args[0].locales = localeList;
864
- if (generatedOutput) mod.exports.default.$args[0].output = generatedOutput;
865
- await writeFile(mod, configPath);
866
- logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
867
- }
868
- } catch (err) {
869
- logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
870
- const fallback = `import { defineConfig } from "dialexjs";
871
-
872
- export default defineConfig({
873
- defaultLocale: "${defaultLocale}",
874
- locales: [${localeList.map((l) => `"${l}"`).join(", ")}],${generatedOutput ? `\n output: "${generatedOutput}",` : ""}
875
- });
876
- `;
877
- fs.writeFileSync(configPath, fallback, "utf-8");
878
- }
879
- if (framework === "nuxt") {
880
- const nuxtConfig = [
881
- "nuxt.config.ts",
882
- "nuxt.config.js",
883
- "nuxt.config.mjs"
884
- ].map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
885
- if (nuxtConfig) try {
886
- const nMod = await loadFile(nuxtConfig);
887
- addNuxtModule(nMod, "dialexjs/nuxt", "dialex", {});
888
- await writeFile(nMod, nuxtConfig);
889
- logger.success(`Registered dialexjs/nuxt in ${pc.bold(path.basename(nuxtConfig))} via Magicast`);
890
- } catch {
891
- logger.warn("Could not update the Nuxt config; add the dialexjs/nuxt module manually.");
892
- }
893
- }
894
- const targetDir = (framework === "react-router" || framework === "adonis") && fs.existsSync(path.join(root, "app")) ? path.join(root, "app") : fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
895
- const dictPath = path.join(targetDir, "home.content.ts");
896
- if (!fs.existsSync(dictPath)) {
897
- const dictContent = `import { defineDictionary } from "dialexjs";
898
-
899
- export default defineDictionary("home", {
900
- ${localeList.map((loc) => ` ${loc}: {
901
- title: "Welcome to Dialex (${loc})!",
902
- description: "Type-safe internationalization.",
903
- greeting: (name: string) => "Hello, " + name + "!",
904
- },`).join("\n")}
905
- });
906
- `;
907
- fs.writeFileSync(dictPath, dictContent, "utf-8");
908
- logger.success(`Created starter dictionary at ${pc.bold(path.relative(root, dictPath))}`);
909
- } else logger.info("home.content.ts already exists, skipping creation.");
910
- generateDictionaries(root, { static: true });
911
- logger.success("Generated types and standalone dictionary registry!");
810
+ //#region src/commands/init/dependencies.ts
811
+ /** Adds `dialexjs` and `@dialexjs/cli` to package.json, and the `dx:generate` script. */
812
+ function updatePackageJson(root) {
912
813
  const pkgPath = path.join(root, "package.json");
913
814
  if (fs.existsSync(pkgPath)) try {
914
815
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -926,67 +827,93 @@ ${localeList.map((loc) => ` ${loc}: {
926
827
  logger.warn("Could not update package.json; add dialexjs and @dialexjs/cli manually.");
927
828
  }
928
829
  else logger.warn("No package.json found; install dialexjs and @dialexjs/cli in your project.");
929
- if (options.ai === true || options.yes && options.ai !== false && options.noAi !== true) try {
930
- const skillDir = path.join(root, ".agents/skills/dialex");
931
- fs.mkdirSync(skillDir, { recursive: true });
932
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
933
- name: dialex
934
- description: Guide for internationalizing JavaScript and TypeScript apps with Dialex. Use when adding translations, creating content dictionaries, configuring locales, or using Dialex with frameworks like Next.js, React, Express, Hono, Fastify, SvelteKit, and Nuxt.
935
- ---
936
-
937
- # Dialex Internationalization
938
-
939
- Dialex is a high-performance, type-safe internationalization toolchain for JavaScript and TypeScript.
940
-
941
- ## Core Concepts
942
- - Dictionaries are defined with \`defineDictionary("name", { ... })\` in \`*.content.ts\` files.
943
- - Keys must match across all locales.
944
- - ICU message syntax is supported for plurals and selects.
945
- - Parity is validated at compile time with \`dialex check\`.
946
-
947
- ## Useful Commands
948
- - \`dialex check --json\`: Check dictionary parity and references.
949
- - \`dialex check --fix\`: Automatically insert missing keys marked with [TODO].
950
- - \`dialex generate\`: Rebuild standalone dictionary bundle and types.
951
- - \`dialex translate --locale <locale>\`: Automatically translate missing keys.
952
- `, "utf-8");
953
- const mcpConfigPath = path.join(root, ".mcp.json");
954
- let mcpConfig = {};
955
- if (fs.existsSync(mcpConfigPath)) try {
956
- mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
957
- } catch {
958
- mcpConfig = {};
959
- }
960
- mcpConfig.mcpServers = mcpConfig.mcpServers || {};
961
- mcpConfig.mcpServers.dialex = mcpConfig.mcpServers.dialex || {
962
- command: "npx",
963
- args: ["@dialexjs/mcp"]
964
- };
965
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
966
- const agentsMdPath = path.join(root, "AGENTS.md");
967
- const dialexAgentSnippet = `
968
- ## Internationalization (Dialex)
969
-
970
- This project uses [Dialex](https://github.com/awaiden/dialex) for type-safe internationalization.
971
- - Dictionaries are in \`*.content.ts\` files using \`defineDictionary\`.
972
- - Run \`dialex check --json\` to verify parity.
973
- - Run \`dialex check --fix\` to insert missing keys marked with \`[TODO]\`.
974
- - Run \`dx generate\` (alias of \`dialex\`) or \`npm run dx:generate\` to compile dictionary bundles and update types.
975
- `;
976
- if (!fs.existsSync(agentsMdPath)) fs.writeFileSync(agentsMdPath, dialexAgentSnippet.trimStart(), "utf-8");
977
- else {
978
- const existing = fs.readFileSync(agentsMdPath, "utf-8");
979
- if (!existing.includes("Dialex")) fs.writeFileSync(agentsMdPath, existing.trimEnd() + "\n" + dialexAgentSnippet, "utf-8");
980
- }
981
- logger.success("Configured AI assistant support (.agents/skills, .mcp.json, AGENTS.md)");
982
- } catch (err) {
983
- logger.warn(`Could not finish AI configuration: ${err.message || String(err)}`);
830
+ }
831
+ //#endregion
832
+ //#region src/commands/init/prompts.ts
833
+ /** The frameworks `dialex init` offers, in the order of the prompt: value and label. */
834
+ const FRAMEWORKS = [
835
+ ["hono", "Hono"],
836
+ ["express", "Express"],
837
+ ["fastify", "Fastify"],
838
+ ["koa", "Koa"],
839
+ ["nestjs", "NestJS"],
840
+ ["next", "Next.js"],
841
+ ["react", "React / Vite"],
842
+ ["elysia", "Elysia"],
843
+ ["sveltekit", "SvelteKit"],
844
+ ["astro", "Astro"],
845
+ ["vue", "Vue / Vite"],
846
+ ["nuxt", "Nuxt"],
847
+ ["svelte", "Svelte / Vite"],
848
+ ["solid", "Solid / Vite"],
849
+ ["react-router", "React Router"],
850
+ ["react-native", "React Native / Expo"],
851
+ ["preact", "Preact / Vite"],
852
+ ["qwik", "Qwik"],
853
+ ["lit", "Lit / Web Components"],
854
+ ["hapi", "Hapi"],
855
+ ["adonis", "AdonisJS"],
856
+ ["trpc", "tRPC"]
857
+ ];
858
+ /**
859
+ * Asks for the framework, the locales and (when not decided by a flag) AI support. Returns
860
+ * `undefined` when the user cancels. May set `options.ai`.
861
+ */
862
+ async function askInit(options, detected) {
863
+ p.intro(pc.bgCyan(pc.black(" dialex init ")));
864
+ const frameworkOptions = FRAMEWORKS.map(([value, label]) => ({
865
+ value,
866
+ label,
867
+ hint: detected.framework === value ? "detected" : void 0
868
+ }));
869
+ const selectedFramework = await p.select({
870
+ message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
871
+ options: frameworkOptions,
872
+ initialValue: detected.framework
873
+ });
874
+ if (p.isCancel(selectedFramework)) {
875
+ p.cancel("Operation cancelled.");
876
+ return;
984
877
  }
985
- logger.log("");
986
- logger.info(pc.bold(`Quick start for ${framework.toUpperCase()}:`));
987
- switch (framework) {
988
- case "hono":
989
- logger.log(`
878
+ const framework = selectedFramework;
879
+ const inputDefault = await p.text({
880
+ message: "What is your default locale?",
881
+ initialValue: "en",
882
+ validate: (v) => !v || !v.trim() ? "Default locale cannot be empty" : void 0
883
+ });
884
+ if (p.isCancel(inputDefault)) {
885
+ p.cancel("Operation cancelled.");
886
+ return;
887
+ }
888
+ const defaultLocale = inputDefault.trim();
889
+ const inputLocales = await p.text({
890
+ message: "Comma-separated list of supported locales:",
891
+ initialValue: `${defaultLocale},tr`,
892
+ validate: (v) => !v || !v.trim() ? "Locales list cannot be empty" : void 0
893
+ });
894
+ if (p.isCancel(inputLocales)) {
895
+ p.cancel("Operation cancelled.");
896
+ return;
897
+ }
898
+ const localesInput = inputLocales.trim();
899
+ if (options.ai === void 0 && options.noAi === void 0) {
900
+ const confirmAi = await p.confirm({
901
+ message: "Configure AI agent support (Claude/Cursor skills, .mcp.json, AGENTS.md)?",
902
+ initialValue: true
903
+ });
904
+ if (!p.isCancel(confirmAi)) options.ai = confirmAi;
905
+ }
906
+ return {
907
+ framework,
908
+ defaultLocale,
909
+ localesInput
910
+ };
911
+ }
912
+ //#endregion
913
+ //#region src/commands/init/quickstart.ts
914
+ /** What `dialex init` prints for each framework, after the files are written. */
915
+ const SNIPPETS = {
916
+ hono: `
990
917
  import { Hono } from "hono";
991
918
  import { dialexHono } from "dialexjs/hono";
992
919
  import { dialex } from "./src/dialex.generated.js";
@@ -998,10 +925,8 @@ app.get("/", (c) => {
998
925
  const dict = c.var.getDictionary("home");
999
926
  return c.json({ title: dict.title });
1000
927
  });
1001
- `);
1002
- break;
1003
- case "fastify":
1004
- logger.log(`
928
+ `,
929
+ fastify: `
1005
930
  import Fastify from "fastify";
1006
931
  import { dialexPlugin } from "dialexjs/fastify";
1007
932
  import { dialex } from "./src/dialex.generated.js";
@@ -1013,10 +938,8 @@ app.get("/", (req) => {
1013
938
  const dict = req.getDictionary("home");
1014
939
  return { title: dict.title };
1015
940
  });
1016
- `);
1017
- break;
1018
- case "express":
1019
- logger.log(`
941
+ `,
942
+ express: `
1020
943
  import express from "express";
1021
944
  import { dialexExpress } from "dialexjs/express";
1022
945
  import { dialex } from "./src/dialex.generated.js";
@@ -1028,10 +951,8 @@ app.get("/", (req, res) => {
1028
951
  const dict = req.getDictionary("home");
1029
952
  res.json({ title: dict.title });
1030
953
  });
1031
- `);
1032
- break;
1033
- case "koa":
1034
- logger.log(`
954
+ `,
955
+ koa: `
1035
956
  import Koa from "koa";
1036
957
  import { dialexKoa } from "dialexjs/koa";
1037
958
  import { dialex } from "./src/dialex.generated.js";
@@ -1043,10 +964,8 @@ app.use((ctx) => {
1043
964
  const dict = ctx.getDictionary("home");
1044
965
  ctx.body = { title: dict.title };
1045
966
  });
1046
- `);
1047
- break;
1048
- case "nestjs":
1049
- logger.log(`
967
+ `,
968
+ nestjs: `
1050
969
  import { Module } from "@nestjs/common";
1051
970
  import { DialexModule } from "dialexjs/nestjs";
1052
971
  import { dialex } from "./src/dialex.generated.js";
@@ -1055,10 +974,8 @@ import { dialex } from "./src/dialex.generated.js";
1055
974
  imports: [DialexModule.forRoot({ ...dialex })],
1056
975
  })
1057
976
  export class AppModule {}
1058
- `);
1059
- break;
1060
- case "next":
1061
- logger.log(`
977
+ `,
978
+ next: `
1062
979
  // src/dialex.ts
1063
980
  import { createDialexServer } from "dialexjs/server";
1064
981
  import { dialex } from "./dialex.generated";
@@ -1079,10 +996,8 @@ export default async function Page({ params }: { params: Promise<{ locale: strin
1079
996
  // import { DialexProvider } from "dialexjs/react";
1080
997
  // import { dialex } from "./dialex.generated";
1081
998
  // export const Providers = ({ children }) => <DialexProvider {...dialex}>{children}</DialexProvider>;
1082
- `);
1083
- break;
1084
- case "react":
1085
- logger.log(`
999
+ `,
1000
+ react: `
1086
1001
  // src/main.tsx
1087
1002
  import { createRoot } from "react-dom/client";
1088
1003
  import { DialexProvider } from "dialexjs/react";
@@ -1096,10 +1011,8 @@ createRoot(document.getElementById("root")!).render(
1096
1011
  );
1097
1012
 
1098
1013
  // In components: const dict = useDictionary("home")
1099
- `);
1100
- break;
1101
- case "elysia":
1102
- logger.log(`
1014
+ `,
1015
+ elysia: `
1103
1016
  import { Elysia } from "elysia";
1104
1017
  import { dialexElysia } from "dialexjs/elysia";
1105
1018
  import { dialex } from "./src/dialex.generated.js";
@@ -1108,10 +1021,8 @@ new Elysia()
1108
1021
  .use(dialexElysia({ ...dialex }))
1109
1022
  .get("/", ({ getDictionary }) => ({ title: getDictionary("home").title }))
1110
1023
  .listen(3000);
1111
- `);
1112
- break;
1113
- case "sveltekit":
1114
- logger.log(`
1024
+ `,
1025
+ sveltekit: `
1115
1026
  // src/hooks.server.ts
1116
1027
  import { dialexHandle } from "dialexjs/sveltekit";
1117
1028
  import { dialex } from "./dialex.generated.js";
@@ -1120,10 +1031,8 @@ export const handle = dialexHandle({ ...dialex });
1120
1031
 
1121
1032
  // src/app.html: <html lang="%dialex.lang%">
1122
1033
  // In load functions: locals.getDictionary("home")
1123
- `);
1124
- break;
1125
- case "astro":
1126
- logger.log(`
1034
+ `,
1035
+ astro: `
1127
1036
  // src/middleware.ts
1128
1037
  import { dialexAstro } from "dialexjs/astro";
1129
1038
  import { dialex } from "./dialex.generated.js";
@@ -1131,10 +1040,8 @@ import { dialex } from "./dialex.generated.js";
1131
1040
  export const onRequest = dialexAstro({ ...dialex });
1132
1041
 
1133
1042
  // In pages: Astro.locals.getDictionary("home")
1134
- `);
1135
- break;
1136
- case "vue":
1137
- logger.log(`
1043
+ `,
1044
+ vue: `
1138
1045
  // src/main.ts
1139
1046
  import { createApp } from "vue";
1140
1047
  import { createDialex } from "dialexjs/vue";
@@ -1144,10 +1051,8 @@ import App from "./App.vue";
1144
1051
  createApp(App).use(createDialex({ ...dialex })).mount("#app");
1145
1052
 
1146
1053
  // In components: const dict = useDictionary("home")
1147
- `);
1148
- break;
1149
- case "svelte":
1150
- logger.log(`
1054
+ `,
1055
+ svelte: `
1151
1056
  <!-- src/App.svelte -->
1152
1057
  <script lang="ts">
1153
1058
  import { provideDialex, useDictionary } from "dialexjs/svelte";
@@ -1159,10 +1064,8 @@ createApp(App).use(createDialex({ ...dialex })).mount("#app");
1159
1064
 
1160
1065
  <h1>{$home.title}</h1>
1161
1066
  <button onclick={() => setLocale("tr")}>TR</button>
1162
- `);
1163
- break;
1164
- case "solid":
1165
- logger.log(`
1067
+ `,
1068
+ solid: `
1166
1069
  // src/index.tsx
1167
1070
  import { render } from "solid-js/web";
1168
1071
  import { DialexProvider } from "dialexjs/solid";
@@ -1179,10 +1082,8 @@ render(
1179
1082
  );
1180
1083
 
1181
1084
  // In components: const home = useDictionary("home"); then {home().title}
1182
- `);
1183
- break;
1184
- case "react-router":
1185
- logger.log(`
1085
+ `,
1086
+ "react-router": `
1186
1087
  // app/dialex.server.ts
1187
1088
  import { createDialexHandler } from "dialexjs/web";
1188
1089
  import { dialex } from "./dialex.generated";
@@ -1208,10 +1109,8 @@ export default function App() {
1208
1109
  </DialexProvider>
1209
1110
  );
1210
1111
  }
1211
- `);
1212
- break;
1213
- case "preact":
1214
- logger.log(`
1112
+ `,
1113
+ preact: `
1215
1114
  // src/index.tsx
1216
1115
  import { render } from "preact";
1217
1116
  import { DialexProvider } from "dialexjs/preact";
@@ -1226,10 +1125,8 @@ render(
1226
1125
  );
1227
1126
 
1228
1127
  // In components: const dict = useDictionary("home")
1229
- `);
1230
- break;
1231
- case "qwik":
1232
- logger.log(`
1128
+ `,
1129
+ qwik: `
1233
1130
  // src/i18n.ts
1234
1131
  import { createDialexQwik } from "dialexjs/qwik";
1235
1132
  import { dialex } from "./dialex.generated";
@@ -1244,10 +1141,8 @@ export default component$(() => {
1244
1141
  });
1245
1142
 
1246
1143
  // In components: const home = useDictionary("home"); then {home().title}
1247
- `);
1248
- break;
1249
- case "lit":
1250
- logger.log(`
1144
+ `,
1145
+ lit: `
1251
1146
  // src/i18n.ts
1252
1147
  import { createDialex } from "dialexjs/lit";
1253
1148
  import { dialex } from "./dialex.generated";
@@ -1266,10 +1161,8 @@ class MyElement extends LitElement {
1266
1161
  return html\`<h1>\${this.dx.dictionary("home").title}</h1>\`;
1267
1162
  }
1268
1163
  }
1269
- `);
1270
- break;
1271
- case "react-native":
1272
- logger.log(`
1164
+ `,
1165
+ "react-native": `
1273
1166
  // App.tsx
1274
1167
  import AsyncStorage from "@react-native-async-storage/async-storage";
1275
1168
  import { getLocales } from "expo-localization";
@@ -1289,10 +1182,8 @@ export default function App() {
1289
1182
  }
1290
1183
 
1291
1184
  // In components: const dict = useDictionary("home")
1292
- `);
1293
- break;
1294
- case "hapi":
1295
- logger.log(`
1185
+ `,
1186
+ hapi: `
1296
1187
  import Hapi from "@hapi/hapi";
1297
1188
  import { dialexPlugin } from "dialexjs/hapi";
1298
1189
  import { dialex } from "./src/dialex.generated.js";
@@ -1305,10 +1196,8 @@ server.route({
1305
1196
  path: "/",
1306
1197
  handler: (request) => ({ title: request.getDictionary("home").title }),
1307
1198
  });
1308
- `);
1309
- break;
1310
- case "adonis":
1311
- logger.log(`
1199
+ `,
1200
+ adonis: `
1312
1201
  // app/middleware/dialex_middleware.ts
1313
1202
  import { createDialexMiddleware } from "dialexjs/adonis";
1314
1203
  import { dialex } from "#start/dialex.generated";
@@ -1319,10 +1208,8 @@ export default createDialexMiddleware({ ...dialex });
1319
1208
  router.use([() => import("#middleware/dialex_middleware")]);
1320
1209
 
1321
1210
  // In handlers: ctx.locale, ctx.getDictionary("home")
1322
- `);
1323
- break;
1324
- case "trpc":
1325
- logger.log(`
1211
+ `,
1212
+ trpc: `
1326
1213
  // src/trpc.ts
1327
1214
  import { initTRPC } from "@trpc/server";
1328
1215
  import { createDialexContext } from "dialexjs/trpc";
@@ -1337,23 +1224,70 @@ const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create()
1337
1224
  export const appRouter = t.router({
1338
1225
  greeting: t.procedure.query(({ ctx }) => ctx.getDictionary("home").title),
1339
1226
  });
1340
- `);
1341
- break;
1342
- case "nuxt":
1343
- logger.log(`
1227
+ `,
1228
+ nuxt: `
1344
1229
  // nuxt.config.ts
1345
1230
  export default defineNuxtConfig({
1346
1231
  modules: ["dialexjs/nuxt"],
1347
1232
  });
1348
1233
 
1349
1234
  // In components: const dict = useDictionary("home")
1350
- `);
1351
- break;
1352
- default: logger.log(`
1235
+ `
1236
+ };
1237
+ /** Printed for a framework without a snippet of its own. */
1238
+ const FALLBACK = `
1353
1239
  Import dictionaries and register your framework middleware to begin!
1354
1240
  Run \`npm run dx:generate\` whenever you add new dictionary files.
1355
- `);
1356
- }
1241
+ `;
1242
+ function printQuickstart(framework) {
1243
+ logger.log("");
1244
+ logger.info(pc.bold(`Quick start for ${framework.toUpperCase()}:`));
1245
+ logger.log(Object.hasOwn(SNIPPETS, framework) ? SNIPPETS[framework] : FALLBACK);
1246
+ }
1247
+ //#endregion
1248
+ //#region src/commands/init/starter-dictionary.ts
1249
+ /** Writes `home.content.ts` with one entry per locale, unless it is already there. */
1250
+ function createStarterDictionary(root, framework, localeList) {
1251
+ const targetDir = (framework === "react-router" || framework === "adonis") && fs.existsSync(path.join(root, "app")) ? path.join(root, "app") : fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
1252
+ const dictPath = path.join(targetDir, "home.content.ts");
1253
+ if (!fs.existsSync(dictPath)) {
1254
+ const dictContent = `import { defineDictionary } from "dialexjs";
1255
+
1256
+ export default defineDictionary("home", {
1257
+ ${localeList.map((loc) => ` ${loc}: {
1258
+ title: "Welcome to Dialex (${loc})!",
1259
+ description: "Type-safe internationalization.",
1260
+ greeting: (name: string) => "Hello, " + name + "!",
1261
+ },`).join("\n")}
1262
+ });
1263
+ `;
1264
+ fs.writeFileSync(dictPath, dictContent, "utf-8");
1265
+ logger.success(`Created starter dictionary at ${pc.bold(path.relative(root, dictPath))}`);
1266
+ } else logger.info("home.content.ts already exists, skipping creation.");
1267
+ }
1268
+ //#endregion
1269
+ //#region src/commands/init/index.ts
1270
+ async function runInit(options = {}) {
1271
+ const root = options.cwd || process.cwd();
1272
+ const detected = detectFramework(root);
1273
+ const isInteractive = !options.yes && process.stdin.isTTY;
1274
+ let framework = options.framework || detected.framework;
1275
+ let defaultLocale = options.defaultLocale || "en";
1276
+ let localesInput = options.locales || "en,tr";
1277
+ if (isInteractive) {
1278
+ const answers = await askInit(options, detected);
1279
+ if (!answers) return;
1280
+ ({framework, defaultLocale, localesInput} = answers);
1281
+ } else logger.info(`Auto-detected framework: ${pc.bold(framework)} (${detected.matchedRule})`);
1282
+ const localeList = Array.from(/* @__PURE__ */ new Set([defaultLocale, ...localesInput.split(",").map((s) => s.trim()).filter(Boolean)]));
1283
+ await writeDialexConfig(root, framework, defaultLocale, localeList);
1284
+ if (framework === "nuxt") await registerNuxtModule(root);
1285
+ createStarterDictionary(root, framework, localeList);
1286
+ generateDictionaries(root, { static: true });
1287
+ logger.success("Generated types and standalone dictionary registry!");
1288
+ updatePackageJson(root);
1289
+ if (options.ai === true || options.yes && options.ai !== false && options.noAi !== true) setupAiSupport(root);
1290
+ printQuickstart(framework);
1357
1291
  logger.info("Keep dialex.generated.ts current: run `dx generate --watch` while you develop, or use the Dialex VS Code extension, which regenerates on save.");
1358
1292
  if (isInteractive) p.outro(pc.green("✔ dialex is configured and ready to go!"));
1359
1293
  }