@dialexjs/cli 0.7.2 → 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/README.md +20 -1
- package/dist/api.d.mts +73 -32
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +2 -2
- package/dist/index.mjs +566 -328
- package/dist/index.mjs.map +1 -1
- package/dist/{project-Dz0FbydQ.mjs → project-BTpZGNjh.mjs} +625 -324
- package/dist/project-BTpZGNjh.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/project-Dz0FbydQ.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as scanFiles, C as lockPath, E as writeLock,
|
|
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
|
|
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.
|
|
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(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/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);
|
|
@@ -510,6 +520,16 @@ function detectFramework(root) {
|
|
|
510
520
|
confidence: "high",
|
|
511
521
|
matchedRule: "package.json dependencies (@nestjs/core)"
|
|
512
522
|
};
|
|
523
|
+
if ("@adonisjs/core" in allDeps) return {
|
|
524
|
+
framework: "adonis",
|
|
525
|
+
confidence: "high",
|
|
526
|
+
matchedRule: "package.json dependencies (@adonisjs/core)"
|
|
527
|
+
};
|
|
528
|
+
if ("@hapi/hapi" in allDeps) return {
|
|
529
|
+
framework: "hapi",
|
|
530
|
+
confidence: "high",
|
|
531
|
+
matchedRule: "package.json dependencies (@hapi/hapi)"
|
|
532
|
+
};
|
|
513
533
|
if ("fastify" in allDeps) return {
|
|
514
534
|
framework: "fastify",
|
|
515
535
|
confidence: "high",
|
|
@@ -535,11 +555,51 @@ function detectFramework(root) {
|
|
|
535
555
|
confidence: "high",
|
|
536
556
|
matchedRule: "package.json dependencies (hono)"
|
|
537
557
|
};
|
|
558
|
+
if ("@trpc/server" in allDeps) return {
|
|
559
|
+
framework: "trpc",
|
|
560
|
+
confidence: "high",
|
|
561
|
+
matchedRule: "package.json dependencies (@trpc/server)"
|
|
562
|
+
};
|
|
563
|
+
if ("@builder.io/qwik" in allDeps) return {
|
|
564
|
+
framework: "qwik",
|
|
565
|
+
confidence: "high",
|
|
566
|
+
matchedRule: "package.json dependencies (@builder.io/qwik)"
|
|
567
|
+
};
|
|
568
|
+
if ("react-native" in allDeps || "expo" in allDeps) return {
|
|
569
|
+
framework: "react-native",
|
|
570
|
+
confidence: "high",
|
|
571
|
+
matchedRule: "package.json dependencies (react-native/expo)"
|
|
572
|
+
};
|
|
573
|
+
if ("@react-router/dev" in allDeps) return {
|
|
574
|
+
framework: "react-router",
|
|
575
|
+
confidence: "high",
|
|
576
|
+
matchedRule: "package.json dependencies (@react-router/dev)"
|
|
577
|
+
};
|
|
578
|
+
if ("solid-js" in allDeps) return {
|
|
579
|
+
framework: "solid",
|
|
580
|
+
confidence: "high",
|
|
581
|
+
matchedRule: "package.json dependencies (solid-js)"
|
|
582
|
+
};
|
|
583
|
+
if ("svelte" in allDeps) return {
|
|
584
|
+
framework: "svelte",
|
|
585
|
+
confidence: "high",
|
|
586
|
+
matchedRule: "package.json dependencies (svelte)"
|
|
587
|
+
};
|
|
538
588
|
if ("vue" in allDeps && !("react" in allDeps)) return {
|
|
539
589
|
framework: "vue",
|
|
540
590
|
confidence: "high",
|
|
541
591
|
matchedRule: "package.json dependencies (vue)"
|
|
542
592
|
};
|
|
593
|
+
if ("preact" in allDeps) return {
|
|
594
|
+
framework: "preact",
|
|
595
|
+
confidence: "high",
|
|
596
|
+
matchedRule: "package.json dependencies (preact)"
|
|
597
|
+
};
|
|
598
|
+
if ("lit" in allDeps && !("react" in allDeps)) return {
|
|
599
|
+
framework: "lit",
|
|
600
|
+
confidence: "high",
|
|
601
|
+
matchedRule: "package.json dependencies (lit)"
|
|
602
|
+
};
|
|
543
603
|
if ("react" in allDeps || "vite" in allDeps) return {
|
|
544
604
|
framework: "react",
|
|
545
605
|
confidence: "high",
|
|
@@ -554,7 +614,8 @@ function detectFramework(root) {
|
|
|
554
614
|
for (const [framework, base] of [
|
|
555
615
|
["nuxt", "nuxt.config"],
|
|
556
616
|
["sveltekit", "svelte.config"],
|
|
557
|
-
["astro", "astro.config"]
|
|
617
|
+
["astro", "astro.config"],
|
|
618
|
+
["react-router", "react-router.config"]
|
|
558
619
|
]) if ([
|
|
559
620
|
"js",
|
|
560
621
|
"mjs",
|
|
@@ -576,6 +637,125 @@ function detectFramework(root) {
|
|
|
576
637
|
};
|
|
577
638
|
}
|
|
578
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
|
|
579
759
|
//#region src/utils/package-json.ts
|
|
580
760
|
/** Version of this CLI, which the other Dialex packages share. Read from the nearest package.json. */
|
|
581
761
|
function cliVersion() {
|
|
@@ -627,184 +807,9 @@ function addRequiredPackages(pkg, version = cliVersion()) {
|
|
|
627
807
|
return added;
|
|
628
808
|
}
|
|
629
809
|
//#endregion
|
|
630
|
-
//#region src/commands/init.ts
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const detected = detectFramework(root);
|
|
634
|
-
const isInteractive = !options.yes && process.stdin.isTTY;
|
|
635
|
-
let framework = options.framework || detected.framework;
|
|
636
|
-
let defaultLocale = options.defaultLocale || "en";
|
|
637
|
-
let localesInput = options.locales || "en,tr";
|
|
638
|
-
if (isInteractive) {
|
|
639
|
-
p.intro(pc.bgCyan(pc.black(" dialex init ")));
|
|
640
|
-
const frameworkOptions = [
|
|
641
|
-
{
|
|
642
|
-
value: "hono",
|
|
643
|
-
label: "Hono",
|
|
644
|
-
hint: detected.framework === "hono" ? "detected" : void 0
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
value: "express",
|
|
648
|
-
label: "Express",
|
|
649
|
-
hint: detected.framework === "express" ? "detected" : void 0
|
|
650
|
-
},
|
|
651
|
-
{
|
|
652
|
-
value: "fastify",
|
|
653
|
-
label: "Fastify",
|
|
654
|
-
hint: detected.framework === "fastify" ? "detected" : void 0
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
value: "koa",
|
|
658
|
-
label: "Koa",
|
|
659
|
-
hint: detected.framework === "koa" ? "detected" : void 0
|
|
660
|
-
},
|
|
661
|
-
{
|
|
662
|
-
value: "nestjs",
|
|
663
|
-
label: "NestJS",
|
|
664
|
-
hint: detected.framework === "nestjs" ? "detected" : void 0
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
value: "next",
|
|
668
|
-
label: "Next.js",
|
|
669
|
-
hint: detected.framework === "next" ? "detected" : void 0
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
value: "react",
|
|
673
|
-
label: "React / Vite",
|
|
674
|
-
hint: detected.framework === "react" ? "detected" : void 0
|
|
675
|
-
},
|
|
676
|
-
{
|
|
677
|
-
value: "elysia",
|
|
678
|
-
label: "Elysia",
|
|
679
|
-
hint: detected.framework === "elysia" ? "detected" : void 0
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
value: "sveltekit",
|
|
683
|
-
label: "SvelteKit",
|
|
684
|
-
hint: detected.framework === "sveltekit" ? "detected" : void 0
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
value: "astro",
|
|
688
|
-
label: "Astro",
|
|
689
|
-
hint: detected.framework === "astro" ? "detected" : void 0
|
|
690
|
-
},
|
|
691
|
-
{
|
|
692
|
-
value: "vue",
|
|
693
|
-
label: "Vue / Vite",
|
|
694
|
-
hint: detected.framework === "vue" ? "detected" : void 0
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
value: "nuxt",
|
|
698
|
-
label: "Nuxt",
|
|
699
|
-
hint: detected.framework === "nuxt" ? "detected" : void 0
|
|
700
|
-
}
|
|
701
|
-
];
|
|
702
|
-
const selectedFramework = await p.select({
|
|
703
|
-
message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
|
|
704
|
-
options: frameworkOptions,
|
|
705
|
-
initialValue: detected.framework
|
|
706
|
-
});
|
|
707
|
-
if (p.isCancel(selectedFramework)) {
|
|
708
|
-
p.cancel("Operation cancelled.");
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
framework = selectedFramework;
|
|
712
|
-
const inputDefault = await p.text({
|
|
713
|
-
message: "What is your default locale?",
|
|
714
|
-
initialValue: "en",
|
|
715
|
-
validate: (v) => !v || !v.trim() ? "Default locale cannot be empty" : void 0
|
|
716
|
-
});
|
|
717
|
-
if (p.isCancel(inputDefault)) {
|
|
718
|
-
p.cancel("Operation cancelled.");
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
defaultLocale = inputDefault.trim();
|
|
722
|
-
const inputLocales = await p.text({
|
|
723
|
-
message: "Comma-separated list of supported locales:",
|
|
724
|
-
initialValue: `${defaultLocale},tr`,
|
|
725
|
-
validate: (v) => !v || !v.trim() ? "Locales list cannot be empty" : void 0
|
|
726
|
-
});
|
|
727
|
-
if (p.isCancel(inputLocales)) {
|
|
728
|
-
p.cancel("Operation cancelled.");
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
localesInput = inputLocales.trim();
|
|
732
|
-
if (options.ai === void 0 && options.noAi === void 0) {
|
|
733
|
-
const confirmAi = await p.confirm({
|
|
734
|
-
message: "Configure AI agent support (Claude/Cursor skills, .mcp.json, AGENTS.md)?",
|
|
735
|
-
initialValue: true
|
|
736
|
-
});
|
|
737
|
-
if (!p.isCancel(confirmAi)) options.ai = confirmAi;
|
|
738
|
-
}
|
|
739
|
-
} else logger.info(`Auto-detected framework: ${pc.bold(framework)} (${detected.matchedRule})`);
|
|
740
|
-
const localeList = Array.from(/* @__PURE__ */ new Set([defaultLocale, ...localesInput.split(",").map((s) => s.trim()).filter(Boolean)]));
|
|
741
|
-
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");
|
|
742
|
-
const configFileName = path.basename(configPath);
|
|
743
|
-
try {
|
|
744
|
-
if (fs.existsSync(configPath)) {
|
|
745
|
-
const mod = await loadFile(configPath);
|
|
746
|
-
if (mod && mod.exports.default) {
|
|
747
|
-
if (mod.exports.default.$args && mod.exports.default.$args[0]) {
|
|
748
|
-
mod.exports.default.$args[0].defaultLocale = defaultLocale;
|
|
749
|
-
mod.exports.default.$args[0].locales = localeList;
|
|
750
|
-
}
|
|
751
|
-
await writeFile(mod, configPath);
|
|
752
|
-
logger.success(`Updated ${pc.bold(configFileName)} via Magicast`);
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
const mod = parseModule(`import { defineConfig } from "dialexjs";
|
|
756
|
-
|
|
757
|
-
export default defineConfig({});
|
|
758
|
-
`);
|
|
759
|
-
mod.exports.default.$args[0].defaultLocale = defaultLocale;
|
|
760
|
-
mod.exports.default.$args[0].locales = localeList;
|
|
761
|
-
await writeFile(mod, configPath);
|
|
762
|
-
logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
|
|
763
|
-
}
|
|
764
|
-
} catch (err) {
|
|
765
|
-
logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
|
|
766
|
-
const fallback = `import { defineConfig } from "dialexjs";
|
|
767
|
-
|
|
768
|
-
export default defineConfig({
|
|
769
|
-
defaultLocale: "${defaultLocale}",
|
|
770
|
-
locales: [${localeList.map((l) => `"${l}"`).join(", ")}],
|
|
771
|
-
});
|
|
772
|
-
`;
|
|
773
|
-
fs.writeFileSync(configPath, fallback, "utf-8");
|
|
774
|
-
}
|
|
775
|
-
if (framework === "nuxt") {
|
|
776
|
-
const nuxtConfig = [
|
|
777
|
-
"nuxt.config.ts",
|
|
778
|
-
"nuxt.config.js",
|
|
779
|
-
"nuxt.config.mjs"
|
|
780
|
-
].map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
|
|
781
|
-
if (nuxtConfig) try {
|
|
782
|
-
const nMod = await loadFile(nuxtConfig);
|
|
783
|
-
addNuxtModule(nMod, "dialexjs/nuxt", "dialex", {});
|
|
784
|
-
await writeFile(nMod, nuxtConfig);
|
|
785
|
-
logger.success(`Registered dialexjs/nuxt in ${pc.bold(path.basename(nuxtConfig))} via Magicast`);
|
|
786
|
-
} catch {
|
|
787
|
-
logger.warn("Could not update the Nuxt config; add the dialexjs/nuxt module manually.");
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
const targetDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
791
|
-
const dictPath = path.join(targetDir, "home.content.ts");
|
|
792
|
-
if (!fs.existsSync(dictPath)) {
|
|
793
|
-
const dictContent = `import { defineDictionary } from "dialexjs";
|
|
794
|
-
|
|
795
|
-
export default defineDictionary("home", {
|
|
796
|
-
${localeList.map((loc) => ` ${loc}: {
|
|
797
|
-
title: "Welcome to Dialex (${loc})!",
|
|
798
|
-
description: "Type-safe internationalization.",
|
|
799
|
-
greeting: (name: string) => "Hello, " + name + "!",
|
|
800
|
-
},`).join("\n")}
|
|
801
|
-
});
|
|
802
|
-
`;
|
|
803
|
-
fs.writeFileSync(dictPath, dictContent, "utf-8");
|
|
804
|
-
logger.success(`Created starter dictionary at ${pc.bold(path.relative(root, dictPath))}`);
|
|
805
|
-
} else logger.info("home.content.ts already exists, skipping creation.");
|
|
806
|
-
generateDictionaries(root);
|
|
807
|
-
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) {
|
|
808
813
|
const pkgPath = path.join(root, "package.json");
|
|
809
814
|
if (fs.existsSync(pkgPath)) try {
|
|
810
815
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -822,139 +827,155 @@ ${localeList.map((loc) => ` ${loc}: {
|
|
|
822
827
|
logger.warn("Could not update package.json; add dialexjs and @dialexjs/cli manually.");
|
|
823
828
|
}
|
|
824
829
|
else logger.warn("No package.json found; install dialexjs and @dialexjs/cli in your project.");
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
-
|
|
845
|
-
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (!fs.existsSync(agentsMdPath)) fs.writeFileSync(agentsMdPath, dialexAgentSnippet.trimStart(), "utf-8");
|
|
873
|
-
else {
|
|
874
|
-
const existing = fs.readFileSync(agentsMdPath, "utf-8");
|
|
875
|
-
if (!existing.includes("Dialex")) fs.writeFileSync(agentsMdPath, existing.trimEnd() + "\n" + dialexAgentSnippet, "utf-8");
|
|
876
|
-
}
|
|
877
|
-
logger.success("Configured AI assistant support (.agents/skills, .mcp.json, AGENTS.md)");
|
|
878
|
-
} catch (err) {
|
|
879
|
-
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;
|
|
880
877
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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: `
|
|
886
917
|
import { Hono } from "hono";
|
|
887
|
-
import {
|
|
888
|
-
import
|
|
918
|
+
import { dialexHono } from "dialexjs/hono";
|
|
919
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
889
920
|
|
|
890
921
|
const app = new Hono();
|
|
891
|
-
app.use("*",
|
|
922
|
+
app.use("*", dialexHono({ ...dialex }));
|
|
892
923
|
|
|
893
924
|
app.get("/", (c) => {
|
|
894
925
|
const dict = c.var.getDictionary("home");
|
|
895
926
|
return c.json({ title: dict.title });
|
|
896
927
|
});
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
case "fastify":
|
|
900
|
-
logger.log(`
|
|
928
|
+
`,
|
|
929
|
+
fastify: `
|
|
901
930
|
import Fastify from "fastify";
|
|
902
931
|
import { dialexPlugin } from "dialexjs/fastify";
|
|
903
|
-
import
|
|
932
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
904
933
|
|
|
905
934
|
const app = Fastify();
|
|
906
|
-
await app.register(dialexPlugin, {
|
|
935
|
+
await app.register(dialexPlugin, { ...dialex });
|
|
907
936
|
|
|
908
937
|
app.get("/", (req) => {
|
|
909
938
|
const dict = req.getDictionary("home");
|
|
910
939
|
return { title: dict.title };
|
|
911
940
|
});
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
case "express":
|
|
915
|
-
logger.log(`
|
|
941
|
+
`,
|
|
942
|
+
express: `
|
|
916
943
|
import express from "express";
|
|
917
|
-
import {
|
|
918
|
-
import
|
|
944
|
+
import { dialexExpress } from "dialexjs/express";
|
|
945
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
919
946
|
|
|
920
947
|
const app = express();
|
|
921
|
-
app.use(
|
|
948
|
+
app.use(dialexExpress({ ...dialex }));
|
|
922
949
|
|
|
923
950
|
app.get("/", (req, res) => {
|
|
924
951
|
const dict = req.getDictionary("home");
|
|
925
952
|
res.json({ title: dict.title });
|
|
926
953
|
});
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
case "koa":
|
|
930
|
-
logger.log(`
|
|
954
|
+
`,
|
|
955
|
+
koa: `
|
|
931
956
|
import Koa from "koa";
|
|
932
|
-
import {
|
|
933
|
-
import
|
|
957
|
+
import { dialexKoa } from "dialexjs/koa";
|
|
958
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
934
959
|
|
|
935
960
|
const app = new Koa();
|
|
936
|
-
app.use(
|
|
961
|
+
app.use(dialexKoa({ ...dialex }));
|
|
937
962
|
|
|
938
963
|
app.use((ctx) => {
|
|
939
964
|
const dict = ctx.getDictionary("home");
|
|
940
965
|
ctx.body = { title: dict.title };
|
|
941
966
|
});
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
case "nestjs":
|
|
945
|
-
logger.log(`
|
|
967
|
+
`,
|
|
968
|
+
nestjs: `
|
|
946
969
|
import { Module } from "@nestjs/common";
|
|
947
970
|
import { DialexModule } from "dialexjs/nestjs";
|
|
948
|
-
import
|
|
971
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
949
972
|
|
|
950
973
|
@Module({
|
|
951
|
-
imports: [DialexModule.forRoot({
|
|
974
|
+
imports: [DialexModule.forRoot({ ...dialex })],
|
|
952
975
|
})
|
|
953
976
|
export class AppModule {}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
case "next":
|
|
957
|
-
logger.log(`
|
|
977
|
+
`,
|
|
978
|
+
next: `
|
|
958
979
|
// src/dialex.ts
|
|
959
980
|
import { createDialexServer } from "dialexjs/server";
|
|
960
981
|
import { dialex } from "./dialex.generated";
|
|
@@ -975,10 +996,8 @@ export default async function Page({ params }: { params: Promise<{ locale: strin
|
|
|
975
996
|
// import { DialexProvider } from "dialexjs/react";
|
|
976
997
|
// import { dialex } from "./dialex.generated";
|
|
977
998
|
// export const Providers = ({ children }) => <DialexProvider {...dialex}>{children}</DialexProvider>;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
case "react":
|
|
981
|
-
logger.log(`
|
|
999
|
+
`,
|
|
1000
|
+
react: `
|
|
982
1001
|
// src/main.tsx
|
|
983
1002
|
import { createRoot } from "react-dom/client";
|
|
984
1003
|
import { DialexProvider } from "dialexjs/react";
|
|
@@ -992,45 +1011,37 @@ createRoot(document.getElementById("root")!).render(
|
|
|
992
1011
|
);
|
|
993
1012
|
|
|
994
1013
|
// In components: const dict = useDictionary("home")
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
case "elysia":
|
|
998
|
-
logger.log(`
|
|
1014
|
+
`,
|
|
1015
|
+
elysia: `
|
|
999
1016
|
import { Elysia } from "elysia";
|
|
1000
|
-
import {
|
|
1001
|
-
import
|
|
1017
|
+
import { dialexElysia } from "dialexjs/elysia";
|
|
1018
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
1002
1019
|
|
|
1003
1020
|
new Elysia()
|
|
1004
|
-
.use(
|
|
1021
|
+
.use(dialexElysia({ ...dialex }))
|
|
1005
1022
|
.get("/", ({ getDictionary }) => ({ title: getDictionary("home").title }))
|
|
1006
1023
|
.listen(3000);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
case "sveltekit":
|
|
1010
|
-
logger.log(`
|
|
1024
|
+
`,
|
|
1025
|
+
sveltekit: `
|
|
1011
1026
|
// src/hooks.server.ts
|
|
1012
1027
|
import { dialexHandle } from "dialexjs/sveltekit";
|
|
1013
|
-
import
|
|
1028
|
+
import { dialex } from "./dialex.generated.js";
|
|
1014
1029
|
|
|
1015
|
-
export const handle = dialexHandle({
|
|
1030
|
+
export const handle = dialexHandle({ ...dialex });
|
|
1016
1031
|
|
|
1017
1032
|
// src/app.html: <html lang="%dialex.lang%">
|
|
1018
1033
|
// In load functions: locals.getDictionary("home")
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
case "astro":
|
|
1022
|
-
logger.log(`
|
|
1034
|
+
`,
|
|
1035
|
+
astro: `
|
|
1023
1036
|
// src/middleware.ts
|
|
1024
|
-
import {
|
|
1025
|
-
import
|
|
1037
|
+
import { dialexAstro } from "dialexjs/astro";
|
|
1038
|
+
import { dialex } from "./dialex.generated.js";
|
|
1026
1039
|
|
|
1027
|
-
export const onRequest =
|
|
1040
|
+
export const onRequest = dialexAstro({ ...dialex });
|
|
1028
1041
|
|
|
1029
1042
|
// In pages: Astro.locals.getDictionary("home")
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
case "vue":
|
|
1033
|
-
logger.log(`
|
|
1043
|
+
`,
|
|
1044
|
+
vue: `
|
|
1034
1045
|
// src/main.ts
|
|
1035
1046
|
import { createApp } from "vue";
|
|
1036
1047
|
import { createDialex } from "dialexjs/vue";
|
|
@@ -1040,23 +1051,243 @@ import App from "./App.vue";
|
|
|
1040
1051
|
createApp(App).use(createDialex({ ...dialex })).mount("#app");
|
|
1041
1052
|
|
|
1042
1053
|
// In components: const dict = useDictionary("home")
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1054
|
+
`,
|
|
1055
|
+
svelte: `
|
|
1056
|
+
<!-- src/App.svelte -->
|
|
1057
|
+
<script lang="ts">
|
|
1058
|
+
import { provideDialex, useDictionary } from "dialexjs/svelte";
|
|
1059
|
+
import { dialex } from "./dialex.generated";
|
|
1060
|
+
|
|
1061
|
+
const { locale, setLocale } = provideDialex({ ...dialex });
|
|
1062
|
+
const home = useDictionary("home");
|
|
1063
|
+
<\/script>
|
|
1064
|
+
|
|
1065
|
+
<h1>{$home.title}</h1>
|
|
1066
|
+
<button onclick={() => setLocale("tr")}>TR</button>
|
|
1067
|
+
`,
|
|
1068
|
+
solid: `
|
|
1069
|
+
// src/index.tsx
|
|
1070
|
+
import { render } from "solid-js/web";
|
|
1071
|
+
import { DialexProvider } from "dialexjs/solid";
|
|
1072
|
+
import { dialex } from "./dialex.generated";
|
|
1073
|
+
import App from "./App";
|
|
1074
|
+
|
|
1075
|
+
render(
|
|
1076
|
+
() => (
|
|
1077
|
+
<DialexProvider {...dialex}>
|
|
1078
|
+
<App />
|
|
1079
|
+
</DialexProvider>
|
|
1080
|
+
),
|
|
1081
|
+
document.getElementById("root")!,
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
// In components: const home = useDictionary("home"); then {home().title}
|
|
1085
|
+
`,
|
|
1086
|
+
"react-router": `
|
|
1087
|
+
// app/dialex.server.ts
|
|
1088
|
+
import { createDialexHandler } from "dialexjs/web";
|
|
1089
|
+
import { dialex } from "./dialex.generated";
|
|
1090
|
+
|
|
1091
|
+
export const resolveDialex = createDialexHandler({ ...dialex });
|
|
1092
|
+
|
|
1093
|
+
// app/root.tsx
|
|
1094
|
+
import { DialexProvider } from "dialexjs/react";
|
|
1095
|
+
import { useLoaderData } from "react-router";
|
|
1096
|
+
import { dialex } from "./dialex.generated";
|
|
1097
|
+
import { resolveDialex } from "./dialex.server";
|
|
1098
|
+
|
|
1099
|
+
export async function loader({ request }: Route.LoaderArgs) {
|
|
1100
|
+
const { locale } = await resolveDialex(request);
|
|
1101
|
+
return { locale };
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export default function App() {
|
|
1105
|
+
const { locale } = useLoaderData<typeof loader>();
|
|
1106
|
+
return (
|
|
1107
|
+
<DialexProvider {...dialex} initialLocale={locale}>
|
|
1108
|
+
<Outlet />
|
|
1109
|
+
</DialexProvider>
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
`,
|
|
1113
|
+
preact: `
|
|
1114
|
+
// src/index.tsx
|
|
1115
|
+
import { render } from "preact";
|
|
1116
|
+
import { DialexProvider } from "dialexjs/preact";
|
|
1117
|
+
import { dialex } from "./dialex.generated";
|
|
1118
|
+
import { App } from "./app";
|
|
1119
|
+
|
|
1120
|
+
render(
|
|
1121
|
+
<DialexProvider {...dialex}>
|
|
1122
|
+
<App />
|
|
1123
|
+
</DialexProvider>,
|
|
1124
|
+
document.getElementById("app")!,
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
// In components: const dict = useDictionary("home")
|
|
1128
|
+
`,
|
|
1129
|
+
qwik: `
|
|
1130
|
+
// src/i18n.ts
|
|
1131
|
+
import { createDialexQwik } from "dialexjs/qwik";
|
|
1132
|
+
import { dialex } from "./dialex.generated";
|
|
1133
|
+
|
|
1134
|
+
export const { useDialexProvider, useDialex, setLocale, useDictionary, useT } =
|
|
1135
|
+
createDialexQwik({ ...dialex });
|
|
1136
|
+
|
|
1137
|
+
// src/routes/layout.tsx
|
|
1138
|
+
export default component$(() => {
|
|
1139
|
+
useDialexProvider();
|
|
1140
|
+
return <Slot />;
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
// In components: const home = useDictionary("home"); then {home().title}
|
|
1144
|
+
`,
|
|
1145
|
+
lit: `
|
|
1146
|
+
// src/i18n.ts
|
|
1147
|
+
import { createDialex } from "dialexjs/lit";
|
|
1148
|
+
import { dialex } from "./dialex.generated";
|
|
1149
|
+
|
|
1150
|
+
export const i18n = createDialex({ ...dialex });
|
|
1151
|
+
|
|
1152
|
+
// src/my-element.ts
|
|
1153
|
+
import { LitElement, html } from "lit";
|
|
1154
|
+
import { DialexController } from "dialexjs/lit";
|
|
1155
|
+
import { i18n } from "./i18n";
|
|
1156
|
+
|
|
1157
|
+
class MyElement extends LitElement {
|
|
1158
|
+
private dx = new DialexController(this, i18n);
|
|
1159
|
+
|
|
1160
|
+
render() {
|
|
1161
|
+
return html\`<h1>\${this.dx.dictionary("home").title}</h1>\`;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
`,
|
|
1165
|
+
"react-native": `
|
|
1166
|
+
// App.tsx
|
|
1167
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
1168
|
+
import { getLocales } from "expo-localization";
|
|
1169
|
+
import { DialexProvider } from "dialexjs/react-native";
|
|
1170
|
+
import { dialex } from "./src/dialex.generated";
|
|
1171
|
+
|
|
1172
|
+
export default function App() {
|
|
1173
|
+
return (
|
|
1174
|
+
<DialexProvider
|
|
1175
|
+
{...dialex}
|
|
1176
|
+
deviceLocales={getLocales().map((locale) => locale.languageTag)}
|
|
1177
|
+
storage={AsyncStorage}
|
|
1178
|
+
>
|
|
1179
|
+
<Home />
|
|
1180
|
+
</DialexProvider>
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// In components: const dict = useDictionary("home")
|
|
1185
|
+
`,
|
|
1186
|
+
hapi: `
|
|
1187
|
+
import Hapi from "@hapi/hapi";
|
|
1188
|
+
import { dialexPlugin } from "dialexjs/hapi";
|
|
1189
|
+
import { dialex } from "./src/dialex.generated.js";
|
|
1190
|
+
|
|
1191
|
+
const server = Hapi.server({ port: 3000 });
|
|
1192
|
+
await server.register({ plugin: dialexPlugin, options: { ...dialex } });
|
|
1193
|
+
|
|
1194
|
+
server.route({
|
|
1195
|
+
method: "GET",
|
|
1196
|
+
path: "/",
|
|
1197
|
+
handler: (request) => ({ title: request.getDictionary("home").title }),
|
|
1198
|
+
});
|
|
1199
|
+
`,
|
|
1200
|
+
adonis: `
|
|
1201
|
+
// app/middleware/dialex_middleware.ts
|
|
1202
|
+
import { createDialexMiddleware } from "dialexjs/adonis";
|
|
1203
|
+
import { dialex } from "#start/dialex.generated";
|
|
1204
|
+
|
|
1205
|
+
export default createDialexMiddleware({ ...dialex });
|
|
1206
|
+
|
|
1207
|
+
// start/kernel.ts
|
|
1208
|
+
router.use([() => import("#middleware/dialex_middleware")]);
|
|
1209
|
+
|
|
1210
|
+
// In handlers: ctx.locale, ctx.getDictionary("home")
|
|
1211
|
+
`,
|
|
1212
|
+
trpc: `
|
|
1213
|
+
// src/trpc.ts
|
|
1214
|
+
import { initTRPC } from "@trpc/server";
|
|
1215
|
+
import { createDialexContext } from "dialexjs/trpc";
|
|
1216
|
+
import { dialex } from "./dialex.generated.js";
|
|
1217
|
+
|
|
1218
|
+
const resolveDialex = createDialexContext({ ...dialex });
|
|
1219
|
+
|
|
1220
|
+
export const createContext = ({ req }: { req: Request }) => ({ ...resolveDialex(req) });
|
|
1221
|
+
|
|
1222
|
+
const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create();
|
|
1223
|
+
|
|
1224
|
+
export const appRouter = t.router({
|
|
1225
|
+
greeting: t.procedure.query(({ ctx }) => ctx.getDictionary("home").title),
|
|
1226
|
+
});
|
|
1227
|
+
`,
|
|
1228
|
+
nuxt: `
|
|
1047
1229
|
// nuxt.config.ts
|
|
1048
1230
|
export default defineNuxtConfig({
|
|
1049
1231
|
modules: ["dialexjs/nuxt"],
|
|
1050
1232
|
});
|
|
1051
1233
|
|
|
1052
1234
|
// In components: const dict = useDictionary("home")
|
|
1053
|
-
`
|
|
1054
|
-
|
|
1055
|
-
|
|
1235
|
+
`
|
|
1236
|
+
};
|
|
1237
|
+
/** Printed for a framework without a snippet of its own. */
|
|
1238
|
+
const FALLBACK = `
|
|
1056
1239
|
Import dictionaries and register your framework middleware to begin!
|
|
1057
1240
|
Run \`npm run dx:generate\` whenever you add new dictionary files.
|
|
1058
|
-
|
|
1059
|
-
|
|
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);
|
|
1060
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.");
|
|
1061
1292
|
if (isInteractive) p.outro(pc.green("✔ dialex is configured and ready to go!"));
|
|
1062
1293
|
}
|
|
@@ -1150,6 +1381,13 @@ function preservesPlaceholders(source, translated) {
|
|
|
1150
1381
|
}
|
|
1151
1382
|
//#endregion
|
|
1152
1383
|
//#region src/commands/translate.ts
|
|
1384
|
+
/**
|
|
1385
|
+
* The JSON of a result. Tasks carry the parsed dictionary and the key path for the writing step;
|
|
1386
|
+
* those are not part of the output (the parsed file is large and not meant to be read).
|
|
1387
|
+
*/
|
|
1388
|
+
function printable(result) {
|
|
1389
|
+
return JSON.stringify(result, (key, value) => key === "dict" || key === "path" ? void 0 : value, 2);
|
|
1390
|
+
}
|
|
1153
1391
|
async function runTranslate(options = {}) {
|
|
1154
1392
|
const root = options.cwd || process.cwd();
|
|
1155
1393
|
const project = await loadProject(root, options.config);
|
|
@@ -1193,12 +1431,12 @@ async function runTranslate(options = {}) {
|
|
|
1193
1431
|
rejected: []
|
|
1194
1432
|
};
|
|
1195
1433
|
if (tasks.length === 0) {
|
|
1196
|
-
if (quiet) console.log(
|
|
1434
|
+
if (quiet) console.log(printable(result));
|
|
1197
1435
|
else logger.success(`Nothing to translate from "${source}".`);
|
|
1198
1436
|
return result;
|
|
1199
1437
|
}
|
|
1200
1438
|
if (options.dryRun) {
|
|
1201
|
-
if (quiet) console.log(
|
|
1439
|
+
if (quiet) console.log(printable(result));
|
|
1202
1440
|
else {
|
|
1203
1441
|
logger.info(`Would translate ${tasks.length} string${tasks.length === 1 ? "" : "s"} from "${source}":`);
|
|
1204
1442
|
for (const t of tasks) logger.log(` ${pc.dim(t.locale)} ${t.key} ${pc.dim(JSON.stringify(t.source))}${t.stale ? pc.yellow(" (out of date)") : ""}`);
|
|
@@ -1243,7 +1481,7 @@ async function runTranslate(options = {}) {
|
|
|
1243
1481
|
for (const dict of touched) await saveDictionaryFile(dict.df);
|
|
1244
1482
|
if (result.translated > 0) writeLock(root, lock);
|
|
1245
1483
|
if (result.rejected.length > 0) result.success = false;
|
|
1246
|
-
if (quiet) console.log(
|
|
1484
|
+
if (quiet) console.log(printable(result));
|
|
1247
1485
|
else {
|
|
1248
1486
|
logger.success(`Translated ${result.translated} of ${tasks.length} string${tasks.length === 1 ? "" : "s"}.`);
|
|
1249
1487
|
if (result.rejected.length > 0) logger.warn(`${result.rejected.length} left unchanged: ${result.rejected.slice(0, 5).join("; ")}${result.rejected.length > 5 ? "; ..." : ""}`);
|
|
@@ -1257,7 +1495,7 @@ program.name("dialex").description("Dialex CLI - High-performance, type-safe int
|
|
|
1257
1495
|
program.command("generate").alias("gen").description("Generate standalone dictionary bundle and TypeScript declarations").option("-w, --watch", "Watch dictionary files for changes").option("-o, --output <path>", "Custom output file path for generated dictionaries").option("-c, --config <path>", "Custom config path").option("--json", "Print machine-readable JSON").action(async (opts) => {
|
|
1258
1496
|
await runGenerate(opts);
|
|
1259
1497
|
});
|
|
1260
|
-
program.command("init").description("Initialize Dialex in the current project").option("-f, --framework <framework>", "Target framework (hono, express, fastify, koa, nestjs, next, react)").option("-d, --default-locale <locale>", "Default locale (e.g. en)").option("-l, --locales <locales>", "Comma-separated supported locales (e.g. en,tr)").option("-y, --yes", "Skip prompts and use defaults").option("--ai", "Configure AI agent support (.agents/skills, .mcp.json, AGENTS.md)").option("--no-ai", "Skip AI agent configuration").action(async (opts) => {
|
|
1498
|
+
program.command("init").description("Initialize Dialex in the current project").option("-f, --framework <framework>", "Target framework (hono, express, fastify, koa, nestjs, elysia, sveltekit, astro, vue, nuxt, next, react, react-router, svelte, solid, preact, qwik, lit, react-native, hapi, adonis, trpc)").option("-d, --default-locale <locale>", "Default locale (e.g. en)").option("-l, --locales <locales>", "Comma-separated supported locales (e.g. en,tr)").option("-y, --yes", "Skip prompts and use defaults").option("--ai", "Configure AI agent support (.agents/skills, .mcp.json, AGENTS.md)").option("--no-ai", "Skip AI agent configuration").action(async (opts) => {
|
|
1261
1499
|
await runInit(opts);
|
|
1262
1500
|
});
|
|
1263
1501
|
program.command("check").alias("lint").description("Verify dictionary key parity and locale completeness").option("-c, --config <path>", "Custom config path").option("--json", "Print machine-readable JSON").option("--github", "Print GitHub Actions annotations").option("--fix", "Insert missing keys marked with [TODO]").option("--fail-on-stale", "Treat translations whose source text changed as errors").option("--src <globs...>", "Source globs to scan for dictionary references").action(async (opts) => {
|