@dialexjs/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1088 @@
1
+ #!/usr/bin/env node
2
+ import { C as saveDictionaryFile, _ as listLeaves, c as analyzeProject, d as TODO_PREFIX, f as copyLeaf, g as hasPath, h as getString, r as toKey, t as loadProject, v as listLocales, w as setString, y as loadDictionaryFile } from "./project-D4PmfqxM.mjs";
3
+ import { Command } from "commander";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import fg from "fast-glob";
7
+ import chokidar from "chokidar";
8
+ import { generateDts, resolveI18nConfig } from "dialexjs/scanner";
9
+ import pc from "picocolors";
10
+ import * as p from "@clack/prompts";
11
+ import { loadFile, parseModule, writeFile } from "magicast";
12
+ import { addNuxtModule, addVitePlugin } from "magicast/helpers";
13
+ import { argumentSignature, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
14
+ //#region src/utils/logger.ts
15
+ const logger = {
16
+ info: (msg) => console.log(pc.blue("ℹ ") + msg),
17
+ success: (msg) => console.log(pc.green("✔ ") + pc.bold(msg)),
18
+ warn: (msg) => console.warn(pc.yellow("⚠ ") + msg),
19
+ error: (msg) => console.error(pc.red("✖ ") + pc.bold(msg)),
20
+ log: (msg) => console.log(msg)
21
+ };
22
+ //#endregion
23
+ //#region src/commands/generate.ts
24
+ function generateDictionaries(root, options = {}) {
25
+ const inlineConfig = options.config ? { configFile: options.config } : {};
26
+ const config = resolveI18nConfig(root, inlineConfig);
27
+ const include = config.include || "**/*.content.ts";
28
+ const files = fg.sync(include, {
29
+ cwd: root,
30
+ absolute: true,
31
+ ignore: [
32
+ "**/node_modules/**",
33
+ "**/dist/**",
34
+ "**/.next/**"
35
+ ]
36
+ });
37
+ const srcDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
38
+ const targetOutput = options.output ? path.resolve(root, options.output) : path.join(srcDir, "i18n.generated.ts");
39
+ const targetDir = path.dirname(targetOutput);
40
+ generateDts(root, files, config.locales);
41
+ const dtsPath = path.join(srcDir, "dialex-env.d.ts");
42
+ const importLines = [];
43
+ const dictIdentifiers = [];
44
+ files.forEach((file, idx) => {
45
+ let rel = path.relative(targetDir, file).replace(/\\/g, "/");
46
+ rel = rel.replace(/\.ts$/, ".js");
47
+ if (!rel.startsWith(".")) rel = "./" + rel;
48
+ const id = `dict_${idx}`;
49
+ importLines.push(`import ${id} from "${rel}";`);
50
+ dictIdentifiers.push(id);
51
+ });
52
+ const content = `// Auto-generated by dialex CLI. Do not edit directly.
53
+ /* eslint-disable */
54
+ // @ts-nocheck
55
+ ${importLines.join("\n")}
56
+
57
+ export const dictionaries = [
58
+ ${dictIdentifiers.join(",\n ")}
59
+ ];
60
+
61
+ export const dictionaryMap = {
62
+ ${dictIdentifiers.map((id) => ` [${id}.name]: ${id}.dictionary,`).join("\n")}
63
+ };
64
+
65
+ export default dictionaries;
66
+ `;
67
+ fs.mkdirSync(targetDir, { recursive: true });
68
+ if ((fs.existsSync(targetOutput) ? fs.readFileSync(targetOutput, "utf-8") : "") !== content) fs.writeFileSync(targetOutput, content, "utf-8");
69
+ return {
70
+ files,
71
+ outputPath: targetOutput,
72
+ dtsPath
73
+ };
74
+ }
75
+ async function runGenerate(options = {}) {
76
+ const root = options.cwd || process.cwd();
77
+ const doGenerate = () => {
78
+ try {
79
+ const { files, outputPath, dtsPath } = generateDictionaries(root, options);
80
+ logger.success(`Generated dictionaries for ${files.length} file${files.length === 1 ? "" : "s"}:`);
81
+ logger.info(`→ ${path.relative(root, outputPath)}`);
82
+ logger.info(`→ ${path.relative(root, dtsPath)}`);
83
+ } catch (err) {
84
+ logger.error(`Generation failed: ${err.message || String(err)}`);
85
+ }
86
+ };
87
+ doGenerate();
88
+ if (options.watch) {
89
+ logger.info("Watching for dictionary changes (-w, --watch)...");
90
+ const inlineConfig = options.config ? { configFile: options.config } : {};
91
+ const pattern = resolveI18nConfig(root, inlineConfig).include || "**/*.content.ts";
92
+ chokidar.watch(pattern, {
93
+ cwd: root,
94
+ ignoreInitial: true,
95
+ ignored: [
96
+ "**/node_modules/**",
97
+ "**/dist/**",
98
+ "**/.next/**"
99
+ ]
100
+ }).on("all", (event, filePath) => {
101
+ logger.info(`[${event}] ${filePath} changed, regenerating...`);
102
+ doGenerate();
103
+ });
104
+ }
105
+ }
106
+ //#endregion
107
+ //#region src/utils/detector.ts
108
+ /**
109
+ * Automatically inspects package.json dependencies and configuration files
110
+ * to detect the active framework in the target project.
111
+ */
112
+ function detectFramework(root) {
113
+ const pkgPath = path.join(root, "package.json");
114
+ if (fs.existsSync(pkgPath)) try {
115
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
116
+ const allDeps = {
117
+ ...pkg.dependencies,
118
+ ...pkg.devDependencies
119
+ };
120
+ if ("nuxt" in allDeps) return {
121
+ framework: "nuxt",
122
+ confidence: "high",
123
+ matchedRule: "package.json dependencies (nuxt)"
124
+ };
125
+ if ("@sveltejs/kit" in allDeps) return {
126
+ framework: "sveltekit",
127
+ confidence: "high",
128
+ matchedRule: "package.json dependencies (@sveltejs/kit)"
129
+ };
130
+ if ("astro" in allDeps) return {
131
+ framework: "astro",
132
+ confidence: "high",
133
+ matchedRule: "package.json dependencies (astro)"
134
+ };
135
+ if ("next" in allDeps) return {
136
+ framework: "next",
137
+ confidence: "high",
138
+ matchedRule: "package.json dependencies (next)"
139
+ };
140
+ if ("@nestjs/core" in allDeps || "@nestjs/common" in allDeps) return {
141
+ framework: "nestjs",
142
+ confidence: "high",
143
+ matchedRule: "package.json dependencies (@nestjs/core)"
144
+ };
145
+ if ("fastify" in allDeps) return {
146
+ framework: "fastify",
147
+ confidence: "high",
148
+ matchedRule: "package.json dependencies (fastify)"
149
+ };
150
+ if ("koa" in allDeps) return {
151
+ framework: "koa",
152
+ confidence: "high",
153
+ matchedRule: "package.json dependencies (koa)"
154
+ };
155
+ if ("express" in allDeps) return {
156
+ framework: "express",
157
+ confidence: "high",
158
+ matchedRule: "package.json dependencies (express)"
159
+ };
160
+ if ("elysia" in allDeps) return {
161
+ framework: "elysia",
162
+ confidence: "high",
163
+ matchedRule: "package.json dependencies (elysia)"
164
+ };
165
+ if ("hono" in allDeps) return {
166
+ framework: "hono",
167
+ confidence: "high",
168
+ matchedRule: "package.json dependencies (hono)"
169
+ };
170
+ if ("vue" in allDeps && !("react" in allDeps)) return {
171
+ framework: "vue",
172
+ confidence: "high",
173
+ matchedRule: "package.json dependencies (vue)"
174
+ };
175
+ if ("react" in allDeps || "vite" in allDeps) return {
176
+ framework: "react",
177
+ confidence: "high",
178
+ matchedRule: "package.json dependencies (react/vite)"
179
+ };
180
+ } catch {}
181
+ if (fs.existsSync(path.join(root, "next.config.js")) || fs.existsSync(path.join(root, "next.config.mjs")) || fs.existsSync(path.join(root, "next.config.ts"))) return {
182
+ framework: "next",
183
+ confidence: "medium",
184
+ matchedRule: "next.config file presence"
185
+ };
186
+ for (const [framework, base] of [
187
+ ["nuxt", "nuxt.config"],
188
+ ["sveltekit", "svelte.config"],
189
+ ["astro", "astro.config"]
190
+ ]) if ([
191
+ "js",
192
+ "mjs",
193
+ "ts"
194
+ ].some((ext) => fs.existsSync(path.join(root, `${base}.${ext}`)))) return {
195
+ framework,
196
+ confidence: "medium",
197
+ matchedRule: `${base} file presence`
198
+ };
199
+ if (fs.existsSync(path.join(root, "vite.config.js")) || fs.existsSync(path.join(root, "vite.config.mjs")) || fs.existsSync(path.join(root, "vite.config.ts"))) return {
200
+ framework: "react",
201
+ confidence: "medium",
202
+ matchedRule: "vite.config file presence"
203
+ };
204
+ return {
205
+ framework: "hono",
206
+ confidence: "fallback",
207
+ matchedRule: "default fallback"
208
+ };
209
+ }
210
+ //#endregion
211
+ //#region src/commands/init.ts
212
+ async function runInit(options = {}) {
213
+ const root = options.cwd || process.cwd();
214
+ const detected = detectFramework(root);
215
+ const isInteractive = !options.yes && process.stdin.isTTY;
216
+ let framework = options.framework || detected.framework;
217
+ let defaultLocale = options.defaultLocale || "en";
218
+ let localesInput = options.locales || "en,tr";
219
+ if (isInteractive) {
220
+ p.intro(pc.bgCyan(pc.black(" dialex init ")));
221
+ const frameworkOptions = [
222
+ {
223
+ value: "hono",
224
+ label: "Hono",
225
+ hint: detected.framework === "hono" ? "detected" : void 0
226
+ },
227
+ {
228
+ value: "express",
229
+ label: "Express",
230
+ hint: detected.framework === "express" ? "detected" : void 0
231
+ },
232
+ {
233
+ value: "fastify",
234
+ label: "Fastify",
235
+ hint: detected.framework === "fastify" ? "detected" : void 0
236
+ },
237
+ {
238
+ value: "koa",
239
+ label: "Koa",
240
+ hint: detected.framework === "koa" ? "detected" : void 0
241
+ },
242
+ {
243
+ value: "nestjs",
244
+ label: "NestJS",
245
+ hint: detected.framework === "nestjs" ? "detected" : void 0
246
+ },
247
+ {
248
+ value: "next",
249
+ label: "Next.js",
250
+ hint: detected.framework === "next" ? "detected" : void 0
251
+ },
252
+ {
253
+ value: "react",
254
+ label: "React / Vite",
255
+ hint: detected.framework === "react" ? "detected" : void 0
256
+ },
257
+ {
258
+ value: "elysia",
259
+ label: "Elysia",
260
+ hint: detected.framework === "elysia" ? "detected" : void 0
261
+ },
262
+ {
263
+ value: "sveltekit",
264
+ label: "SvelteKit",
265
+ hint: detected.framework === "sveltekit" ? "detected" : void 0
266
+ },
267
+ {
268
+ value: "astro",
269
+ label: "Astro",
270
+ hint: detected.framework === "astro" ? "detected" : void 0
271
+ },
272
+ {
273
+ value: "vue",
274
+ label: "Vue / Vite",
275
+ hint: detected.framework === "vue" ? "detected" : void 0
276
+ },
277
+ {
278
+ value: "nuxt",
279
+ label: "Nuxt",
280
+ hint: detected.framework === "nuxt" ? "detected" : void 0
281
+ }
282
+ ];
283
+ const selectedFramework = await p.select({
284
+ message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
285
+ options: frameworkOptions,
286
+ initialValue: detected.framework
287
+ });
288
+ if (p.isCancel(selectedFramework)) {
289
+ p.cancel("Operation cancelled.");
290
+ return;
291
+ }
292
+ framework = selectedFramework;
293
+ const inputDefault = await p.text({
294
+ message: "What is your default locale?",
295
+ initialValue: "en",
296
+ validate: (v) => !v || !v.trim() ? "Default locale cannot be empty" : void 0
297
+ });
298
+ if (p.isCancel(inputDefault)) {
299
+ p.cancel("Operation cancelled.");
300
+ return;
301
+ }
302
+ defaultLocale = inputDefault.trim();
303
+ const inputLocales = await p.text({
304
+ message: "Comma-separated list of supported locales:",
305
+ initialValue: `${defaultLocale},tr`,
306
+ validate: (v) => !v || !v.trim() ? "Locales list cannot be empty" : void 0
307
+ });
308
+ if (p.isCancel(inputLocales)) {
309
+ p.cancel("Operation cancelled.");
310
+ return;
311
+ }
312
+ localesInput = inputLocales.trim();
313
+ } else logger.info(`Auto-detected framework: ${pc.bold(framework)} (${detected.matchedRule})`);
314
+ const localeList = Array.from(/* @__PURE__ */ new Set([defaultLocale, ...localesInput.split(",").map((s) => s.trim()).filter(Boolean)]));
315
+ 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");
316
+ const configFileName = path.basename(configPath);
317
+ try {
318
+ if (fs.existsSync(configPath)) {
319
+ const mod = await loadFile(configPath);
320
+ if (mod && mod.exports.default) {
321
+ if (mod.exports.default.$args && mod.exports.default.$args[0]) {
322
+ mod.exports.default.$args[0].defaultLocale = defaultLocale;
323
+ mod.exports.default.$args[0].locales = localeList;
324
+ }
325
+ await writeFile(mod, configPath);
326
+ logger.success(`Updated ${pc.bold(configFileName)} via Magicast`);
327
+ }
328
+ } else {
329
+ const mod = parseModule(`import { defineConfig } from "dialexjs";
330
+
331
+ export default defineConfig({});
332
+ `);
333
+ mod.exports.default.$args[0].defaultLocale = defaultLocale;
334
+ mod.exports.default.$args[0].locales = localeList;
335
+ await writeFile(mod, configPath);
336
+ logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
337
+ }
338
+ } catch (err) {
339
+ logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
340
+ const fallback = `import { defineConfig } from "dialexjs";
341
+
342
+ export default defineConfig({
343
+ defaultLocale: "${defaultLocale}",
344
+ locales: [${localeList.map((l) => `"${l}"`).join(", ")}],
345
+ });
346
+ `;
347
+ fs.writeFileSync(configPath, fallback, "utf-8");
348
+ }
349
+ if (framework === "react" || framework === "vue") {
350
+ const possibleViteConfigs = [
351
+ path.join(root, "vite.config.ts"),
352
+ path.join(root, "vite.config.js"),
353
+ path.join(root, "vite.config.mjs")
354
+ ];
355
+ for (const vConfig of possibleViteConfigs) if (fs.existsSync(vConfig)) try {
356
+ const vMod = await loadFile(vConfig);
357
+ addVitePlugin(vMod, {
358
+ from: "dialexjs/vite",
359
+ imported: "i18nPlugin",
360
+ constructor: "i18nPlugin"
361
+ });
362
+ await writeFile(vMod, vConfig);
363
+ logger.success(`Injected i18nPlugin() into ${pc.bold(path.basename(vConfig))} via Magicast`);
364
+ break;
365
+ } catch {}
366
+ }
367
+ if (framework === "nuxt") {
368
+ const nuxtConfig = [
369
+ "nuxt.config.ts",
370
+ "nuxt.config.js",
371
+ "nuxt.config.mjs"
372
+ ].map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
373
+ if (nuxtConfig) try {
374
+ const nMod = await loadFile(nuxtConfig);
375
+ addNuxtModule(nMod, "dialexjs/nuxt", "dialex", {
376
+ defaultLocale,
377
+ locales: localeList
378
+ });
379
+ await writeFile(nMod, nuxtConfig);
380
+ logger.success(`Registered dialexjs/nuxt in ${pc.bold(path.basename(nuxtConfig))} via Magicast`);
381
+ } catch {
382
+ logger.warn("Could not update the Nuxt config; add the dialexjs/nuxt module manually.");
383
+ }
384
+ }
385
+ const targetDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
386
+ const dictPath = path.join(targetDir, "home.content.ts");
387
+ if (!fs.existsSync(dictPath)) {
388
+ const dictContent = `import { defineDictionary } from "dialexjs";
389
+
390
+ export default defineDictionary("home", {
391
+ ${localeList.map((loc) => ` ${loc}: {
392
+ title: "Welcome to Dialex (${loc})!",
393
+ description: "Type-safe internationalization.",
394
+ greeting: (name: string) => "Hello, " + name + "!",
395
+ },`).join("\n")}
396
+ });
397
+ `;
398
+ fs.writeFileSync(dictPath, dictContent, "utf-8");
399
+ logger.success(`Created starter dictionary at ${pc.bold(path.relative(root, dictPath))}`);
400
+ } else logger.info("home.content.ts already exists, skipping creation.");
401
+ generateDictionaries(root);
402
+ logger.success("Generated types and standalone dictionary registry!");
403
+ const pkgPath = path.join(root, "package.json");
404
+ if (fs.existsSync(pkgPath)) try {
405
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
406
+ pkg.scripts = pkg.scripts || {};
407
+ if (!pkg.scripts["i18n:generate"]) {
408
+ pkg.scripts["i18n:generate"] = "dialex generate";
409
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
410
+ logger.success("Added \"i18n:generate\" script to package.json");
411
+ }
412
+ } catch {}
413
+ logger.log("");
414
+ logger.info(pc.bold(`Quick start for ${framework.toUpperCase()}:`));
415
+ switch (framework) {
416
+ case "hono":
417
+ logger.log(`
418
+ import { Hono } from "hono";
419
+ import { i18n } from "dialexjs/hono";
420
+ import dictionaries from "./src/i18n.generated.js";
421
+
422
+ const app = new Hono();
423
+ app.use("*", i18n({ dictionaries }));
424
+
425
+ app.get("/", (c) => {
426
+ const dict = c.var.getDictionary("home");
427
+ return c.json({ title: dict.title });
428
+ });
429
+ `);
430
+ break;
431
+ case "fastify":
432
+ logger.log(`
433
+ import Fastify from "fastify";
434
+ import { i18nPlugin } from "dialexjs/fastify";
435
+ import dictionaries from "./src/i18n.generated.js";
436
+
437
+ const app = Fastify();
438
+ await app.register(i18nPlugin, { dictionaries });
439
+
440
+ app.get("/", (req) => {
441
+ const dict = req.getDictionary("home");
442
+ return { title: dict.title };
443
+ });
444
+ `);
445
+ break;
446
+ case "express":
447
+ logger.log(`
448
+ import express from "express";
449
+ import { i18n } from "dialexjs/express";
450
+ import dictionaries from "./src/i18n.generated.js";
451
+
452
+ const app = express();
453
+ app.use(i18n({ dictionaries }));
454
+
455
+ app.get("/", (req, res) => {
456
+ const dict = req.getDictionary("home");
457
+ res.json({ title: dict.title });
458
+ });
459
+ `);
460
+ break;
461
+ case "koa":
462
+ logger.log(`
463
+ import Koa from "koa";
464
+ import { i18n } from "dialexjs/koa";
465
+ import dictionaries from "./src/i18n.generated.js";
466
+
467
+ const app = new Koa();
468
+ app.use(i18n({ dictionaries }));
469
+
470
+ app.use((ctx) => {
471
+ const dict = ctx.getDictionary("home");
472
+ ctx.body = { title: dict.title };
473
+ });
474
+ `);
475
+ break;
476
+ case "nestjs":
477
+ logger.log(`
478
+ import { Module } from "@nestjs/common";
479
+ import { I18nModule } from "dialexjs/nestjs";
480
+ import dictionaries from "./src/i18n.generated.js";
481
+
482
+ @Module({
483
+ imports: [I18nModule.forRoot({ dictionaries })],
484
+ })
485
+ export class AppModule {}
486
+ `);
487
+ break;
488
+ case "next":
489
+ logger.log(`
490
+ import { getDictionary } from "dialexjs/server";
491
+
492
+ export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
493
+ const { locale = "en" } = await params;
494
+ const dict = getDictionary("home", locale);
495
+ return <h1>{dict.title}</h1>;
496
+ }
497
+ `);
498
+ break;
499
+ case "elysia":
500
+ logger.log(`
501
+ import { Elysia } from "elysia";
502
+ import { i18n } from "dialexjs/elysia";
503
+ import dictionaries from "./src/i18n.generated.js";
504
+
505
+ new Elysia()
506
+ .use(i18n({ dictionaries }))
507
+ .get("/", ({ getDictionary }) => ({ title: getDictionary("home").title }))
508
+ .listen(3000);
509
+ `);
510
+ break;
511
+ case "sveltekit":
512
+ logger.log(`
513
+ // src/hooks.server.ts
514
+ import { i18nHandle } from "dialexjs/sveltekit";
515
+ import dictionaries from "./i18n.generated.js";
516
+
517
+ export const handle = i18nHandle({ dictionaries });
518
+
519
+ // src/app.html: <html lang="%dialex.lang%">
520
+ // In load functions: locals.getDictionary("home")
521
+ `);
522
+ break;
523
+ case "astro":
524
+ logger.log(`
525
+ // src/middleware.ts
526
+ import { i18n } from "dialexjs/astro";
527
+ import dictionaries from "./i18n.generated.js";
528
+
529
+ export const onRequest = i18n({ dictionaries });
530
+
531
+ // In pages: Astro.locals.getDictionary("home")
532
+ `);
533
+ break;
534
+ case "vue":
535
+ logger.log(`
536
+ // src/main.ts
537
+ import { createApp } from "vue";
538
+ import { createI18n } from "dialexjs/vue";
539
+ import App from "./App.vue";
540
+
541
+ createApp(App).use(createI18n()).mount("#app");
542
+
543
+ // In components: const dict = useDictionary("home")
544
+ `);
545
+ break;
546
+ case "nuxt":
547
+ logger.log(`
548
+ // nuxt.config.ts
549
+ export default defineNuxtConfig({
550
+ modules: ["dialexjs/nuxt"],
551
+ });
552
+
553
+ // In components: const dict = useDictionary("home")
554
+ `);
555
+ break;
556
+ default: logger.log(`
557
+ Import dictionaries and register your framework middleware to begin!
558
+ Run \`npm run i18n:generate\` whenever you add new dictionary files.
559
+ `);
560
+ }
561
+ if (isInteractive) p.outro(pc.green("✔ dialex is configured and ready to go!"));
562
+ }
563
+ //#endregion
564
+ //#region src/commands/check.ts
565
+ const DICTIONARY_IGNORE = [
566
+ "**/node_modules/**",
567
+ "**/dist/**",
568
+ "**/.next/**"
569
+ ];
570
+ function addIssue(diag, issue) {
571
+ const level = issue.level === "error" ? "error" : "warning";
572
+ (level === "error" ? diag.errors : diag.warnings).push(issue.message);
573
+ diag.annotations.push({
574
+ level,
575
+ message: issue.message,
576
+ line: issue.range ? issue.range.start.line + 1 : void 0
577
+ });
578
+ }
579
+ /**
580
+ * Inserts keys that exist in one locale but not in another, copying the default-locale value
581
+ * (strings get a `[TODO]` prefix). Returns how many keys were inserted.
582
+ */
583
+ async function applyFixes(files, configuredLocales, defaultLocale) {
584
+ let fixed = 0;
585
+ for (const file of files) {
586
+ const df = await loadDictionaryFile(file);
587
+ if (!df?.complete) continue;
588
+ const present = listLocales(df);
589
+ const targets = Array.from(/* @__PURE__ */ new Set([...present, ...configuredLocales]));
590
+ const sourceOrder = [defaultLocale, ...present.filter((l) => l !== defaultLocale)].filter((l) => present.includes(l));
591
+ const allPaths = /* @__PURE__ */ new Map();
592
+ for (const locale of present) for (const leaf of listLeaves(df, locale)) allPaths.set(leaf.path.join("\0"), leaf.path);
593
+ let changed = false;
594
+ for (const target of targets) for (const leafPath of allPaths.values()) {
595
+ if (hasPath(df, target, leafPath)) continue;
596
+ const source = sourceOrder.find((l) => l !== target && hasPath(df, l, leafPath));
597
+ if (source && copyLeaf(df, source, target, leafPath, (v) => `[TODO] ${v}`)) {
598
+ fixed++;
599
+ changed = true;
600
+ }
601
+ }
602
+ if (changed) await saveDictionaryFile(df);
603
+ }
604
+ return fixed;
605
+ }
606
+ function escapeAnnotation(value) {
607
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
608
+ }
609
+ async function runCheck(options = {}) {
610
+ const root = options.cwd || process.cwd();
611
+ const quiet = options.json === true;
612
+ const inlineConfig = options.config ? { configFile: options.config } : {};
613
+ const config = resolveI18nConfig(root, inlineConfig);
614
+ const include = config.include || "**/*.content.ts";
615
+ const configuredLocales = config.locales || [];
616
+ const defaultLocale = config.defaultLocale || "en";
617
+ const files = fg.sync(include, {
618
+ cwd: root,
619
+ absolute: true,
620
+ ignore: DICTIONARY_IGNORE
621
+ });
622
+ if (files.length === 0) {
623
+ if (!quiet) {
624
+ const patternStr = Array.isArray(include) ? include.join(", ") : include;
625
+ logger.warn(`No dictionary files found matching "${patternStr}".`);
626
+ }
627
+ const empty = {
628
+ success: true,
629
+ diagnostics: [],
630
+ totalIssues: 0,
631
+ totalWarnings: 0,
632
+ fixed: 0
633
+ };
634
+ if (quiet) console.log(JSON.stringify(empty, null, 2));
635
+ return empty;
636
+ }
637
+ let fixed = 0;
638
+ if (options.fix) {
639
+ fixed = await applyFixes(files, configuredLocales, defaultLocale);
640
+ if (fixed > 0 && !quiet) logger.success(`Inserted ${fixed} missing key${fixed === 1 ? "" : "s"} marked ${TODO_PREFIX.trim()}`);
641
+ }
642
+ const analysis = await analyzeProject({
643
+ root,
644
+ config,
645
+ src: options.src && options.src.length > 0 ? options.src : void 0,
646
+ runtime: true
647
+ });
648
+ const byFile = /* @__PURE__ */ new Map();
649
+ const dictionaryFiles = new Set(analysis.files);
650
+ for (const issue of analysis.issues) {
651
+ if (issue.level === "info") continue;
652
+ const rel = path.relative(root, issue.file);
653
+ let diag = byFile.get(rel);
654
+ if (!diag) {
655
+ diag = {
656
+ file: rel,
657
+ errors: [],
658
+ warnings: [],
659
+ annotations: []
660
+ };
661
+ byFile.set(rel, diag);
662
+ }
663
+ if (dictionaryFiles.has(issue.file) && issue.dictionary && !diag.name) diag.name = issue.dictionary;
664
+ addIssue(diag, issue);
665
+ }
666
+ const diagnostics = [...byFile.values()];
667
+ const totalIssues = diagnostics.reduce((n, d) => n + d.errors.length, 0);
668
+ const totalWarnings = diagnostics.reduce((n, d) => n + d.warnings.length, 0);
669
+ const reported = diagnostics.filter((d) => d.errors.length > 0 || d.warnings.length > 0);
670
+ const result = {
671
+ success: totalIssues === 0,
672
+ diagnostics: reported,
673
+ totalIssues,
674
+ totalWarnings,
675
+ fixed
676
+ };
677
+ if (options.github) for (const d of reported) for (const a of d.annotations) {
678
+ const location = `file=${d.file}${a.line ? `,line=${a.line}` : ""}`;
679
+ console.log(`::${a.level} ${location},title=dialex::${escapeAnnotation(a.message)}`);
680
+ }
681
+ if (quiet) console.log(JSON.stringify(result, null, 2));
682
+ else if (reported.length > 0) {
683
+ logger.log("");
684
+ if (totalIssues > 0) logger.error(`Found ${totalIssues} issue${totalIssues === 1 ? "" : "s"} across ${reported.filter((d) => d.errors.length > 0).length} file${reported.length === 1 ? "" : "s"}:`);
685
+ logger.log("");
686
+ for (const d of reported) {
687
+ logger.log(` ${pc.underline(pc.bold(d.file))} ${d.name ? pc.dim(`(${d.name})`) : ""}:`);
688
+ for (const a of d.annotations) {
689
+ const where = a.line ? pc.dim(`:${a.line}`) : "";
690
+ const mark = a.level === "error" ? pc.red("✖") : pc.yellow("⚠");
691
+ logger.log(` ${mark} ${a.message}${where}`);
692
+ }
693
+ logger.log("");
694
+ }
695
+ }
696
+ if (totalIssues > 0) {
697
+ if (options.exitOnError !== false) process.exit(1);
698
+ return result;
699
+ }
700
+ if (!quiet) {
701
+ const warningNote = totalWarnings > 0 ? ` (${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})` : "";
702
+ logger.success(`All ${files.length} dictionary file${files.length === 1 ? "" : "s"} passed checks with 0 issues${warningNote}!`);
703
+ }
704
+ return result;
705
+ }
706
+ //#endregion
707
+ //#region src/commands/export.ts
708
+ const escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
709
+ const csvCell = (value) => /[",\r\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
710
+ async function runExport(options = {}) {
711
+ const root = options.cwd || process.cwd();
712
+ const format = options.format || "json";
713
+ if (![
714
+ "json",
715
+ "csv",
716
+ "xliff"
717
+ ].includes(format)) throw new Error(`Unsupported format "${format}". Use json, csv, or xliff.`);
718
+ const outDir = path.resolve(root, options.out || "i18n-export");
719
+ const project = await loadProject(root, options.config);
720
+ const defaultLocale = project.config.defaultLocale || "en";
721
+ const table = /* @__PURE__ */ new Map();
722
+ const orderedKeys = /* @__PURE__ */ new Set();
723
+ let skippedFunctions = 0;
724
+ const localeSet = new Set(project.config.locales || []);
725
+ for (const { df } of project.dictionaries) listLocales(df).forEach((l) => localeSet.add(l));
726
+ let locales = Array.from(localeSet);
727
+ if (options.locale && options.locale.length > 0) locales = locales.filter((l) => options.locale.includes(l) || l === defaultLocale);
728
+ for (const locale of locales) table.set(locale, /* @__PURE__ */ new Map());
729
+ for (const { name, df } of project.dictionaries) for (const locale of listLocales(df)) {
730
+ if (!table.has(locale)) continue;
731
+ for (const leaf of listLeaves(df, locale)) if (leaf.kind === "string" && leaf.value !== void 0) {
732
+ const key = toKey(name, leaf.path);
733
+ table.get(locale).set(key, leaf.value);
734
+ orderedKeys.add(key);
735
+ } else if (locale === defaultLocale) skippedFunctions++;
736
+ }
737
+ fs.mkdirSync(outDir, { recursive: true });
738
+ const files = [];
739
+ const write = (name, content) => {
740
+ const file = path.join(outDir, name);
741
+ fs.writeFileSync(file, content, "utf-8");
742
+ files.push(path.relative(root, file));
743
+ };
744
+ if (format === "json") for (const [locale, values] of table) write(`${locale}.json`, JSON.stringify(Object.fromEntries(values), null, 2) + "\n");
745
+ else if (format === "csv") write("translations.csv", [["key", ...table.keys()].map(csvCell).join(","), ...[...orderedKeys].map((key) => [key, ...[...table.values()].map((v) => v.get(key) ?? "")].map(csvCell).join(","))].join("\n") + "\n");
746
+ else {
747
+ const source = table.get(defaultLocale);
748
+ for (const [locale, values] of table) {
749
+ if (locale === defaultLocale) continue;
750
+ const units = [...orderedKeys].map((key) => {
751
+ const src = source?.get(key) ?? "";
752
+ const target = values.get(key);
753
+ const targetXml = target === void 0 ? "" : `<target>${escapeXml(target)}</target>`;
754
+ return ` <trans-unit id="${escapeXml(key)}"><source>${escapeXml(src)}</source>${targetXml}</trans-unit>`;
755
+ });
756
+ write(`${locale}.xlf`, `<?xml version="1.0" encoding="UTF-8"?>
757
+ <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
758
+ <file original="dialex" source-language="${escapeXml(defaultLocale)}" target-language="${escapeXml(locale)}" datatype="plaintext">
759
+ <body>
760
+ ${units.join("\n")}
761
+ </body>
762
+ </file>
763
+ </xliff>
764
+ `);
765
+ }
766
+ }
767
+ logger.success(`Exported ${orderedKeys.size} key${orderedKeys.size === 1 ? "" : "s"} to ${files.length} ${format} file${files.length === 1 ? "" : "s"} in ${path.relative(root, outDir) || "."}`);
768
+ if (skippedFunctions > 0) logger.warn(`${skippedFunctions} function value${skippedFunctions === 1 ? "" : "s"} skipped (not translatable as text)`);
769
+ for (const file of project.unsupported) logger.warn(`Skipped ${file}: unsupported dictionary shape`);
770
+ return {
771
+ files,
772
+ keys: orderedKeys.size,
773
+ skippedFunctions,
774
+ skippedFiles: project.unsupported
775
+ };
776
+ }
777
+ //#endregion
778
+ //#region src/commands/import.ts
779
+ const unescapeXml = (value) => value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
780
+ function parseCsv(text) {
781
+ const rows = [];
782
+ let row = [];
783
+ let cell = "";
784
+ let quoted = false;
785
+ for (let i = 0; i < text.length; i++) {
786
+ const ch = text[i];
787
+ if (quoted) {
788
+ if (ch === "\"" && text[i + 1] === "\"") {
789
+ cell += "\"";
790
+ i++;
791
+ } else if (ch === "\"") quoted = false;
792
+ else cell += ch;
793
+ } else if (ch === "\"") quoted = true;
794
+ else if (ch === ",") {
795
+ row.push(cell);
796
+ cell = "";
797
+ } else if (ch === "\n" || ch === "\r") {
798
+ if (ch === "\r" && text[i + 1] === "\n") i++;
799
+ row.push(cell);
800
+ rows.push(row);
801
+ row = [];
802
+ cell = "";
803
+ } else cell += ch;
804
+ }
805
+ if (cell !== "" || row.length > 0) {
806
+ row.push(cell);
807
+ rows.push(row);
808
+ }
809
+ return rows.filter((r) => r.some((c) => c !== ""));
810
+ }
811
+ function parseEntries(content, format, fallbackLocale) {
812
+ const entries = [];
813
+ if (format === "json") {
814
+ const data = JSON.parse(content);
815
+ if (Object.keys(data).length > 0 && Object.values(data).every((v) => v && typeof v === "object" && !Array.isArray(v))) {
816
+ for (const [locale, values] of Object.entries(data)) for (const [key, value] of Object.entries(values)) if (typeof value === "string") entries.push({
817
+ locale,
818
+ key,
819
+ value
820
+ });
821
+ } else {
822
+ if (!fallbackLocale) throw new Error("Cannot infer the locale; pass --locale.");
823
+ for (const [key, value] of Object.entries(data)) if (typeof value === "string") entries.push({
824
+ locale: fallbackLocale,
825
+ key,
826
+ value
827
+ });
828
+ }
829
+ } else if (format === "csv") {
830
+ const [header, ...rows] = parseCsv(content);
831
+ if (!header || header[0] !== "key") throw new Error("CSV must start with a \"key\" column.");
832
+ for (const row of rows) header.slice(1).forEach((locale, i) => {
833
+ const value = row[i + 1];
834
+ if (value !== void 0 && value !== "") entries.push({
835
+ locale,
836
+ key: row[0],
837
+ value
838
+ });
839
+ });
840
+ } else {
841
+ const locale = /target-language="([^"]*)"/.exec(content)?.[1] ?? fallbackLocale;
842
+ if (!locale) throw new Error("Cannot infer the locale; pass --locale.");
843
+ for (const match of content.matchAll(/<trans-unit\b[^>]*\bid="([^"]*)"[^>]*>([\s\S]*?)<\/trans-unit>/g)) {
844
+ const target = /<target\b[^>]*>([\s\S]*?)<\/target>/.exec(match[2])?.[1];
845
+ if (target !== void 0 && target !== "") entries.push({
846
+ locale,
847
+ key: unescapeXml(match[1]),
848
+ value: unescapeXml(target)
849
+ });
850
+ }
851
+ }
852
+ return entries;
853
+ }
854
+ function inferFormat(file) {
855
+ const ext = path.extname(file).toLowerCase();
856
+ if (ext === ".json") return "json";
857
+ if (ext === ".csv") return "csv";
858
+ if (ext === ".xlf" || ext === ".xliff") return "xliff";
859
+ throw new Error(`Cannot infer format from "${ext}". Pass --format json|csv|xliff.`);
860
+ }
861
+ /**
862
+ * Why `value` must not replace the current text, if it is an ICU message that is invalid or uses
863
+ * different arguments than the same key in another locale.
864
+ */
865
+ function icuProblem(df, defaultLocale, locale, keyPath, value) {
866
+ if (!value.includes("{")) return void 0;
867
+ let signature;
868
+ try {
869
+ signature = argumentSignature(parseMessage(value));
870
+ } catch (error) {
871
+ return `invalid ICU: ${error.message}`;
872
+ }
873
+ const locales = listLocales(df);
874
+ for (const other of [defaultLocale, ...locales.filter((l) => l !== defaultLocale)]) {
875
+ if (other === locale) continue;
876
+ const reference = getString(df, other, keyPath);
877
+ if (!reference?.includes("{")) continue;
878
+ try {
879
+ const expected = argumentSignature(parseMessage(reference));
880
+ return expected === signature ? void 0 : `arguments differ from "${other}": expected ${expected || "none"}, found ${signature || "none"}`;
881
+ } catch {
882
+ continue;
883
+ }
884
+ }
885
+ }
886
+ async function runImport(options) {
887
+ const root = options.cwd || process.cwd();
888
+ const file = path.resolve(root, options.file);
889
+ const format = options.format || inferFormat(file);
890
+ const fallbackLocale = options.locale ?? path.basename(file, path.extname(file));
891
+ const entries = parseEntries(fs.readFileSync(file, "utf-8"), format, options.locale ?? fallbackLocale);
892
+ const project = await loadProject(root, options.config);
893
+ const byName = new Map(project.dictionaries.map((d) => [d.name, d]));
894
+ const result = {
895
+ created: 0,
896
+ updated: 0,
897
+ skipped: [],
898
+ rejected: [],
899
+ unknown: [],
900
+ errors: []
901
+ };
902
+ const touched = /* @__PURE__ */ new Set();
903
+ for (const { locale, key, value } of entries) {
904
+ const [name, ...keyPath] = key.split(".");
905
+ const dict = byName.get(name);
906
+ if (!dict || keyPath.length === 0) {
907
+ result.unknown.push(key);
908
+ continue;
909
+ }
910
+ if (!listLocales(dict.df).some((l) => hasPath(dict.df, l, keyPath)) && !options.allowNew) {
911
+ result.unknown.push(key);
912
+ continue;
913
+ }
914
+ const problem = icuProblem(dict.df, project.config.defaultLocale ?? "en", locale, keyPath, value);
915
+ if (problem) {
916
+ result.rejected.push(`${locale}:${key} (${problem})`);
917
+ continue;
918
+ }
919
+ const outcome = setString(dict.df, locale, keyPath, value);
920
+ if (outcome === "skipped") result.skipped.push(`${locale}:${key}`);
921
+ else {
922
+ result[outcome]++;
923
+ touched.add(dict.rel);
924
+ }
925
+ }
926
+ for (const dict of project.dictionaries) if (touched.has(dict.rel)) await saveDictionaryFile(dict.df);
927
+ logger.success(`Imported ${result.created + result.updated} value${result.created + result.updated === 1 ? "" : "s"} (${result.created} added, ${result.updated} updated)`);
928
+ if (result.unknown.length > 0) logger.warn(`${result.unknown.length} unknown key${result.unknown.length === 1 ? "" : "s"} ignored (use --allow-new to add): ${result.unknown.slice(0, 5).join(", ")}${result.unknown.length > 5 ? ", ..." : ""}`);
929
+ if (result.rejected.length > 0) logger.warn(`${result.rejected.length} value${result.rejected.length === 1 ? "" : "s"} rejected: ${result.rejected.slice(0, 5).join("; ")}`);
930
+ if (result.skipped.length > 0) logger.warn(`${result.skipped.length} non-text value${result.skipped.length === 1 ? "" : "s"} left unchanged: ${result.skipped.slice(0, 5).join(", ")}`);
931
+ return result;
932
+ }
933
+ //#endregion
934
+ //#region src/translate/placeholders.ts
935
+ /** `{name}`, `%s` / `%1$d`, and HTML/XML tags: text that must survive translation unchanged. */
936
+ const PLACEHOLDER = /\{[^{}]+\}|%\d*\$?[sdif]|<\/?[A-Za-z][^<>]*>/g;
937
+ function extractPlaceholders(text) {
938
+ return text.match(PLACEHOLDER) ?? [];
939
+ }
940
+ /**
941
+ * ICU plural/select messages put translatable text inside `{...}` branches, so brace matching
942
+ * is meaningless for them. Compare the structure instead: the same arguments with the same
943
+ * types, every `select` key and every exact `=N` plural match preserved. A translation may
944
+ * add or drop plural categories (`one`, `few`, ...), because languages need different ones.
945
+ */
946
+ function preservesIcuStructure(source, translated) {
947
+ try {
948
+ const before = getArguments(parseMessage(source));
949
+ const after = getArguments(parseMessage(translated));
950
+ if (!(before.length === after.length && before.every((a) => after.some((b) => a.name === b.name && a.type === b.type)))) return false;
951
+ return before.every((a) => {
952
+ const other = after.find((b) => b.name === a.name && b.type === a.type);
953
+ return (a.type === "select" ? a.selectors ?? [] : (a.selectors ?? []).filter((selector) => selector.startsWith("="))).every((selector) => other.selectors?.includes(selector));
954
+ });
955
+ } catch {
956
+ return false;
957
+ }
958
+ }
959
+ /** True when every placeholder of `source` appears in `translated` (as many times as in the source). */
960
+ function preservesPlaceholders(source, translated) {
961
+ if (isIcuStructured(source)) return preservesIcuStructure(source, translated);
962
+ const remaining = extractPlaceholders(translated);
963
+ for (const placeholder of extractPlaceholders(source)) {
964
+ const index = remaining.indexOf(placeholder);
965
+ if (index === -1) return false;
966
+ remaining.splice(index, 1);
967
+ }
968
+ return true;
969
+ }
970
+ //#endregion
971
+ //#region src/commands/translate.ts
972
+ async function runTranslate(options = {}) {
973
+ const root = options.cwd || process.cwd();
974
+ const project = await loadProject(root, options.config);
975
+ const settings = project.config.translate;
976
+ const provider = options.provider ?? settings?.provider;
977
+ if (!provider && !options.dryRun) throw new Error("No translate provider configured. Add `translate: { provider: claudeProvider() }` to dialex.config.ts (import it from \"@dialexjs/cli/translate\").");
978
+ const source = options.source ?? settings?.sourceLocale ?? project.config.defaultLocale ?? "en";
979
+ const everyLocale = new Set(project.config.locales ?? []);
980
+ for (const { df } of project.dictionaries) listLocales(df).forEach((l) => everyLocale.add(l));
981
+ const targets = (options.locale?.length ? options.locale : [...everyLocale]).filter((l) => l !== source);
982
+ const tasks = [];
983
+ for (const dict of project.dictionaries) {
984
+ const sourceLeaves = listLeaves(dict.df, source).filter((leaf) => leaf.kind === "string" && leaf.value !== void 0);
985
+ for (const locale of targets) {
986
+ const existing = new Map(listLeaves(dict.df, locale).map((leaf) => [leaf.path.join("\0"), leaf]));
987
+ for (const leaf of sourceLeaves) {
988
+ const current = existing.get(leaf.path.join("\0"));
989
+ if (!current || current.kind === "string" && current.value?.startsWith("[TODO] ")) tasks.push({
990
+ dict,
991
+ path: leaf.path,
992
+ file: dict.rel,
993
+ locale,
994
+ key: toKey(dict.name, leaf.path),
995
+ source: leaf.value
996
+ });
997
+ }
998
+ }
999
+ }
1000
+ const result = {
1001
+ dryRun: options.dryRun === true,
1002
+ pending: tasks,
1003
+ translated: 0,
1004
+ rejected: []
1005
+ };
1006
+ if (tasks.length === 0) {
1007
+ logger.success(`Nothing to translate from "${source}".`);
1008
+ return result;
1009
+ }
1010
+ if (options.dryRun) {
1011
+ logger.info(`Would translate ${tasks.length} string${tasks.length === 1 ? "" : "s"} from "${source}":`);
1012
+ for (const t of tasks) logger.log(` ${pc.dim(t.locale)} ${t.key} ${pc.dim(JSON.stringify(t.source))}`);
1013
+ return result;
1014
+ }
1015
+ const touched = /* @__PURE__ */ new Set();
1016
+ for (const locale of targets) {
1017
+ const localeTasks = tasks.filter((t) => t.locale === locale);
1018
+ if (localeTasks.length === 0) continue;
1019
+ const sendable = provider.supportsIcu ? localeTasks : localeTasks.filter((task) => {
1020
+ if (!isIcuStructured(task.source)) return true;
1021
+ result.rejected.push(`${locale}:${task.key} (provider does not support ICU plural/select messages)`);
1022
+ return false;
1023
+ });
1024
+ if (sendable.length === 0) continue;
1025
+ const unique = [...new Set(sendable.map((t) => t.source))];
1026
+ logger.info(`Translating ${unique.length} string${unique.length === 1 ? "" : "s"} to "${locale}" with ${provider.name ?? "provider"}...`);
1027
+ const translations = await provider.translate(unique, source, locale);
1028
+ if (translations.length !== unique.length) throw new Error(`Provider returned ${translations.length} translations for ${unique.length} strings.`);
1029
+ const byText = new Map(unique.map((text, i) => [text, translations[i]]));
1030
+ for (const task of sendable) {
1031
+ const translated = byText.get(task.source);
1032
+ if (typeof translated !== "string" || translated.trim() === "") {
1033
+ result.rejected.push(`${locale}:${task.key} (empty translation)`);
1034
+ continue;
1035
+ }
1036
+ if (!preservesPlaceholders(task.source, translated)) {
1037
+ result.rejected.push(`${locale}:${task.key} (placeholders changed)`);
1038
+ continue;
1039
+ }
1040
+ if (setString(task.dict.df, locale, task.path, translated) === "skipped") {
1041
+ result.rejected.push(`${locale}:${task.key} (not a text value)`);
1042
+ continue;
1043
+ }
1044
+ task.translated = translated;
1045
+ result.translated++;
1046
+ touched.add(task.dict);
1047
+ }
1048
+ }
1049
+ for (const dict of touched) await saveDictionaryFile(dict.df);
1050
+ logger.success(`Translated ${result.translated} of ${tasks.length} string${tasks.length === 1 ? "" : "s"}.`);
1051
+ if (result.rejected.length > 0) logger.warn(`${result.rejected.length} left unchanged: ${result.rejected.slice(0, 5).join("; ")}${result.rejected.length > 5 ? "; ..." : ""}`);
1052
+ return result;
1053
+ }
1054
+ //#endregion
1055
+ //#region src/index.ts
1056
+ const program = new Command();
1057
+ program.name("dialex").description("Dialex CLI - High-performance, type-safe internationalization compiler and tooling").version("0.1.0");
1058
+ 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").action(async (opts) => {
1059
+ await runGenerate(opts);
1060
+ });
1061
+ 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").action(async (opts) => {
1062
+ await runInit(opts);
1063
+ });
1064
+ 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("--src <globs...>", "Source globs to scan for dictionary references").action(async (opts) => {
1065
+ await runCheck(opts);
1066
+ });
1067
+ program.command("export").description("Export dictionary strings for translators (json, csv, or xliff)").option("-f, --format <format>", "Output format: json, csv, xliff", "json").option("-o, --out <dir>", "Output directory", "i18n-export").option("-l, --locale <locales...>", "Limit to these locales").option("-c, --config <path>", "Custom config path").action(async (opts) => {
1068
+ await runExport(opts);
1069
+ });
1070
+ program.command("import <file>").description("Write translated strings from a json, csv, or xliff file back into dictionaries").option("-f, --format <format>", "Input format (inferred from the extension)").option("-l, --locale <locale>", "Locale for single-locale files").option("--allow-new", "Add keys that do not exist in any locale yet").option("-c, --config <path>", "Custom config path").action(async (file, opts) => {
1071
+ await runImport({
1072
+ file,
1073
+ ...opts
1074
+ });
1075
+ });
1076
+ program.command("translate").description("Fill missing translations with the provider configured in dialex.config.ts").option("-l, --locale <locales...>", "Target locales (default: all configured)").option("-s, --source <locale>", "Source locale (default: translate.sourceLocale or defaultLocale)").option("--dry-run", "List what would be translated without calling the provider").option("-c, --config <path>", "Custom config path").action(async (opts) => {
1077
+ try {
1078
+ await runTranslate(opts);
1079
+ } catch (err) {
1080
+ logger.error(err.message || String(err));
1081
+ process.exit(1);
1082
+ }
1083
+ });
1084
+ program.parse();
1085
+ //#endregion
1086
+ export {};
1087
+
1088
+ //# sourceMappingURL=index.mjs.map