@dialexjs/cli 0.3.0 → 0.5.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/README.md +1 -1
- package/dist/api.d.mts +69 -3
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +2 -2
- package/dist/index.mjs +1073 -904
- package/dist/index.mjs.map +1 -1
- package/dist/{project-ClQxaIMc.mjs → project-BJYmnKiU.mjs} +594 -121
- package/dist/project-BJYmnKiU.mjs.map +1 -0
- package/dist/translate/index.d.mts.map +1 -1
- package/dist/translate/index.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/project-ClQxaIMc.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,972 +1,1125 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as
|
|
2
|
+
import { A as listLocales, C as TODO_PREFIX, D as getString, F as saveDictionaryFile, I as setString, O as hasPath, S as writeLock, _ as isStale, a as renderGenerated, b as readLock, f as analyzeProject, h as LOCK_FILE, i as generateDictionaries, j as loadDictionaryFile, k as listLeaves, o as runGenerate, r as toKey, t as loadProject, u as logger, v as lockFor, w as copyLeaf, x as record, y as lockPath } from "./project-BJYmnKiU.mjs";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import fg from "fast-glob";
|
|
7
6
|
import { resolveDialexConfig } from "dialexjs/scanner";
|
|
7
|
+
import fg from "fast-glob";
|
|
8
8
|
import pc from "picocolors";
|
|
9
|
-
import * as p from "@clack/prompts";
|
|
10
|
-
import { loadFile, parseModule, writeFile } from "magicast";
|
|
11
|
-
import { addNuxtModule, addVitePlugin } from "magicast/helpers";
|
|
12
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { loadFile, parseModule, writeFile } from "magicast";
|
|
13
11
|
import { argumentSignature, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
|
|
14
|
-
|
|
12
|
+
import * as p from "@clack/prompts";
|
|
13
|
+
import { addNuxtModule } from "magicast/helpers";
|
|
14
|
+
//#region package.json
|
|
15
|
+
var version = "0.5.0";
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/commands/check.ts
|
|
18
|
+
const DICTIONARY_IGNORE = [
|
|
19
|
+
"**/node_modules/**",
|
|
20
|
+
"**/dist/**",
|
|
21
|
+
"**/.next/**",
|
|
22
|
+
"**/dialex.locales/**"
|
|
23
|
+
];
|
|
24
|
+
function addIssue(diag, issue) {
|
|
25
|
+
const level = issue.level === "error" ? "error" : "warning";
|
|
26
|
+
(level === "error" ? diag.errors : diag.warnings).push(issue.message);
|
|
27
|
+
diag.annotations.push({
|
|
28
|
+
level,
|
|
29
|
+
message: issue.message,
|
|
30
|
+
line: issue.range ? issue.range.start.line + 1 : void 0
|
|
31
|
+
});
|
|
32
|
+
}
|
|
15
33
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
34
|
+
* Inserts keys that exist in one locale but not in another, copying the default-locale value
|
|
35
|
+
* (strings get a `[TODO]` prefix). Returns how many keys were inserted.
|
|
18
36
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
confidence: "high",
|
|
40
|
-
matchedRule: "package.json dependencies (astro)"
|
|
41
|
-
};
|
|
42
|
-
if ("next" in allDeps) return {
|
|
43
|
-
framework: "next",
|
|
44
|
-
confidence: "high",
|
|
45
|
-
matchedRule: "package.json dependencies (next)"
|
|
46
|
-
};
|
|
47
|
-
if ("@nestjs/core" in allDeps || "@nestjs/common" in allDeps) return {
|
|
48
|
-
framework: "nestjs",
|
|
49
|
-
confidence: "high",
|
|
50
|
-
matchedRule: "package.json dependencies (@nestjs/core)"
|
|
51
|
-
};
|
|
52
|
-
if ("fastify" in allDeps) return {
|
|
53
|
-
framework: "fastify",
|
|
54
|
-
confidence: "high",
|
|
55
|
-
matchedRule: "package.json dependencies (fastify)"
|
|
56
|
-
};
|
|
57
|
-
if ("koa" in allDeps) return {
|
|
58
|
-
framework: "koa",
|
|
59
|
-
confidence: "high",
|
|
60
|
-
matchedRule: "package.json dependencies (koa)"
|
|
61
|
-
};
|
|
62
|
-
if ("express" in allDeps) return {
|
|
63
|
-
framework: "express",
|
|
64
|
-
confidence: "high",
|
|
65
|
-
matchedRule: "package.json dependencies (express)"
|
|
66
|
-
};
|
|
67
|
-
if ("elysia" in allDeps) return {
|
|
68
|
-
framework: "elysia",
|
|
69
|
-
confidence: "high",
|
|
70
|
-
matchedRule: "package.json dependencies (elysia)"
|
|
71
|
-
};
|
|
72
|
-
if ("hono" in allDeps) return {
|
|
73
|
-
framework: "hono",
|
|
74
|
-
confidence: "high",
|
|
75
|
-
matchedRule: "package.json dependencies (hono)"
|
|
76
|
-
};
|
|
77
|
-
if ("vue" in allDeps && !("react" in allDeps)) return {
|
|
78
|
-
framework: "vue",
|
|
79
|
-
confidence: "high",
|
|
80
|
-
matchedRule: "package.json dependencies (vue)"
|
|
81
|
-
};
|
|
82
|
-
if ("react" in allDeps || "vite" in allDeps) return {
|
|
83
|
-
framework: "react",
|
|
84
|
-
confidence: "high",
|
|
85
|
-
matchedRule: "package.json dependencies (react/vite)"
|
|
86
|
-
};
|
|
87
|
-
} catch {}
|
|
88
|
-
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 {
|
|
89
|
-
framework: "next",
|
|
90
|
-
confidence: "medium",
|
|
91
|
-
matchedRule: "next.config file presence"
|
|
92
|
-
};
|
|
93
|
-
for (const [framework, base] of [
|
|
94
|
-
["nuxt", "nuxt.config"],
|
|
95
|
-
["sveltekit", "svelte.config"],
|
|
96
|
-
["astro", "astro.config"]
|
|
97
|
-
]) if ([
|
|
98
|
-
"js",
|
|
99
|
-
"mjs",
|
|
100
|
-
"ts"
|
|
101
|
-
].some((ext) => fs.existsSync(path.join(root, `${base}.${ext}`)))) return {
|
|
102
|
-
framework,
|
|
103
|
-
confidence: "medium",
|
|
104
|
-
matchedRule: `${base} file presence`
|
|
105
|
-
};
|
|
106
|
-
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 {
|
|
107
|
-
framework: "react",
|
|
108
|
-
confidence: "medium",
|
|
109
|
-
matchedRule: "vite.config file presence"
|
|
110
|
-
};
|
|
111
|
-
return {
|
|
112
|
-
framework: "hono",
|
|
113
|
-
confidence: "fallback",
|
|
114
|
-
matchedRule: "default fallback"
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/utils/package-json.ts
|
|
119
|
-
/** Version of this CLI, which the other Dialex packages share. Read from the nearest package.json. */
|
|
120
|
-
function cliVersion() {
|
|
121
|
-
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
122
|
-
for (let i = 0; i < 4; i++) {
|
|
123
|
-
try {
|
|
124
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
|
|
125
|
-
if (pkg.name === "@dialexjs/cli") return pkg.version;
|
|
126
|
-
} catch {}
|
|
127
|
-
dir = path.dirname(dir);
|
|
37
|
+
async function applyFixes(files, configuredLocales, defaultLocale) {
|
|
38
|
+
let fixed = 0;
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const df = await loadDictionaryFile(file);
|
|
41
|
+
if (!df?.complete) continue;
|
|
42
|
+
const present = listLocales(df);
|
|
43
|
+
const targets = Array.from(/* @__PURE__ */ new Set([...present, ...configuredLocales]));
|
|
44
|
+
const sourceOrder = [defaultLocale, ...present.filter((l) => l !== defaultLocale)].filter((l) => present.includes(l));
|
|
45
|
+
const allPaths = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const locale of present) for (const leaf of listLeaves(df, locale)) allPaths.set(leaf.path.join("\0"), leaf.path);
|
|
47
|
+
let changed = false;
|
|
48
|
+
for (const target of targets) for (const leafPath of allPaths.values()) {
|
|
49
|
+
if (hasPath(df, target, leafPath)) continue;
|
|
50
|
+
const source = sourceOrder.find((l) => l !== target && hasPath(df, l, leafPath));
|
|
51
|
+
if (source && copyLeaf(df, source, target, leafPath, (v) => `[TODO] ${v}`)) {
|
|
52
|
+
fixed++;
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (changed) await saveDictionaryFile(df);
|
|
128
57
|
}
|
|
58
|
+
return fixed;
|
|
129
59
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const has = (file) => fs.existsSync(path.join(root, file));
|
|
133
|
-
if (has("bun.lock") || has("bun.lockb")) return "bun install";
|
|
134
|
-
if (has("pnpm-lock.yaml")) return "pnpm install";
|
|
135
|
-
if (has("yarn.lock")) return "yarn install";
|
|
136
|
-
return "npm install";
|
|
60
|
+
function escapeAnnotation(value) {
|
|
61
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
137
62
|
}
|
|
138
|
-
const sorted = (deps) => Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
|
|
139
63
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
64
|
+
* What a formatter (Prettier, Biome, oxfmt) can change without changing the code: whitespace,
|
|
65
|
+
* quote style, quoted object keys and trailing commas. Comparing this form keeps `check` quiet
|
|
66
|
+
* when your pre-commit hook formats the generated files.
|
|
143
67
|
*/
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
68
|
+
function normalizeGenerated(text) {
|
|
69
|
+
return text.replace(/'/g, "\"").replace(/"([A-Za-z_$][\w$]*)"\s*:/g, "$1:").replace(/\s+/g, "").replace(/,(?=[\]})])/g, "");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Generated files that exist but no longer match the dictionaries and config. Nothing builds them
|
|
73
|
+
* for you any more, so a stale `dialex.generated.ts` would otherwise fail silently at runtime.
|
|
74
|
+
* With `fix` they are rewritten.
|
|
75
|
+
*/
|
|
76
|
+
function staleGeneratedFiles(root, options) {
|
|
77
|
+
let rendered;
|
|
78
|
+
try {
|
|
79
|
+
rendered = renderGenerated(root, { config: options.config });
|
|
80
|
+
} catch {
|
|
81
|
+
return {
|
|
82
|
+
stale: [],
|
|
83
|
+
rewritten: 0
|
|
84
|
+
};
|
|
158
85
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
86
|
+
const stale = [];
|
|
87
|
+
let rewritten = 0;
|
|
88
|
+
const targets = [[rendered.outputPath, rendered.content], [rendered.dtsPath, rendered.dtsContent]];
|
|
89
|
+
if (fs.existsSync(rendered.outputPath)) for (const extra of rendered.extraFiles) targets.push([extra.path, extra.content]);
|
|
90
|
+
for (const [file, content] of targets) {
|
|
91
|
+
if (content === void 0) continue;
|
|
92
|
+
const isExtra = rendered.extraFiles.some((f) => f.path === file);
|
|
93
|
+
if (!fs.existsSync(file)) {
|
|
94
|
+
if (!isExtra) continue;
|
|
95
|
+
if (options.fix) {
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
97
|
+
fs.writeFileSync(file, content, "utf-8");
|
|
98
|
+
rewritten++;
|
|
99
|
+
} else stale.push({
|
|
100
|
+
file: path.relative(root, file),
|
|
101
|
+
message: "Missing. Run `dx generate` (or `dx check --fix`)."
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (normalizeGenerated(fs.readFileSync(file, "utf-8")) === normalizeGenerated(content)) continue;
|
|
106
|
+
if (options.fix) {
|
|
107
|
+
fs.writeFileSync(file, content, "utf-8");
|
|
108
|
+
rewritten++;
|
|
109
|
+
} else stale.push({
|
|
110
|
+
file: path.relative(root, file),
|
|
111
|
+
message: "Out of date with your dictionaries or config. Run `dx generate` (or `dx check --fix`)."
|
|
163
112
|
});
|
|
164
|
-
added.push("@dialexjs/cli");
|
|
165
113
|
}
|
|
166
|
-
return
|
|
114
|
+
return {
|
|
115
|
+
stale,
|
|
116
|
+
rewritten
|
|
117
|
+
};
|
|
167
118
|
}
|
|
168
|
-
|
|
169
|
-
//#region src/commands/init.ts
|
|
170
|
-
async function runInit(options = {}) {
|
|
119
|
+
async function runCheck(options = {}) {
|
|
171
120
|
const root = options.cwd || process.cwd();
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
label: "Express",
|
|
188
|
-
hint: detected.framework === "express" ? "detected" : void 0
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
value: "fastify",
|
|
192
|
-
label: "Fastify",
|
|
193
|
-
hint: detected.framework === "fastify" ? "detected" : void 0
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
value: "koa",
|
|
197
|
-
label: "Koa",
|
|
198
|
-
hint: detected.framework === "koa" ? "detected" : void 0
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
value: "nestjs",
|
|
202
|
-
label: "NestJS",
|
|
203
|
-
hint: detected.framework === "nestjs" ? "detected" : void 0
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
value: "next",
|
|
207
|
-
label: "Next.js",
|
|
208
|
-
hint: detected.framework === "next" ? "detected" : void 0
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
value: "react",
|
|
212
|
-
label: "React / Vite",
|
|
213
|
-
hint: detected.framework === "react" ? "detected" : void 0
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
value: "elysia",
|
|
217
|
-
label: "Elysia",
|
|
218
|
-
hint: detected.framework === "elysia" ? "detected" : void 0
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
value: "sveltekit",
|
|
222
|
-
label: "SvelteKit",
|
|
223
|
-
hint: detected.framework === "sveltekit" ? "detected" : void 0
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
value: "astro",
|
|
227
|
-
label: "Astro",
|
|
228
|
-
hint: detected.framework === "astro" ? "detected" : void 0
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
value: "vue",
|
|
232
|
-
label: "Vue / Vite",
|
|
233
|
-
hint: detected.framework === "vue" ? "detected" : void 0
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
value: "nuxt",
|
|
237
|
-
label: "Nuxt",
|
|
238
|
-
hint: detected.framework === "nuxt" ? "detected" : void 0
|
|
239
|
-
}
|
|
240
|
-
];
|
|
241
|
-
const selectedFramework = await p.select({
|
|
242
|
-
message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
|
|
243
|
-
options: frameworkOptions,
|
|
244
|
-
initialValue: detected.framework
|
|
245
|
-
});
|
|
246
|
-
if (p.isCancel(selectedFramework)) {
|
|
247
|
-
p.cancel("Operation cancelled.");
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
framework = selectedFramework;
|
|
251
|
-
const inputDefault = await p.text({
|
|
252
|
-
message: "What is your default locale?",
|
|
253
|
-
initialValue: "en",
|
|
254
|
-
validate: (v) => !v || !v.trim() ? "Default locale cannot be empty" : void 0
|
|
255
|
-
});
|
|
256
|
-
if (p.isCancel(inputDefault)) {
|
|
257
|
-
p.cancel("Operation cancelled.");
|
|
258
|
-
return;
|
|
121
|
+
const quiet = options.json === true;
|
|
122
|
+
const inlineConfig = options.config ? { configFile: options.config } : {};
|
|
123
|
+
const config = resolveDialexConfig(root, inlineConfig);
|
|
124
|
+
const include = config.include || "**/*.content.ts";
|
|
125
|
+
const configuredLocales = config.locales || [];
|
|
126
|
+
const defaultLocale = config.defaultLocale || "en";
|
|
127
|
+
const files = fg.sync(include, {
|
|
128
|
+
cwd: root,
|
|
129
|
+
absolute: true,
|
|
130
|
+
ignore: DICTIONARY_IGNORE
|
|
131
|
+
});
|
|
132
|
+
if (files.length === 0) {
|
|
133
|
+
if (!quiet) {
|
|
134
|
+
const patternStr = Array.isArray(include) ? include.join(", ") : include;
|
|
135
|
+
logger.warn(`No dictionary files found matching "${patternStr}".`);
|
|
259
136
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
137
|
+
const empty = {
|
|
138
|
+
success: true,
|
|
139
|
+
diagnostics: [],
|
|
140
|
+
totalIssues: 0,
|
|
141
|
+
totalWarnings: 0,
|
|
142
|
+
fixed: 0
|
|
143
|
+
};
|
|
144
|
+
if (quiet) console.log(JSON.stringify(empty, null, 2));
|
|
145
|
+
return empty;
|
|
146
|
+
}
|
|
147
|
+
let fixed = 0;
|
|
148
|
+
if (options.fix) {
|
|
149
|
+
fixed = await applyFixes(files, configuredLocales, defaultLocale);
|
|
150
|
+
if (fixed > 0 && !quiet) logger.success(`Inserted ${fixed} missing key${fixed === 1 ? "" : "s"} marked ${TODO_PREFIX.trim()}`);
|
|
151
|
+
}
|
|
152
|
+
const generated = staleGeneratedFiles(root, options);
|
|
153
|
+
fixed += generated.rewritten;
|
|
154
|
+
if (generated.rewritten > 0 && !quiet) logger.success(`Regenerated ${generated.rewritten} out-of-date generated file${generated.rewritten === 1 ? "" : "s"}`);
|
|
155
|
+
const analysis = await analyzeProject({
|
|
156
|
+
root,
|
|
157
|
+
config,
|
|
158
|
+
src: options.src && options.src.length > 0 ? options.src : void 0,
|
|
159
|
+
runtime: true
|
|
160
|
+
});
|
|
161
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
162
|
+
const dictionaryFiles = new Set(analysis.files);
|
|
163
|
+
for (const issue of analysis.issues) {
|
|
164
|
+
if (options.failOnStale && issue.code === "stale-translation") issue.level = "error";
|
|
165
|
+
if (issue.level === "info") continue;
|
|
166
|
+
const rel = path.relative(root, issue.file);
|
|
167
|
+
let diag = byFile.get(rel);
|
|
168
|
+
if (!diag) {
|
|
169
|
+
diag = {
|
|
170
|
+
file: rel,
|
|
171
|
+
errors: [],
|
|
172
|
+
warnings: [],
|
|
173
|
+
annotations: []
|
|
174
|
+
};
|
|
175
|
+
byFile.set(rel, diag);
|
|
269
176
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
177
|
+
if (dictionaryFiles.has(issue.file) && issue.dictionary && !diag.name) diag.name = issue.dictionary;
|
|
178
|
+
addIssue(diag, issue);
|
|
179
|
+
}
|
|
180
|
+
for (const { file, message } of generated.stale) {
|
|
181
|
+
let diag = byFile.get(file);
|
|
182
|
+
if (!diag) {
|
|
183
|
+
diag = {
|
|
184
|
+
file,
|
|
185
|
+
errors: [],
|
|
186
|
+
warnings: [],
|
|
187
|
+
annotations: []
|
|
188
|
+
};
|
|
189
|
+
byFile.set(file, diag);
|
|
277
190
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
191
|
+
diag.errors.push(message);
|
|
192
|
+
diag.annotations.push({
|
|
193
|
+
level: "error",
|
|
194
|
+
message
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const diagnostics = [...byFile.values()];
|
|
198
|
+
const totalIssues = diagnostics.reduce((n, d) => n + d.errors.length, 0);
|
|
199
|
+
const totalWarnings = diagnostics.reduce((n, d) => n + d.warnings.length, 0);
|
|
200
|
+
const reported = diagnostics.filter((d) => d.errors.length > 0 || d.warnings.length > 0);
|
|
201
|
+
const result = {
|
|
202
|
+
success: totalIssues === 0,
|
|
203
|
+
diagnostics: reported,
|
|
204
|
+
totalIssues,
|
|
205
|
+
totalWarnings,
|
|
206
|
+
fixed
|
|
207
|
+
};
|
|
208
|
+
if (options.github) for (const d of reported) for (const a of d.annotations) {
|
|
209
|
+
const location = `file=${d.file}${a.line ? `,line=${a.line}` : ""}`;
|
|
210
|
+
console.log(`::${a.level} ${location},title=dialex::${escapeAnnotation(a.message)}`);
|
|
211
|
+
}
|
|
212
|
+
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
213
|
+
else if (reported.length > 0) {
|
|
214
|
+
logger.log("");
|
|
215
|
+
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"}:`);
|
|
216
|
+
logger.log("");
|
|
217
|
+
for (const d of reported) {
|
|
218
|
+
logger.log(` ${pc.underline(pc.bold(d.file))} ${d.name ? pc.dim(`(${d.name})`) : ""}:`);
|
|
219
|
+
for (const a of d.annotations) {
|
|
220
|
+
const where = a.line ? pc.dim(`:${a.line}`) : "";
|
|
221
|
+
const mark = a.level === "error" ? pc.red("✖") : pc.yellow("⚠");
|
|
222
|
+
logger.log(` ${mark} ${a.message}${where}`);
|
|
292
223
|
}
|
|
293
|
-
|
|
294
|
-
const mod = parseModule(`import { defineConfig } from "dialexjs";
|
|
295
|
-
|
|
296
|
-
export default defineConfig({});
|
|
297
|
-
`);
|
|
298
|
-
mod.exports.default.$args[0].defaultLocale = defaultLocale;
|
|
299
|
-
mod.exports.default.$args[0].locales = localeList;
|
|
300
|
-
await writeFile(mod, configPath);
|
|
301
|
-
logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
|
|
224
|
+
logger.log("");
|
|
302
225
|
}
|
|
303
|
-
} catch (err) {
|
|
304
|
-
logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
|
|
305
|
-
const fallback = `import { defineConfig } from "dialexjs";
|
|
306
|
-
|
|
307
|
-
export default defineConfig({
|
|
308
|
-
defaultLocale: "${defaultLocale}",
|
|
309
|
-
locales: [${localeList.map((l) => `"${l}"`).join(", ")}],
|
|
310
|
-
});
|
|
311
|
-
`;
|
|
312
|
-
fs.writeFileSync(configPath, fallback, "utf-8");
|
|
313
226
|
}
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
path.join(root, "vite.config.js"),
|
|
318
|
-
path.join(root, "vite.config.mjs")
|
|
319
|
-
];
|
|
320
|
-
for (const vConfig of possibleViteConfigs) if (fs.existsSync(vConfig)) try {
|
|
321
|
-
const vMod = await loadFile(vConfig);
|
|
322
|
-
addVitePlugin(vMod, {
|
|
323
|
-
from: "dialexjs/vite",
|
|
324
|
-
imported: "dialexPlugin",
|
|
325
|
-
constructor: "dialexPlugin"
|
|
326
|
-
});
|
|
327
|
-
await writeFile(vMod, vConfig);
|
|
328
|
-
logger.success(`Injected dialexPlugin() into ${pc.bold(path.basename(vConfig))} via Magicast`);
|
|
329
|
-
break;
|
|
330
|
-
} catch {}
|
|
227
|
+
if (totalIssues > 0) {
|
|
228
|
+
if (options.exitOnError !== false) process.exit(1);
|
|
229
|
+
return result;
|
|
331
230
|
}
|
|
332
|
-
if (
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
231
|
+
if (!quiet) {
|
|
232
|
+
const warningNote = totalWarnings > 0 ? ` (${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})` : "";
|
|
233
|
+
logger.success(`All ${files.length} dictionary file${files.length === 1 ? "" : "s"} passed checks with 0 issues${warningNote}!`);
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/commands/export.ts
|
|
239
|
+
const escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
240
|
+
const csvCell = (value) => /[",\r\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
|
|
241
|
+
async function runExport(options = {}) {
|
|
242
|
+
const root = options.cwd || process.cwd();
|
|
243
|
+
const format = options.format || "json";
|
|
244
|
+
if (![
|
|
245
|
+
"json",
|
|
246
|
+
"csv",
|
|
247
|
+
"xliff"
|
|
248
|
+
].includes(format)) throw new Error(`Unsupported format "${format}". Use json, csv, or xliff.`);
|
|
249
|
+
const outDir = path.resolve(root, options.out || "dialex-export");
|
|
250
|
+
const project = await loadProject(root, options.config);
|
|
251
|
+
const defaultLocale = project.config.defaultLocale || "en";
|
|
252
|
+
const table = /* @__PURE__ */ new Map();
|
|
253
|
+
const orderedKeys = /* @__PURE__ */ new Set();
|
|
254
|
+
let skippedFunctions = 0;
|
|
255
|
+
const localeSet = new Set(project.config.locales || []);
|
|
256
|
+
for (const { df } of project.dictionaries) listLocales(df).forEach((l) => localeSet.add(l));
|
|
257
|
+
let locales = Array.from(localeSet);
|
|
258
|
+
if (options.locale && options.locale.length > 0) locales = locales.filter((l) => options.locale.includes(l) || l === defaultLocale);
|
|
259
|
+
for (const locale of locales) table.set(locale, /* @__PURE__ */ new Map());
|
|
260
|
+
for (const { name, df } of project.dictionaries) for (const locale of listLocales(df)) {
|
|
261
|
+
if (!table.has(locale)) continue;
|
|
262
|
+
for (const leaf of listLeaves(df, locale)) if (leaf.kind === "string" && leaf.value !== void 0) {
|
|
263
|
+
const key = toKey(name, leaf.path);
|
|
264
|
+
table.get(locale).set(key, leaf.value);
|
|
265
|
+
orderedKeys.add(key);
|
|
266
|
+
} else if (locale === defaultLocale) skippedFunctions++;
|
|
267
|
+
}
|
|
268
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
269
|
+
const files = [];
|
|
270
|
+
const write = (name, content) => {
|
|
271
|
+
const file = path.join(outDir, name);
|
|
272
|
+
fs.writeFileSync(file, content, "utf-8");
|
|
273
|
+
files.push(path.relative(root, file));
|
|
274
|
+
};
|
|
275
|
+
if (format === "json") for (const [locale, values] of table) write(`${locale}.json`, JSON.stringify(Object.fromEntries(values), null, 2) + "\n");
|
|
276
|
+
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");
|
|
277
|
+
else {
|
|
278
|
+
const source = table.get(defaultLocale);
|
|
279
|
+
for (const [locale, values] of table) {
|
|
280
|
+
if (locale === defaultLocale) continue;
|
|
281
|
+
const units = [...orderedKeys].map((key) => {
|
|
282
|
+
const src = source?.get(key) ?? "";
|
|
283
|
+
const target = values.get(key);
|
|
284
|
+
const targetXml = target === void 0 ? "" : `<target>${escapeXml(target)}</target>`;
|
|
285
|
+
return ` <trans-unit id="${escapeXml(key)}"><source>${escapeXml(src)}</source>${targetXml}</trans-unit>`;
|
|
343
286
|
});
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
287
|
+
write(`${locale}.xlf`, `<?xml version="1.0" encoding="UTF-8"?>
|
|
288
|
+
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
|
289
|
+
<file original="dialex" source-language="${escapeXml(defaultLocale)}" target-language="${escapeXml(locale)}" datatype="plaintext">
|
|
290
|
+
<body>
|
|
291
|
+
${units.join("\n")}
|
|
292
|
+
</body>
|
|
293
|
+
</file>
|
|
294
|
+
</xliff>
|
|
295
|
+
`);
|
|
348
296
|
}
|
|
349
297
|
}
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
fs.writeFileSync(dictPath, dictContent, "utf-8");
|
|
364
|
-
logger.success(`Created starter dictionary at ${pc.bold(path.relative(root, dictPath))}`);
|
|
365
|
-
} else logger.info("home.content.ts already exists, skipping creation.");
|
|
366
|
-
generateDictionaries(root);
|
|
367
|
-
logger.success("Generated types and standalone dictionary registry!");
|
|
368
|
-
const pkgPath = path.join(root, "package.json");
|
|
369
|
-
if (fs.existsSync(pkgPath)) try {
|
|
370
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
371
|
-
const added = addRequiredPackages(pkg);
|
|
372
|
-
pkg.scripts = pkg.scripts || {};
|
|
373
|
-
const addScript = !pkg.scripts["dx:generate"];
|
|
374
|
-
if (addScript) pkg.scripts["dx:generate"] = "dx generate";
|
|
375
|
-
if (added.length > 0 || addScript) fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
376
|
-
if (added.length > 0) {
|
|
377
|
-
logger.success(`Added ${added.join(" and ")} to package.json`);
|
|
378
|
-
logger.info(`Run ${pc.bold(installCommand(root))} to install them.`);
|
|
379
|
-
}
|
|
380
|
-
if (addScript) logger.success("Added \"dx:generate\" script to package.json");
|
|
381
|
-
} catch {
|
|
382
|
-
logger.warn("Could not update package.json; add dialexjs and @dialexjs/cli manually.");
|
|
383
|
-
}
|
|
384
|
-
else logger.warn("No package.json found; install dialexjs and @dialexjs/cli in your project.");
|
|
385
|
-
if (options.ai === true || options.yes && options.ai !== false && options.noAi !== true) try {
|
|
386
|
-
const skillDir = path.join(root, ".agents/skills/dialex");
|
|
387
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
388
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
389
|
-
name: dialex
|
|
390
|
-
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.
|
|
391
|
-
---
|
|
392
|
-
|
|
393
|
-
# Dialex Internationalization
|
|
394
|
-
|
|
395
|
-
Dialex is a high-performance, type-safe internationalization toolchain for JavaScript and TypeScript.
|
|
396
|
-
|
|
397
|
-
## Core Concepts
|
|
398
|
-
- Dictionaries are defined with \`defineDictionary("name", { ... })\` in \`*.content.ts\` files.
|
|
399
|
-
- Keys must match across all locales.
|
|
400
|
-
- ICU message syntax is supported for plurals and selects.
|
|
401
|
-
- Parity is validated at compile time with \`dialex check\`.
|
|
402
|
-
|
|
403
|
-
## Useful Commands
|
|
404
|
-
- \`dialex check --json\`: Check dictionary parity and references.
|
|
405
|
-
- \`dialex check --fix\`: Automatically insert missing keys marked with [TODO].
|
|
406
|
-
- \`dialex generate\`: Rebuild standalone dictionary bundle and types.
|
|
407
|
-
- \`dialex translate --locale <locale>\`: Automatically translate missing keys.
|
|
408
|
-
`, "utf-8");
|
|
409
|
-
const mcpConfigPath = path.join(root, ".mcp.json");
|
|
410
|
-
let mcpConfig = {};
|
|
411
|
-
if (fs.existsSync(mcpConfigPath)) try {
|
|
412
|
-
mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
413
|
-
} catch {
|
|
414
|
-
mcpConfig = {};
|
|
415
|
-
}
|
|
416
|
-
mcpConfig.mcpServers = mcpConfig.mcpServers || {};
|
|
417
|
-
mcpConfig.mcpServers.dialex = mcpConfig.mcpServers.dialex || {
|
|
418
|
-
command: "npx",
|
|
419
|
-
args: ["@dialexjs/mcp"]
|
|
420
|
-
};
|
|
421
|
-
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
422
|
-
const agentsMdPath = path.join(root, "AGENTS.md");
|
|
423
|
-
const dialexAgentSnippet = `
|
|
424
|
-
## Internationalization (Dialex)
|
|
425
|
-
|
|
426
|
-
This project uses [Dialex](https://github.com/awaiden/dialex) for type-safe internationalization.
|
|
427
|
-
- Dictionaries are in \`*.content.ts\` files using \`defineDictionary\`.
|
|
428
|
-
- Run \`dialex check --json\` to verify parity.
|
|
429
|
-
- Run \`dialex check --fix\` to insert missing keys marked with \`[TODO]\`.
|
|
430
|
-
- Run \`dx generate\` (alias of \`dialex\`) or \`npm run dx:generate\` to compile dictionary bundles and update types.
|
|
431
|
-
`;
|
|
432
|
-
if (!fs.existsSync(agentsMdPath)) fs.writeFileSync(agentsMdPath, dialexAgentSnippet.trimStart(), "utf-8");
|
|
433
|
-
else {
|
|
434
|
-
const existing = fs.readFileSync(agentsMdPath, "utf-8");
|
|
435
|
-
if (!existing.includes("Dialex")) fs.writeFileSync(agentsMdPath, existing.trimEnd() + "\n" + dialexAgentSnippet, "utf-8");
|
|
436
|
-
}
|
|
437
|
-
logger.success("Configured AI assistant support (.agents/skills, .mcp.json, AGENTS.md)");
|
|
438
|
-
} catch (err) {
|
|
439
|
-
logger.warn(`Could not finish AI configuration: ${err.message || String(err)}`);
|
|
298
|
+
const quiet = options.json === true;
|
|
299
|
+
const result = {
|
|
300
|
+
success: true,
|
|
301
|
+
files,
|
|
302
|
+
keys: orderedKeys.size,
|
|
303
|
+
skippedFunctions,
|
|
304
|
+
skippedFiles: project.unsupported
|
|
305
|
+
};
|
|
306
|
+
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
307
|
+
else {
|
|
308
|
+
logger.success(`Exported ${orderedKeys.size} key${orderedKeys.size === 1 ? "" : "s"} to ${files.length} ${format} file${files.length === 1 ? "" : "s"} in ${path.relative(root, outDir) || "."}`);
|
|
309
|
+
if (skippedFunctions > 0) logger.warn(`${skippedFunctions} function value${skippedFunctions === 1 ? "" : "s"} skipped (not translatable as text)`);
|
|
310
|
+
for (const file of project.unsupported) logger.warn(`Skipped ${file}: unsupported dictionary shape`);
|
|
440
311
|
}
|
|
441
|
-
|
|
442
|
-
logger.info(pc.bold(`Quick start for ${framework.toUpperCase()}:`));
|
|
443
|
-
switch (framework) {
|
|
444
|
-
case "hono":
|
|
445
|
-
logger.log(`
|
|
446
|
-
import { Hono } from "hono";
|
|
447
|
-
import { dialex } from "dialexjs/hono";
|
|
448
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
449
|
-
|
|
450
|
-
const app = new Hono();
|
|
451
|
-
app.use("*", dialex({ dictionaries }));
|
|
452
|
-
|
|
453
|
-
app.get("/", (c) => {
|
|
454
|
-
const dict = c.var.getDictionary("home");
|
|
455
|
-
return c.json({ title: dict.title });
|
|
456
|
-
});
|
|
457
|
-
`);
|
|
458
|
-
break;
|
|
459
|
-
case "fastify":
|
|
460
|
-
logger.log(`
|
|
461
|
-
import Fastify from "fastify";
|
|
462
|
-
import { dialexPlugin } from "dialexjs/fastify";
|
|
463
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
464
|
-
|
|
465
|
-
const app = Fastify();
|
|
466
|
-
await app.register(dialexPlugin, { dictionaries });
|
|
467
|
-
|
|
468
|
-
app.get("/", (req) => {
|
|
469
|
-
const dict = req.getDictionary("home");
|
|
470
|
-
return { title: dict.title };
|
|
471
|
-
});
|
|
472
|
-
`);
|
|
473
|
-
break;
|
|
474
|
-
case "express":
|
|
475
|
-
logger.log(`
|
|
476
|
-
import express from "express";
|
|
477
|
-
import { dialex } from "dialexjs/express";
|
|
478
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
479
|
-
|
|
480
|
-
const app = express();
|
|
481
|
-
app.use(dialex({ dictionaries }));
|
|
482
|
-
|
|
483
|
-
app.get("/", (req, res) => {
|
|
484
|
-
const dict = req.getDictionary("home");
|
|
485
|
-
res.json({ title: dict.title });
|
|
486
|
-
});
|
|
487
|
-
`);
|
|
488
|
-
break;
|
|
489
|
-
case "koa":
|
|
490
|
-
logger.log(`
|
|
491
|
-
import Koa from "koa";
|
|
492
|
-
import { dialex } from "dialexjs/koa";
|
|
493
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
494
|
-
|
|
495
|
-
const app = new Koa();
|
|
496
|
-
app.use(dialex({ dictionaries }));
|
|
497
|
-
|
|
498
|
-
app.use((ctx) => {
|
|
499
|
-
const dict = ctx.getDictionary("home");
|
|
500
|
-
ctx.body = { title: dict.title };
|
|
501
|
-
});
|
|
502
|
-
`);
|
|
503
|
-
break;
|
|
504
|
-
case "nestjs":
|
|
505
|
-
logger.log(`
|
|
506
|
-
import { Module } from "@nestjs/common";
|
|
507
|
-
import { DialexModule } from "dialexjs/nestjs";
|
|
508
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
509
|
-
|
|
510
|
-
@Module({
|
|
511
|
-
imports: [DialexModule.forRoot({ dictionaries })],
|
|
512
|
-
})
|
|
513
|
-
export class AppModule {}
|
|
514
|
-
`);
|
|
515
|
-
break;
|
|
516
|
-
case "next":
|
|
517
|
-
logger.log(`
|
|
518
|
-
import { getDictionary } from "dialexjs/server";
|
|
519
|
-
|
|
520
|
-
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
|
|
521
|
-
const { locale = "en" } = await params;
|
|
522
|
-
const dict = getDictionary("home", locale);
|
|
523
|
-
return <h1>{dict.title}</h1>;
|
|
312
|
+
return result;
|
|
524
313
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
logger.log(`
|
|
553
|
-
// src/middleware.ts
|
|
554
|
-
import { dialex } from "dialexjs/astro";
|
|
555
|
-
import dictionaries from "./dialex.generated.js";
|
|
556
|
-
|
|
557
|
-
export const onRequest = dialex({ dictionaries });
|
|
558
|
-
|
|
559
|
-
// In pages: Astro.locals.getDictionary("home")
|
|
560
|
-
`);
|
|
561
|
-
break;
|
|
562
|
-
case "vue":
|
|
563
|
-
logger.log(`
|
|
564
|
-
// src/main.ts
|
|
565
|
-
import { createApp } from "vue";
|
|
566
|
-
import { createDialex } from "dialexjs/vue";
|
|
567
|
-
import App from "./App.vue";
|
|
568
|
-
|
|
569
|
-
createApp(App).use(createDialex()).mount("#app");
|
|
570
|
-
|
|
571
|
-
// In components: const dict = useDictionary("home")
|
|
572
|
-
`);
|
|
573
|
-
break;
|
|
574
|
-
case "nuxt":
|
|
575
|
-
logger.log(`
|
|
576
|
-
// nuxt.config.ts
|
|
577
|
-
export default defineNuxtConfig({
|
|
578
|
-
modules: ["dialexjs/nuxt"],
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// In components: const dict = useDictionary("home")
|
|
582
|
-
`);
|
|
583
|
-
break;
|
|
584
|
-
default: logger.log(`
|
|
585
|
-
Import dictionaries and register your framework middleware to begin!
|
|
586
|
-
Run \`npm run dx:generate\` whenever you add new dictionary files.
|
|
587
|
-
`);
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/commands/import.ts
|
|
316
|
+
const unescapeXml = (value) => value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
317
|
+
function parseCsv(text) {
|
|
318
|
+
const rows = [];
|
|
319
|
+
let row = [];
|
|
320
|
+
let cell = "";
|
|
321
|
+
let quoted = false;
|
|
322
|
+
for (let i = 0; i < text.length; i++) {
|
|
323
|
+
const ch = text[i];
|
|
324
|
+
if (quoted) {
|
|
325
|
+
if (ch === "\"" && text[i + 1] === "\"") {
|
|
326
|
+
cell += "\"";
|
|
327
|
+
i++;
|
|
328
|
+
} else if (ch === "\"") quoted = false;
|
|
329
|
+
else cell += ch;
|
|
330
|
+
} else if (ch === "\"") quoted = true;
|
|
331
|
+
else if (ch === ",") {
|
|
332
|
+
row.push(cell);
|
|
333
|
+
cell = "";
|
|
334
|
+
} else if (ch === "\n" || ch === "\r") {
|
|
335
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
336
|
+
row.push(cell);
|
|
337
|
+
rows.push(row);
|
|
338
|
+
row = [];
|
|
339
|
+
cell = "";
|
|
340
|
+
} else cell += ch;
|
|
588
341
|
}
|
|
589
|
-
if (
|
|
342
|
+
if (cell !== "" || row.length > 0) {
|
|
343
|
+
row.push(cell);
|
|
344
|
+
rows.push(row);
|
|
345
|
+
}
|
|
346
|
+
return rows.filter((r) => r.some((c) => c !== ""));
|
|
590
347
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
348
|
+
function parseEntries(content, format, fallbackLocale) {
|
|
349
|
+
const entries = [];
|
|
350
|
+
if (format === "json") {
|
|
351
|
+
const data = JSON.parse(content);
|
|
352
|
+
if (Object.keys(data).length > 0 && Object.values(data).every((v) => v && typeof v === "object" && !Array.isArray(v))) {
|
|
353
|
+
for (const [locale, values] of Object.entries(data)) for (const [key, value] of Object.entries(values)) if (typeof value === "string") entries.push({
|
|
354
|
+
locale,
|
|
355
|
+
key,
|
|
356
|
+
value
|
|
357
|
+
});
|
|
358
|
+
} else {
|
|
359
|
+
if (!fallbackLocale) throw new Error("Cannot infer the locale; pass --locale.");
|
|
360
|
+
for (const [key, value] of Object.entries(data)) if (typeof value === "string") entries.push({
|
|
361
|
+
locale: fallbackLocale,
|
|
362
|
+
key,
|
|
363
|
+
value
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
} else if (format === "csv") {
|
|
367
|
+
const [header, ...rows] = parseCsv(content);
|
|
368
|
+
if (!header || header[0] !== "key") throw new Error("CSV must start with a \"key\" column.");
|
|
369
|
+
for (const row of rows) header.slice(1).forEach((locale, i) => {
|
|
370
|
+
const value = row[i + 1];
|
|
371
|
+
if (value !== void 0 && value !== "") entries.push({
|
|
372
|
+
locale,
|
|
373
|
+
key: row[0],
|
|
374
|
+
value
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
const locale = /target-language="([^"]*)"/.exec(content)?.[1] ?? fallbackLocale;
|
|
379
|
+
if (!locale) throw new Error("Cannot infer the locale; pass --locale.");
|
|
380
|
+
for (const match of content.matchAll(/<trans-unit\b[^>]*\bid="([^"]*)"[^>]*>([\s\S]*?)<\/trans-unit>/g)) {
|
|
381
|
+
const target = /<target\b[^>]*>([\s\S]*?)<\/target>/.exec(match[2])?.[1];
|
|
382
|
+
if (target !== void 0 && target !== "") entries.push({
|
|
383
|
+
locale,
|
|
384
|
+
key: unescapeXml(match[1]),
|
|
385
|
+
value: unescapeXml(target)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return entries;
|
|
390
|
+
}
|
|
391
|
+
function inferFormat(file) {
|
|
392
|
+
const ext = path.extname(file).toLowerCase();
|
|
393
|
+
if (ext === ".json") return "json";
|
|
394
|
+
if (ext === ".csv") return "csv";
|
|
395
|
+
if (ext === ".xlf" || ext === ".xliff") return "xliff";
|
|
396
|
+
throw new Error(`Cannot infer format from "${ext}". Pass --format json|csv|xliff.`);
|
|
606
397
|
}
|
|
607
398
|
/**
|
|
608
|
-
*
|
|
609
|
-
*
|
|
399
|
+
* Why `value` must not replace the current text, if it is an ICU message that is invalid or uses
|
|
400
|
+
* different arguments than the same key in another locale.
|
|
610
401
|
*/
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
402
|
+
function icuProblem(df, defaultLocale, locale, keyPath, value) {
|
|
403
|
+
if (!value.includes("{")) return void 0;
|
|
404
|
+
let signature;
|
|
405
|
+
try {
|
|
406
|
+
signature = argumentSignature(parseMessage(value));
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return `invalid ICU: ${error.message}`;
|
|
409
|
+
}
|
|
410
|
+
const locales = listLocales(df);
|
|
411
|
+
for (const other of [defaultLocale, ...locales.filter((l) => l !== defaultLocale)]) {
|
|
412
|
+
if (other === locale) continue;
|
|
413
|
+
const reference = getString(df, other, keyPath);
|
|
414
|
+
if (!reference?.includes("{")) continue;
|
|
415
|
+
try {
|
|
416
|
+
const expected = argumentSignature(parseMessage(reference));
|
|
417
|
+
return expected === signature ? void 0 : `arguments differ from "${other}": expected ${expected || "none"}, found ${signature || "none"}`;
|
|
418
|
+
} catch {
|
|
419
|
+
continue;
|
|
629
420
|
}
|
|
630
|
-
if (changed) await saveDictionaryFile(df);
|
|
631
421
|
}
|
|
632
|
-
return fixed;
|
|
633
422
|
}
|
|
634
|
-
function
|
|
635
|
-
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
636
|
-
}
|
|
637
|
-
async function runCheck(options = {}) {
|
|
423
|
+
async function runImport(options) {
|
|
638
424
|
const root = options.cwd || process.cwd();
|
|
425
|
+
const file = path.resolve(root, options.file);
|
|
426
|
+
const format = options.format || inferFormat(file);
|
|
427
|
+
const fallbackLocale = options.locale ?? path.basename(file, path.extname(file));
|
|
428
|
+
const entries = parseEntries(fs.readFileSync(file, "utf-8"), format, options.locale ?? fallbackLocale);
|
|
429
|
+
const project = await loadProject(root, options.config);
|
|
430
|
+
const byName = new Map(project.dictionaries.map((d) => [d.name, d]));
|
|
639
431
|
const quiet = options.json === true;
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
432
|
+
const result = {
|
|
433
|
+
success: true,
|
|
434
|
+
created: 0,
|
|
435
|
+
updated: 0,
|
|
436
|
+
skipped: [],
|
|
437
|
+
rejected: [],
|
|
438
|
+
unknown: [],
|
|
439
|
+
errors: []
|
|
440
|
+
};
|
|
441
|
+
const touched = /* @__PURE__ */ new Set();
|
|
442
|
+
const lockInUse = readLock(root) !== void 0;
|
|
443
|
+
const sourceLocale = readLock(root)?.sourceLocale ?? project.config.defaultLocale ?? "en";
|
|
444
|
+
const { lock } = lockFor(root, sourceLocale);
|
|
445
|
+
let recorded = 0;
|
|
446
|
+
for (const { locale, key, value } of entries) {
|
|
447
|
+
const [name, ...keyPath] = key.split(".");
|
|
448
|
+
const dict = byName.get(name);
|
|
449
|
+
if (!dict || keyPath.length === 0) {
|
|
450
|
+
result.unknown.push(key);
|
|
451
|
+
continue;
|
|
654
452
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
453
|
+
if (!listLocales(dict.df).some((l) => hasPath(dict.df, l, keyPath)) && !options.allowNew) {
|
|
454
|
+
result.unknown.push(key);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const problem = icuProblem(dict.df, project.config.defaultLocale ?? "en", locale, keyPath, value);
|
|
458
|
+
if (problem) {
|
|
459
|
+
result.rejected.push(`${locale}:${key} (${problem})`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const outcome = setString(dict.df, locale, keyPath, value);
|
|
463
|
+
if (outcome === "skipped") result.skipped.push(`${locale}:${key}`);
|
|
464
|
+
else {
|
|
465
|
+
result[outcome]++;
|
|
466
|
+
touched.add(dict.rel);
|
|
467
|
+
const sourceText = getString(dict.df, sourceLocale, keyPath);
|
|
468
|
+
if (lockInUse && locale !== sourceLocale && sourceText !== void 0) {
|
|
469
|
+
record(lock, locale, key, sourceText);
|
|
470
|
+
recorded++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (recorded > 0) writeLock(root, lock);
|
|
475
|
+
for (const dict of project.dictionaries) if (touched.has(dict.rel)) await saveDictionaryFile(dict.df);
|
|
476
|
+
if (result.rejected.length > 0) result.success = false;
|
|
477
|
+
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
478
|
+
else {
|
|
479
|
+
logger.success(`Imported ${result.created + result.updated} value${result.created + result.updated === 1 ? "" : "s"} (${result.created} added, ${result.updated} updated)`);
|
|
480
|
+
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 ? ", ..." : ""}`);
|
|
481
|
+
if (result.rejected.length > 0) logger.warn(`${result.rejected.length} value${result.rejected.length === 1 ? "" : "s"} rejected: ${result.rejected.slice(0, 5).join("; ")}`);
|
|
482
|
+
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(", ")}`);
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/utils/detector.ts
|
|
488
|
+
/**
|
|
489
|
+
* Automatically inspects package.json dependencies and configuration files
|
|
490
|
+
* to detect the active framework in the target project.
|
|
491
|
+
*/
|
|
492
|
+
function detectFramework(root) {
|
|
493
|
+
const pkgPath = path.join(root, "package.json");
|
|
494
|
+
if (fs.existsSync(pkgPath)) try {
|
|
495
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
496
|
+
const allDeps = {
|
|
497
|
+
...pkg.dependencies,
|
|
498
|
+
...pkg.devDependencies
|
|
499
|
+
};
|
|
500
|
+
if ("nuxt" in allDeps) return {
|
|
501
|
+
framework: "nuxt",
|
|
502
|
+
confidence: "high",
|
|
503
|
+
matchedRule: "package.json dependencies (nuxt)"
|
|
504
|
+
};
|
|
505
|
+
if ("@sveltejs/kit" in allDeps) return {
|
|
506
|
+
framework: "sveltekit",
|
|
507
|
+
confidence: "high",
|
|
508
|
+
matchedRule: "package.json dependencies (@sveltejs/kit)"
|
|
509
|
+
};
|
|
510
|
+
if ("astro" in allDeps) return {
|
|
511
|
+
framework: "astro",
|
|
512
|
+
confidence: "high",
|
|
513
|
+
matchedRule: "package.json dependencies (astro)"
|
|
514
|
+
};
|
|
515
|
+
if ("next" in allDeps) return {
|
|
516
|
+
framework: "next",
|
|
517
|
+
confidence: "high",
|
|
518
|
+
matchedRule: "package.json dependencies (next)"
|
|
519
|
+
};
|
|
520
|
+
if ("@nestjs/core" in allDeps || "@nestjs/common" in allDeps) return {
|
|
521
|
+
framework: "nestjs",
|
|
522
|
+
confidence: "high",
|
|
523
|
+
matchedRule: "package.json dependencies (@nestjs/core)"
|
|
524
|
+
};
|
|
525
|
+
if ("fastify" in allDeps) return {
|
|
526
|
+
framework: "fastify",
|
|
527
|
+
confidence: "high",
|
|
528
|
+
matchedRule: "package.json dependencies (fastify)"
|
|
529
|
+
};
|
|
530
|
+
if ("koa" in allDeps) return {
|
|
531
|
+
framework: "koa",
|
|
532
|
+
confidence: "high",
|
|
533
|
+
matchedRule: "package.json dependencies (koa)"
|
|
534
|
+
};
|
|
535
|
+
if ("express" in allDeps) return {
|
|
536
|
+
framework: "express",
|
|
537
|
+
confidence: "high",
|
|
538
|
+
matchedRule: "package.json dependencies (express)"
|
|
539
|
+
};
|
|
540
|
+
if ("elysia" in allDeps) return {
|
|
541
|
+
framework: "elysia",
|
|
542
|
+
confidence: "high",
|
|
543
|
+
matchedRule: "package.json dependencies (elysia)"
|
|
544
|
+
};
|
|
545
|
+
if ("hono" in allDeps) return {
|
|
546
|
+
framework: "hono",
|
|
547
|
+
confidence: "high",
|
|
548
|
+
matchedRule: "package.json dependencies (hono)"
|
|
549
|
+
};
|
|
550
|
+
if ("vue" in allDeps && !("react" in allDeps)) return {
|
|
551
|
+
framework: "vue",
|
|
552
|
+
confidence: "high",
|
|
553
|
+
matchedRule: "package.json dependencies (vue)"
|
|
661
554
|
};
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
config,
|
|
673
|
-
src: options.src && options.src.length > 0 ? options.src : void 0,
|
|
674
|
-
runtime: true
|
|
675
|
-
});
|
|
676
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
677
|
-
const dictionaryFiles = new Set(analysis.files);
|
|
678
|
-
for (const issue of analysis.issues) {
|
|
679
|
-
if (issue.level === "info") continue;
|
|
680
|
-
const rel = path.relative(root, issue.file);
|
|
681
|
-
let diag = byFile.get(rel);
|
|
682
|
-
if (!diag) {
|
|
683
|
-
diag = {
|
|
684
|
-
file: rel,
|
|
685
|
-
errors: [],
|
|
686
|
-
warnings: [],
|
|
687
|
-
annotations: []
|
|
688
|
-
};
|
|
689
|
-
byFile.set(rel, diag);
|
|
690
|
-
}
|
|
691
|
-
if (dictionaryFiles.has(issue.file) && issue.dictionary && !diag.name) diag.name = issue.dictionary;
|
|
692
|
-
addIssue(diag, issue);
|
|
693
|
-
}
|
|
694
|
-
const diagnostics = [...byFile.values()];
|
|
695
|
-
const totalIssues = diagnostics.reduce((n, d) => n + d.errors.length, 0);
|
|
696
|
-
const totalWarnings = diagnostics.reduce((n, d) => n + d.warnings.length, 0);
|
|
697
|
-
const reported = diagnostics.filter((d) => d.errors.length > 0 || d.warnings.length > 0);
|
|
698
|
-
const result = {
|
|
699
|
-
success: totalIssues === 0,
|
|
700
|
-
diagnostics: reported,
|
|
701
|
-
totalIssues,
|
|
702
|
-
totalWarnings,
|
|
703
|
-
fixed
|
|
555
|
+
if ("react" in allDeps || "vite" in allDeps) return {
|
|
556
|
+
framework: "react",
|
|
557
|
+
confidence: "high",
|
|
558
|
+
matchedRule: "package.json dependencies (react/vite)"
|
|
559
|
+
};
|
|
560
|
+
} catch {}
|
|
561
|
+
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 {
|
|
562
|
+
framework: "next",
|
|
563
|
+
confidence: "medium",
|
|
564
|
+
matchedRule: "next.config file presence"
|
|
704
565
|
};
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
566
|
+
for (const [framework, base] of [
|
|
567
|
+
["nuxt", "nuxt.config"],
|
|
568
|
+
["sveltekit", "svelte.config"],
|
|
569
|
+
["astro", "astro.config"]
|
|
570
|
+
]) if ([
|
|
571
|
+
"js",
|
|
572
|
+
"mjs",
|
|
573
|
+
"ts"
|
|
574
|
+
].some((ext) => fs.existsSync(path.join(root, `${base}.${ext}`)))) return {
|
|
575
|
+
framework,
|
|
576
|
+
confidence: "medium",
|
|
577
|
+
matchedRule: `${base} file presence`
|
|
578
|
+
};
|
|
579
|
+
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 {
|
|
580
|
+
framework: "react",
|
|
581
|
+
confidence: "medium",
|
|
582
|
+
matchedRule: "vite.config file presence"
|
|
583
|
+
};
|
|
584
|
+
return {
|
|
585
|
+
framework: "hono",
|
|
586
|
+
confidence: "fallback",
|
|
587
|
+
matchedRule: "default fallback"
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/utils/package-json.ts
|
|
592
|
+
/** Version of this CLI, which the other Dialex packages share. Read from the nearest package.json. */
|
|
593
|
+
function cliVersion() {
|
|
594
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
595
|
+
for (let i = 0; i < 4; i++) {
|
|
596
|
+
try {
|
|
597
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
|
|
598
|
+
if (pkg.name === "@dialexjs/cli") return pkg.version;
|
|
599
|
+
} catch {}
|
|
600
|
+
dir = path.dirname(dir);
|
|
723
601
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
602
|
+
}
|
|
603
|
+
/** The install command for the package manager a project uses, judged by its lockfile. */
|
|
604
|
+
function installCommand(root) {
|
|
605
|
+
const has = (file) => fs.existsSync(path.join(root, file));
|
|
606
|
+
if (has("bun.lock") || has("bun.lockb")) return "bun install";
|
|
607
|
+
if (has("pnpm-lock.yaml")) return "pnpm install";
|
|
608
|
+
if (has("yarn.lock")) return "yarn install";
|
|
609
|
+
return "npm install";
|
|
610
|
+
}
|
|
611
|
+
const sorted = (deps) => Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
|
|
612
|
+
/**
|
|
613
|
+
* Adds the packages a Dialex project needs: `dialexjs` as a dependency and the CLI as a dev
|
|
614
|
+
* dependency (the `dx:generate` script runs it). Packages already listed anywhere are left alone.
|
|
615
|
+
* Returns the names it added.
|
|
616
|
+
*/
|
|
617
|
+
function addRequiredPackages(pkg, version = cliVersion()) {
|
|
618
|
+
const range = version ? `^${version}` : "latest";
|
|
619
|
+
const listed = (name) => [
|
|
620
|
+
pkg.dependencies,
|
|
621
|
+
pkg.devDependencies,
|
|
622
|
+
pkg.peerDependencies
|
|
623
|
+
].some((deps) => deps && name in deps);
|
|
624
|
+
const added = [];
|
|
625
|
+
if (!listed("dialexjs")) {
|
|
626
|
+
pkg.dependencies = sorted({
|
|
627
|
+
...pkg.dependencies,
|
|
628
|
+
dialexjs: range
|
|
629
|
+
});
|
|
630
|
+
added.push("dialexjs");
|
|
727
631
|
}
|
|
728
|
-
if (!
|
|
729
|
-
|
|
730
|
-
|
|
632
|
+
if (!listed("@dialexjs/cli")) {
|
|
633
|
+
pkg.devDependencies = sorted({
|
|
634
|
+
...pkg.devDependencies,
|
|
635
|
+
"@dialexjs/cli": range
|
|
636
|
+
});
|
|
637
|
+
added.push("@dialexjs/cli");
|
|
731
638
|
}
|
|
732
|
-
return
|
|
639
|
+
return added;
|
|
733
640
|
}
|
|
734
641
|
//#endregion
|
|
735
|
-
//#region src/commands/
|
|
736
|
-
|
|
737
|
-
const csvCell = (value) => /[",\r\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
|
|
738
|
-
async function runExport(options = {}) {
|
|
642
|
+
//#region src/commands/init.ts
|
|
643
|
+
async function runInit(options = {}) {
|
|
739
644
|
const root = options.cwd || process.cwd();
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
645
|
+
const detected = detectFramework(root);
|
|
646
|
+
const isInteractive = !options.yes && process.stdin.isTTY;
|
|
647
|
+
let framework = options.framework || detected.framework;
|
|
648
|
+
let defaultLocale = options.defaultLocale || "en";
|
|
649
|
+
let localesInput = options.locales || "en,tr";
|
|
650
|
+
if (isInteractive) {
|
|
651
|
+
p.intro(pc.bgCyan(pc.black(" dialex init ")));
|
|
652
|
+
const frameworkOptions = [
|
|
653
|
+
{
|
|
654
|
+
value: "hono",
|
|
655
|
+
label: "Hono",
|
|
656
|
+
hint: detected.framework === "hono" ? "detected" : void 0
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
value: "express",
|
|
660
|
+
label: "Express",
|
|
661
|
+
hint: detected.framework === "express" ? "detected" : void 0
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
value: "fastify",
|
|
665
|
+
label: "Fastify",
|
|
666
|
+
hint: detected.framework === "fastify" ? "detected" : void 0
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
value: "koa",
|
|
670
|
+
label: "Koa",
|
|
671
|
+
hint: detected.framework === "koa" ? "detected" : void 0
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
value: "nestjs",
|
|
675
|
+
label: "NestJS",
|
|
676
|
+
hint: detected.framework === "nestjs" ? "detected" : void 0
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
value: "next",
|
|
680
|
+
label: "Next.js",
|
|
681
|
+
hint: detected.framework === "next" ? "detected" : void 0
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
value: "react",
|
|
685
|
+
label: "React / Vite",
|
|
686
|
+
hint: detected.framework === "react" ? "detected" : void 0
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
value: "elysia",
|
|
690
|
+
label: "Elysia",
|
|
691
|
+
hint: detected.framework === "elysia" ? "detected" : void 0
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
value: "sveltekit",
|
|
695
|
+
label: "SvelteKit",
|
|
696
|
+
hint: detected.framework === "sveltekit" ? "detected" : void 0
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
value: "astro",
|
|
700
|
+
label: "Astro",
|
|
701
|
+
hint: detected.framework === "astro" ? "detected" : void 0
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
value: "vue",
|
|
705
|
+
label: "Vue / Vite",
|
|
706
|
+
hint: detected.framework === "vue" ? "detected" : void 0
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
value: "nuxt",
|
|
710
|
+
label: "Nuxt",
|
|
711
|
+
hint: detected.framework === "nuxt" ? "detected" : void 0
|
|
712
|
+
}
|
|
713
|
+
];
|
|
714
|
+
const selectedFramework = await p.select({
|
|
715
|
+
message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
|
|
716
|
+
options: frameworkOptions,
|
|
717
|
+
initialValue: detected.framework
|
|
718
|
+
});
|
|
719
|
+
if (p.isCancel(selectedFramework)) {
|
|
720
|
+
p.cancel("Operation cancelled.");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
framework = selectedFramework;
|
|
724
|
+
const inputDefault = await p.text({
|
|
725
|
+
message: "What is your default locale?",
|
|
726
|
+
initialValue: "en",
|
|
727
|
+
validate: (v) => !v || !v.trim() ? "Default locale cannot be empty" : void 0
|
|
728
|
+
});
|
|
729
|
+
if (p.isCancel(inputDefault)) {
|
|
730
|
+
p.cancel("Operation cancelled.");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
defaultLocale = inputDefault.trim();
|
|
734
|
+
const inputLocales = await p.text({
|
|
735
|
+
message: "Comma-separated list of supported locales:",
|
|
736
|
+
initialValue: `${defaultLocale},tr`,
|
|
737
|
+
validate: (v) => !v || !v.trim() ? "Locales list cannot be empty" : void 0
|
|
738
|
+
});
|
|
739
|
+
if (p.isCancel(inputLocales)) {
|
|
740
|
+
p.cancel("Operation cancelled.");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
localesInput = inputLocales.trim();
|
|
744
|
+
if (options.ai === void 0 && options.noAi === void 0) {
|
|
745
|
+
const confirmAi = await p.confirm({
|
|
746
|
+
message: "Configure AI agent support (Claude/Cursor skills, .mcp.json, AGENTS.md)?",
|
|
747
|
+
initialValue: true
|
|
783
748
|
});
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
749
|
+
if (!p.isCancel(confirmAi)) options.ai = confirmAi;
|
|
750
|
+
}
|
|
751
|
+
} else logger.info(`Auto-detected framework: ${pc.bold(framework)} (${detected.matchedRule})`);
|
|
752
|
+
const localeList = Array.from(/* @__PURE__ */ new Set([defaultLocale, ...localesInput.split(",").map((s) => s.trim()).filter(Boolean)]));
|
|
753
|
+
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");
|
|
754
|
+
const configFileName = path.basename(configPath);
|
|
755
|
+
try {
|
|
756
|
+
if (fs.existsSync(configPath)) {
|
|
757
|
+
const mod = await loadFile(configPath);
|
|
758
|
+
if (mod && mod.exports.default) {
|
|
759
|
+
if (mod.exports.default.$args && mod.exports.default.$args[0]) {
|
|
760
|
+
mod.exports.default.$args[0].defaultLocale = defaultLocale;
|
|
761
|
+
mod.exports.default.$args[0].locales = localeList;
|
|
762
|
+
}
|
|
763
|
+
await writeFile(mod, configPath);
|
|
764
|
+
logger.success(`Updated ${pc.bold(configFileName)} via Magicast`);
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
const mod = parseModule(`import { defineConfig } from "dialexjs";
|
|
768
|
+
|
|
769
|
+
export default defineConfig({});
|
|
792
770
|
`);
|
|
771
|
+
mod.exports.default.$args[0].defaultLocale = defaultLocale;
|
|
772
|
+
mod.exports.default.$args[0].locales = localeList;
|
|
773
|
+
await writeFile(mod, configPath);
|
|
774
|
+
logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
|
|
793
775
|
}
|
|
776
|
+
} catch (err) {
|
|
777
|
+
logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
|
|
778
|
+
const fallback = `import { defineConfig } from "dialexjs";
|
|
779
|
+
|
|
780
|
+
export default defineConfig({
|
|
781
|
+
defaultLocale: "${defaultLocale}",
|
|
782
|
+
locales: [${localeList.map((l) => `"${l}"`).join(", ")}],
|
|
783
|
+
});
|
|
784
|
+
`;
|
|
785
|
+
fs.writeFileSync(configPath, fallback, "utf-8");
|
|
794
786
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
return result;
|
|
810
|
-
}
|
|
811
|
-
//#endregion
|
|
812
|
-
//#region src/commands/import.ts
|
|
813
|
-
const unescapeXml = (value) => value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
814
|
-
function parseCsv(text) {
|
|
815
|
-
const rows = [];
|
|
816
|
-
let row = [];
|
|
817
|
-
let cell = "";
|
|
818
|
-
let quoted = false;
|
|
819
|
-
for (let i = 0; i < text.length; i++) {
|
|
820
|
-
const ch = text[i];
|
|
821
|
-
if (quoted) {
|
|
822
|
-
if (ch === "\"" && text[i + 1] === "\"") {
|
|
823
|
-
cell += "\"";
|
|
824
|
-
i++;
|
|
825
|
-
} else if (ch === "\"") quoted = false;
|
|
826
|
-
else cell += ch;
|
|
827
|
-
} else if (ch === "\"") quoted = true;
|
|
828
|
-
else if (ch === ",") {
|
|
829
|
-
row.push(cell);
|
|
830
|
-
cell = "";
|
|
831
|
-
} else if (ch === "\n" || ch === "\r") {
|
|
832
|
-
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
833
|
-
row.push(cell);
|
|
834
|
-
rows.push(row);
|
|
835
|
-
row = [];
|
|
836
|
-
cell = "";
|
|
837
|
-
} else cell += ch;
|
|
787
|
+
if (framework === "nuxt") {
|
|
788
|
+
const nuxtConfig = [
|
|
789
|
+
"nuxt.config.ts",
|
|
790
|
+
"nuxt.config.js",
|
|
791
|
+
"nuxt.config.mjs"
|
|
792
|
+
].map((f) => path.join(root, f)).find((f) => fs.existsSync(f));
|
|
793
|
+
if (nuxtConfig) try {
|
|
794
|
+
const nMod = await loadFile(nuxtConfig);
|
|
795
|
+
addNuxtModule(nMod, "dialexjs/nuxt", "dialex", {});
|
|
796
|
+
await writeFile(nMod, nuxtConfig);
|
|
797
|
+
logger.success(`Registered dialexjs/nuxt in ${pc.bold(path.basename(nuxtConfig))} via Magicast`);
|
|
798
|
+
} catch {
|
|
799
|
+
logger.warn("Could not update the Nuxt config; add the dialexjs/nuxt module manually.");
|
|
800
|
+
}
|
|
838
801
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
802
|
+
const targetDir = fs.existsSync(path.join(root, "src")) ? path.join(root, "src") : root;
|
|
803
|
+
const dictPath = path.join(targetDir, "home.content.ts");
|
|
804
|
+
if (!fs.existsSync(dictPath)) {
|
|
805
|
+
const dictContent = `import { defineDictionary } from "dialexjs";
|
|
806
|
+
|
|
807
|
+
export default defineDictionary("home", {
|
|
808
|
+
${localeList.map((loc) => ` ${loc}: {
|
|
809
|
+
title: "Welcome to Dialex (${loc})!",
|
|
810
|
+
description: "Type-safe internationalization.",
|
|
811
|
+
greeting: (name: string) => "Hello, " + name + "!",
|
|
812
|
+
},`).join("\n")}
|
|
813
|
+
});
|
|
814
|
+
`;
|
|
815
|
+
fs.writeFileSync(dictPath, dictContent, "utf-8");
|
|
816
|
+
logger.success(`Created starter dictionary at ${pc.bold(path.relative(root, dictPath))}`);
|
|
817
|
+
} else logger.info("home.content.ts already exists, skipping creation.");
|
|
818
|
+
generateDictionaries(root);
|
|
819
|
+
logger.success("Generated types and standalone dictionary registry!");
|
|
820
|
+
const pkgPath = path.join(root, "package.json");
|
|
821
|
+
if (fs.existsSync(pkgPath)) try {
|
|
822
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
823
|
+
const added = addRequiredPackages(pkg);
|
|
824
|
+
pkg.scripts = pkg.scripts || {};
|
|
825
|
+
const addScript = !pkg.scripts["dx:generate"];
|
|
826
|
+
if (addScript) pkg.scripts["dx:generate"] = "dx generate";
|
|
827
|
+
if (added.length > 0 || addScript) fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
828
|
+
if (added.length > 0) {
|
|
829
|
+
logger.success(`Added ${added.join(" and ")} to package.json`);
|
|
830
|
+
logger.info(`Run ${pc.bold(installCommand(root))} to install them.`);
|
|
831
|
+
}
|
|
832
|
+
if (addScript) logger.success("Added \"dx:generate\" script to package.json");
|
|
833
|
+
} catch {
|
|
834
|
+
logger.warn("Could not update package.json; add dialexjs and @dialexjs/cli manually.");
|
|
842
835
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
836
|
+
else logger.warn("No package.json found; install dialexjs and @dialexjs/cli in your project.");
|
|
837
|
+
if (options.ai === true || options.yes && options.ai !== false && options.noAi !== true) try {
|
|
838
|
+
const skillDir = path.join(root, ".agents/skills/dialex");
|
|
839
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
840
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
841
|
+
name: dialex
|
|
842
|
+
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.
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
# Dialex Internationalization
|
|
846
|
+
|
|
847
|
+
Dialex is a high-performance, type-safe internationalization toolchain for JavaScript and TypeScript.
|
|
848
|
+
|
|
849
|
+
## Core Concepts
|
|
850
|
+
- Dictionaries are defined with \`defineDictionary("name", { ... })\` in \`*.content.ts\` files.
|
|
851
|
+
- Keys must match across all locales.
|
|
852
|
+
- ICU message syntax is supported for plurals and selects.
|
|
853
|
+
- Parity is validated at compile time with \`dialex check\`.
|
|
854
|
+
|
|
855
|
+
## Useful Commands
|
|
856
|
+
- \`dialex check --json\`: Check dictionary parity and references.
|
|
857
|
+
- \`dialex check --fix\`: Automatically insert missing keys marked with [TODO].
|
|
858
|
+
- \`dialex generate\`: Rebuild standalone dictionary bundle and types.
|
|
859
|
+
- \`dialex translate --locale <locale>\`: Automatically translate missing keys.
|
|
860
|
+
`, "utf-8");
|
|
861
|
+
const mcpConfigPath = path.join(root, ".mcp.json");
|
|
862
|
+
let mcpConfig = {};
|
|
863
|
+
if (fs.existsSync(mcpConfigPath)) try {
|
|
864
|
+
mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
865
|
+
} catch {
|
|
866
|
+
mcpConfig = {};
|
|
862
867
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
});
|
|
868
|
+
mcpConfig.mcpServers = mcpConfig.mcpServers || {};
|
|
869
|
+
mcpConfig.mcpServers.dialex = mcpConfig.mcpServers.dialex || {
|
|
870
|
+
command: "npx",
|
|
871
|
+
args: ["@dialexjs/mcp"]
|
|
872
|
+
};
|
|
873
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
|
|
874
|
+
const agentsMdPath = path.join(root, "AGENTS.md");
|
|
875
|
+
const dialexAgentSnippet = `
|
|
876
|
+
## Internationalization (Dialex)
|
|
877
|
+
|
|
878
|
+
This project uses [Dialex](https://github.com/awaiden/dialex) for type-safe internationalization.
|
|
879
|
+
- Dictionaries are in \`*.content.ts\` files using \`defineDictionary\`.
|
|
880
|
+
- Run \`dialex check --json\` to verify parity.
|
|
881
|
+
- Run \`dialex check --fix\` to insert missing keys marked with \`[TODO]\`.
|
|
882
|
+
- Run \`dx generate\` (alias of \`dialex\`) or \`npm run dx:generate\` to compile dictionary bundles and update types.
|
|
883
|
+
`;
|
|
884
|
+
if (!fs.existsSync(agentsMdPath)) fs.writeFileSync(agentsMdPath, dialexAgentSnippet.trimStart(), "utf-8");
|
|
885
|
+
else {
|
|
886
|
+
const existing = fs.readFileSync(agentsMdPath, "utf-8");
|
|
887
|
+
if (!existing.includes("Dialex")) fs.writeFileSync(agentsMdPath, existing.trimEnd() + "\n" + dialexAgentSnippet, "utf-8");
|
|
884
888
|
}
|
|
889
|
+
logger.success("Configured AI assistant support (.agents/skills, .mcp.json, AGENTS.md)");
|
|
890
|
+
} catch (err) {
|
|
891
|
+
logger.warn(`Could not finish AI configuration: ${err.message || String(err)}`);
|
|
885
892
|
}
|
|
886
|
-
|
|
893
|
+
logger.log("");
|
|
894
|
+
logger.info(pc.bold(`Quick start for ${framework.toUpperCase()}:`));
|
|
895
|
+
switch (framework) {
|
|
896
|
+
case "hono":
|
|
897
|
+
logger.log(`
|
|
898
|
+
import { Hono } from "hono";
|
|
899
|
+
import { dialex } from "dialexjs/hono";
|
|
900
|
+
import dictionaries from "./src/dialex.generated.js";
|
|
901
|
+
|
|
902
|
+
const app = new Hono();
|
|
903
|
+
app.use("*", dialex({ dictionaries }));
|
|
904
|
+
|
|
905
|
+
app.get("/", (c) => {
|
|
906
|
+
const dict = c.var.getDictionary("home");
|
|
907
|
+
return c.json({ title: dict.title });
|
|
908
|
+
});
|
|
909
|
+
`);
|
|
910
|
+
break;
|
|
911
|
+
case "fastify":
|
|
912
|
+
logger.log(`
|
|
913
|
+
import Fastify from "fastify";
|
|
914
|
+
import { dialexPlugin } from "dialexjs/fastify";
|
|
915
|
+
import dictionaries from "./src/dialex.generated.js";
|
|
916
|
+
|
|
917
|
+
const app = Fastify();
|
|
918
|
+
await app.register(dialexPlugin, { dictionaries });
|
|
919
|
+
|
|
920
|
+
app.get("/", (req) => {
|
|
921
|
+
const dict = req.getDictionary("home");
|
|
922
|
+
return { title: dict.title };
|
|
923
|
+
});
|
|
924
|
+
`);
|
|
925
|
+
break;
|
|
926
|
+
case "express":
|
|
927
|
+
logger.log(`
|
|
928
|
+
import express from "express";
|
|
929
|
+
import { dialex } from "dialexjs/express";
|
|
930
|
+
import dictionaries from "./src/dialex.generated.js";
|
|
931
|
+
|
|
932
|
+
const app = express();
|
|
933
|
+
app.use(dialex({ dictionaries }));
|
|
934
|
+
|
|
935
|
+
app.get("/", (req, res) => {
|
|
936
|
+
const dict = req.getDictionary("home");
|
|
937
|
+
res.json({ title: dict.title });
|
|
938
|
+
});
|
|
939
|
+
`);
|
|
940
|
+
break;
|
|
941
|
+
case "koa":
|
|
942
|
+
logger.log(`
|
|
943
|
+
import Koa from "koa";
|
|
944
|
+
import { dialex } from "dialexjs/koa";
|
|
945
|
+
import dictionaries from "./src/dialex.generated.js";
|
|
946
|
+
|
|
947
|
+
const app = new Koa();
|
|
948
|
+
app.use(dialex({ dictionaries }));
|
|
949
|
+
|
|
950
|
+
app.use((ctx) => {
|
|
951
|
+
const dict = ctx.getDictionary("home");
|
|
952
|
+
ctx.body = { title: dict.title };
|
|
953
|
+
});
|
|
954
|
+
`);
|
|
955
|
+
break;
|
|
956
|
+
case "nestjs":
|
|
957
|
+
logger.log(`
|
|
958
|
+
import { Module } from "@nestjs/common";
|
|
959
|
+
import { DialexModule } from "dialexjs/nestjs";
|
|
960
|
+
import dictionaries from "./src/dialex.generated.js";
|
|
961
|
+
|
|
962
|
+
@Module({
|
|
963
|
+
imports: [DialexModule.forRoot({ dictionaries })],
|
|
964
|
+
})
|
|
965
|
+
export class AppModule {}
|
|
966
|
+
`);
|
|
967
|
+
break;
|
|
968
|
+
case "next":
|
|
969
|
+
logger.log(`
|
|
970
|
+
// src/dialex.ts
|
|
971
|
+
import { createDialexServer } from "dialexjs/server";
|
|
972
|
+
import { dialex } from "./dialex.generated";
|
|
973
|
+
|
|
974
|
+
export const { getDictionary, getT } = createDialexServer(dialex);
|
|
975
|
+
|
|
976
|
+
// app/[locale]/page.tsx (Server Component)
|
|
977
|
+
import { getDictionary } from "../../dialex";
|
|
978
|
+
|
|
979
|
+
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
|
|
980
|
+
const { locale = "en" } = await params;
|
|
981
|
+
const dict = getDictionary("home", locale);
|
|
982
|
+
return <h1>{dict.title}</h1>;
|
|
887
983
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
984
|
+
|
|
985
|
+
// For client components, render the provider inside a "use client" file:
|
|
986
|
+
// "use client";
|
|
987
|
+
// import { DialexProvider } from "dialexjs/react";
|
|
988
|
+
// import { dialex } from "./dialex.generated";
|
|
989
|
+
// export const Providers = ({ children }) => <DialexProvider {...dialex}>{children}</DialexProvider>;
|
|
990
|
+
`);
|
|
991
|
+
break;
|
|
992
|
+
case "react":
|
|
993
|
+
logger.log(`
|
|
994
|
+
// src/main.tsx
|
|
995
|
+
import { createRoot } from "react-dom/client";
|
|
996
|
+
import { DialexProvider } from "dialexjs/react";
|
|
997
|
+
import { dialex } from "./dialex.generated";
|
|
998
|
+
import App from "./App";
|
|
999
|
+
|
|
1000
|
+
createRoot(document.getElementById("root")!).render(
|
|
1001
|
+
<DialexProvider {...dialex}>
|
|
1002
|
+
<App />
|
|
1003
|
+
</DialexProvider>,
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
// In components: const dict = useDictionary("home")
|
|
1007
|
+
`);
|
|
1008
|
+
break;
|
|
1009
|
+
case "elysia":
|
|
1010
|
+
logger.log(`
|
|
1011
|
+
import { Elysia } from "elysia";
|
|
1012
|
+
import { dialex } from "dialexjs/elysia";
|
|
1013
|
+
import dictionaries from "./src/dialex.generated.js";
|
|
1014
|
+
|
|
1015
|
+
new Elysia()
|
|
1016
|
+
.use(dialex({ dictionaries }))
|
|
1017
|
+
.get("/", ({ getDictionary }) => ({ title: getDictionary("home").title }))
|
|
1018
|
+
.listen(3000);
|
|
1019
|
+
`);
|
|
1020
|
+
break;
|
|
1021
|
+
case "sveltekit":
|
|
1022
|
+
logger.log(`
|
|
1023
|
+
// src/hooks.server.ts
|
|
1024
|
+
import { dialexHandle } from "dialexjs/sveltekit";
|
|
1025
|
+
import dictionaries from "./dialex.generated.js";
|
|
1026
|
+
|
|
1027
|
+
export const handle = dialexHandle({ dictionaries });
|
|
1028
|
+
|
|
1029
|
+
// src/app.html: <html lang="%dialex.lang%">
|
|
1030
|
+
// In load functions: locals.getDictionary("home")
|
|
1031
|
+
`);
|
|
1032
|
+
break;
|
|
1033
|
+
case "astro":
|
|
1034
|
+
logger.log(`
|
|
1035
|
+
// src/middleware.ts
|
|
1036
|
+
import { dialex } from "dialexjs/astro";
|
|
1037
|
+
import dictionaries from "./dialex.generated.js";
|
|
1038
|
+
|
|
1039
|
+
export const onRequest = dialex({ dictionaries });
|
|
1040
|
+
|
|
1041
|
+
// In pages: Astro.locals.getDictionary("home")
|
|
1042
|
+
`);
|
|
1043
|
+
break;
|
|
1044
|
+
case "vue":
|
|
1045
|
+
logger.log(`
|
|
1046
|
+
// src/main.ts
|
|
1047
|
+
import { createApp } from "vue";
|
|
1048
|
+
import { createDialex } from "dialexjs/vue";
|
|
1049
|
+
import { dialex } from "./dialex.generated";
|
|
1050
|
+
import App from "./App.vue";
|
|
1051
|
+
|
|
1052
|
+
createApp(App).use(createDialex({ ...dialex })).mount("#app");
|
|
1053
|
+
|
|
1054
|
+
// In components: const dict = useDictionary("home")
|
|
1055
|
+
`);
|
|
1056
|
+
break;
|
|
1057
|
+
case "nuxt":
|
|
1058
|
+
logger.log(`
|
|
1059
|
+
// nuxt.config.ts
|
|
1060
|
+
export default defineNuxtConfig({
|
|
1061
|
+
modules: ["dialexjs/nuxt"],
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// In components: const dict = useDictionary("home")
|
|
1065
|
+
`);
|
|
1066
|
+
break;
|
|
1067
|
+
default: logger.log(`
|
|
1068
|
+
Import dictionaries and register your framework middleware to begin!
|
|
1069
|
+
Run \`npm run dx:generate\` whenever you add new dictionary files.
|
|
1070
|
+
`);
|
|
1071
|
+
}
|
|
1072
|
+
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.");
|
|
1073
|
+
if (isInteractive) p.outro(pc.green("✔ dialex is configured and ready to go!"));
|
|
894
1074
|
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/commands/lock.ts
|
|
895
1077
|
/**
|
|
896
|
-
*
|
|
897
|
-
*
|
|
1078
|
+
* Records the current translations as up to date with the current source text, so later edits to
|
|
1079
|
+
* the source can be noticed. Translations that are already out of date are left alone unless
|
|
1080
|
+
* `--all` is given. Run it once when you adopt the lock file, and after reviewing stale ones.
|
|
898
1081
|
*/
|
|
899
|
-
function
|
|
900
|
-
if (!value.includes("{")) return void 0;
|
|
901
|
-
let signature;
|
|
902
|
-
try {
|
|
903
|
-
signature = argumentSignature(parseMessage(value));
|
|
904
|
-
} catch (error) {
|
|
905
|
-
return `invalid ICU: ${error.message}`;
|
|
906
|
-
}
|
|
907
|
-
const locales = listLocales(df);
|
|
908
|
-
for (const other of [defaultLocale, ...locales.filter((l) => l !== defaultLocale)]) {
|
|
909
|
-
if (other === locale) continue;
|
|
910
|
-
const reference = getString(df, other, keyPath);
|
|
911
|
-
if (!reference?.includes("{")) continue;
|
|
912
|
-
try {
|
|
913
|
-
const expected = argumentSignature(parseMessage(reference));
|
|
914
|
-
return expected === signature ? void 0 : `arguments differ from "${other}": expected ${expected || "none"}, found ${signature || "none"}`;
|
|
915
|
-
} catch {
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
async function runImport(options) {
|
|
1082
|
+
async function runLock(options = {}) {
|
|
921
1083
|
const root = options.cwd || process.cwd();
|
|
922
|
-
const file = path.resolve(root, options.file);
|
|
923
|
-
const format = options.format || inferFormat(file);
|
|
924
|
-
const fallbackLocale = options.locale ?? path.basename(file, path.extname(file));
|
|
925
|
-
const entries = parseEntries(fs.readFileSync(file, "utf-8"), format, options.locale ?? fallbackLocale);
|
|
926
1084
|
const project = await loadProject(root, options.config);
|
|
927
|
-
const
|
|
928
|
-
const
|
|
1085
|
+
const source = options.source ?? project.config.translate?.sourceLocale ?? project.config.defaultLocale ?? "en";
|
|
1086
|
+
const { lock } = lockFor(root, source);
|
|
929
1087
|
const result = {
|
|
930
1088
|
success: true,
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
rejected: [],
|
|
935
|
-
unknown: [],
|
|
936
|
-
errors: []
|
|
1089
|
+
sourceLocale: source,
|
|
1090
|
+
recorded: 0,
|
|
1091
|
+
stale: []
|
|
937
1092
|
};
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
result[outcome]++;
|
|
959
|
-
touched.add(dict.rel);
|
|
1093
|
+
for (const dict of project.dictionaries) {
|
|
1094
|
+
const sourceText = new Map(listLeaves(dict.df, source).filter((leaf) => leaf.kind === "string" && leaf.value !== void 0).map((leaf) => [leaf.path.join("."), leaf.value]));
|
|
1095
|
+
for (const locale of listLocales(dict.df)) {
|
|
1096
|
+
if (locale === source) continue;
|
|
1097
|
+
for (const leaf of listLeaves(dict.df, locale)) {
|
|
1098
|
+
if (leaf.kind !== "string" || leaf.value === void 0) continue;
|
|
1099
|
+
if (leaf.value.startsWith("[TODO] ")) continue;
|
|
1100
|
+
const text = sourceText.get(leaf.path.join("."));
|
|
1101
|
+
if (text === void 0) continue;
|
|
1102
|
+
const key = toKey(dict.name, leaf.path);
|
|
1103
|
+
const known = lock.locales[locale]?.[key] !== void 0;
|
|
1104
|
+
if (known && isStale(lock, locale, key, text)) {
|
|
1105
|
+
if (!options.all) {
|
|
1106
|
+
result.stale.push(`${locale}:${key}`);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
} else if (known) continue;
|
|
1110
|
+
record(lock, locale, key, text);
|
|
1111
|
+
result.recorded++;
|
|
1112
|
+
}
|
|
960
1113
|
}
|
|
961
1114
|
}
|
|
962
|
-
|
|
963
|
-
if (
|
|
964
|
-
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
1115
|
+
if (result.recorded > 0 || !fs.existsSync(lockPath(root))) writeLock(root, lock);
|
|
1116
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
965
1117
|
else {
|
|
966
|
-
logger.success(`
|
|
967
|
-
if (result.
|
|
968
|
-
|
|
969
|
-
|
|
1118
|
+
logger.success(`Recorded ${result.recorded} translation${result.recorded === 1 ? "" : "s"} in ${path.basename(LOCK_FILE)} (source "${source}")`);
|
|
1119
|
+
if (result.stale.length > 0) {
|
|
1120
|
+
logger.warn(`${result.stale.length} out of date and left as they are: ${result.stale.slice(0, 5).join(", ")}${result.stale.length > 5 ? ", ..." : ""}`);
|
|
1121
|
+
logger.log(` ${pc.dim("Re-translate them with")} dialex translate --stale${pc.dim(", or accept them with")} dialex lock --all`);
|
|
1122
|
+
}
|
|
970
1123
|
}
|
|
971
1124
|
return result;
|
|
972
1125
|
}
|
|
@@ -1019,6 +1172,8 @@ async function runTranslate(options = {}) {
|
|
|
1019
1172
|
const everyLocale = new Set(project.config.locales ?? []);
|
|
1020
1173
|
for (const { df } of project.dictionaries) listLocales(df).forEach((l) => everyLocale.add(l));
|
|
1021
1174
|
const targets = (options.locale?.length ? options.locale : [...everyLocale]).filter((l) => l !== source);
|
|
1175
|
+
const { lock, replaced } = lockFor(root, source);
|
|
1176
|
+
if (replaced && !options.json) logger.warn(`dialex.lock.json was recorded from another source locale; starting a new one from "${source}".`);
|
|
1022
1177
|
const tasks = [];
|
|
1023
1178
|
for (const dict of project.dictionaries) {
|
|
1024
1179
|
const sourceLeaves = listLeaves(dict.df, source).filter((leaf) => leaf.kind === "string" && leaf.value !== void 0);
|
|
@@ -1026,13 +1181,17 @@ async function runTranslate(options = {}) {
|
|
|
1026
1181
|
const existing = new Map(listLeaves(dict.df, locale).map((leaf) => [leaf.path.join("\0"), leaf]));
|
|
1027
1182
|
for (const leaf of sourceLeaves) {
|
|
1028
1183
|
const current = existing.get(leaf.path.join("\0"));
|
|
1029
|
-
|
|
1184
|
+
const key = toKey(dict.name, leaf.path);
|
|
1185
|
+
const placeholder = !current || current.kind === "string" && current.value?.startsWith("[TODO] ");
|
|
1186
|
+
const stale = !placeholder && options.stale === true && current?.kind === "string" && isStale(lock, locale, key, leaf.value);
|
|
1187
|
+
if (placeholder || stale) tasks.push({
|
|
1030
1188
|
dict,
|
|
1031
1189
|
path: leaf.path,
|
|
1032
1190
|
file: dict.rel,
|
|
1033
1191
|
locale,
|
|
1034
|
-
key
|
|
1035
|
-
source: leaf.value
|
|
1192
|
+
key,
|
|
1193
|
+
source: leaf.value,
|
|
1194
|
+
...stale ? { stale: true } : {}
|
|
1036
1195
|
});
|
|
1037
1196
|
}
|
|
1038
1197
|
}
|
|
@@ -1054,7 +1213,7 @@ async function runTranslate(options = {}) {
|
|
|
1054
1213
|
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
1055
1214
|
else {
|
|
1056
1215
|
logger.info(`Would translate ${tasks.length} string${tasks.length === 1 ? "" : "s"} from "${source}":`);
|
|
1057
|
-
for (const t of tasks) logger.log(` ${pc.dim(t.locale)} ${t.key} ${pc.dim(JSON.stringify(t.source))}`);
|
|
1216
|
+
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)") : ""}`);
|
|
1058
1217
|
}
|
|
1059
1218
|
return result;
|
|
1060
1219
|
}
|
|
@@ -1088,11 +1247,13 @@ async function runTranslate(options = {}) {
|
|
|
1088
1247
|
continue;
|
|
1089
1248
|
}
|
|
1090
1249
|
task.translated = translated;
|
|
1250
|
+
record(lock, locale, task.key, task.source);
|
|
1091
1251
|
result.translated++;
|
|
1092
1252
|
touched.add(task.dict);
|
|
1093
1253
|
}
|
|
1094
1254
|
}
|
|
1095
1255
|
for (const dict of touched) await saveDictionaryFile(dict.df);
|
|
1256
|
+
if (result.translated > 0) writeLock(root, lock);
|
|
1096
1257
|
if (result.rejected.length > 0) result.success = false;
|
|
1097
1258
|
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
1098
1259
|
else {
|
|
@@ -1104,14 +1265,14 @@ async function runTranslate(options = {}) {
|
|
|
1104
1265
|
//#endregion
|
|
1105
1266
|
//#region src/index.ts
|
|
1106
1267
|
const program = new Command();
|
|
1107
|
-
program.name("dialex").description("Dialex CLI - High-performance, type-safe internationalization compiler and tooling").version(
|
|
1268
|
+
program.name("dialex").description("Dialex CLI - High-performance, type-safe internationalization compiler and tooling").version(version);
|
|
1108
1269
|
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) => {
|
|
1109
1270
|
await runGenerate(opts);
|
|
1110
1271
|
});
|
|
1111
1272
|
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) => {
|
|
1112
1273
|
await runInit(opts);
|
|
1113
1274
|
});
|
|
1114
|
-
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) => {
|
|
1275
|
+
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) => {
|
|
1115
1276
|
await runCheck(opts);
|
|
1116
1277
|
});
|
|
1117
1278
|
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", "dialex-export").option("-l, --locale <locales...>", "Limit to these locales").option("-c, --config <path>", "Custom config path").option("--json", "Print machine-readable JSON").action(async (opts) => {
|
|
@@ -1123,7 +1284,7 @@ program.command("import <file>").description("Write translated strings from a js
|
|
|
1123
1284
|
...opts
|
|
1124
1285
|
});
|
|
1125
1286
|
});
|
|
1126
|
-
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").option("--json", "Print machine-readable JSON").action(async (opts) => {
|
|
1287
|
+
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("--stale", "Also re-translate strings whose source text changed since they were translated").option("-c, --config <path>", "Custom config path").option("--json", "Print machine-readable JSON").action(async (opts) => {
|
|
1127
1288
|
try {
|
|
1128
1289
|
await runTranslate(opts);
|
|
1129
1290
|
} catch (err) {
|
|
@@ -1135,6 +1296,14 @@ program.command("translate").description("Fill missing translations with the pro
|
|
|
1135
1296
|
process.exit(1);
|
|
1136
1297
|
}
|
|
1137
1298
|
});
|
|
1299
|
+
program.command("lock").description("Record translations as up to date with the current source text (dialex.lock.json)").option("-s, --source <locale>", "Locale the translations were made from").option("--all", "Also accept translations that are already out of date").option("--json", "Print machine-readable JSON").option("-c, --config <path>", "Custom config path").action(async (opts) => {
|
|
1300
|
+
try {
|
|
1301
|
+
await runLock(opts);
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
logger.error(err.message || String(err));
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1138
1307
|
program.parse();
|
|
1139
1308
|
//#endregion
|
|
1140
1309
|
export {};
|