@dialexjs/cli 0.2.3 → 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 +71 -5
- package/dist/api.d.mts.map +1 -1
- package/dist/api.mjs +2 -2
- package/dist/index.mjs +1084 -856
- package/dist/index.mjs.map +1 -1
- package/dist/{project-BnG6V2I0.mjs → project-BJYmnKiU.mjs} +597 -124
- 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-BnG6V2I0.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,913 +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 { resolveDialexConfig } from "dialexjs/scanner";
|
|
6
7
|
import fg from "fast-glob";
|
|
7
|
-
import { resolveI18nConfig } from "dialexjs/scanner";
|
|
8
8
|
import pc from "picocolors";
|
|
9
|
-
import
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { loadFile, parseModule, writeFile } from "magicast";
|
|
11
|
-
import { addNuxtModule, addVitePlugin } from "magicast/helpers";
|
|
12
11
|
import { argumentSignature, getArguments, isIcuStructured, parseMessage } from "dialexjs/icu";
|
|
13
|
-
|
|
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
|
+
}
|
|
14
33
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
17
36
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
if ("elysia" in allDeps) return {
|
|
67
|
-
framework: "elysia",
|
|
68
|
-
confidence: "high",
|
|
69
|
-
matchedRule: "package.json dependencies (elysia)"
|
|
70
|
-
};
|
|
71
|
-
if ("hono" in allDeps) return {
|
|
72
|
-
framework: "hono",
|
|
73
|
-
confidence: "high",
|
|
74
|
-
matchedRule: "package.json dependencies (hono)"
|
|
75
|
-
};
|
|
76
|
-
if ("vue" in allDeps && !("react" in allDeps)) return {
|
|
77
|
-
framework: "vue",
|
|
78
|
-
confidence: "high",
|
|
79
|
-
matchedRule: "package.json dependencies (vue)"
|
|
80
|
-
};
|
|
81
|
-
if ("react" in allDeps || "vite" in allDeps) return {
|
|
82
|
-
framework: "react",
|
|
83
|
-
confidence: "high",
|
|
84
|
-
matchedRule: "package.json dependencies (react/vite)"
|
|
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);
|
|
57
|
+
}
|
|
58
|
+
return fixed;
|
|
59
|
+
}
|
|
60
|
+
function escapeAnnotation(value) {
|
|
61
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
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.
|
|
67
|
+
*/
|
|
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
|
|
85
84
|
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
85
|
+
}
|
|
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`)."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
110
114
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
matchedRule: "default fallback"
|
|
115
|
+
stale,
|
|
116
|
+
rewritten
|
|
114
117
|
};
|
|
115
118
|
}
|
|
116
|
-
|
|
117
|
-
//#region src/commands/init.ts
|
|
118
|
-
async function runInit(options = {}) {
|
|
119
|
+
async function runCheck(options = {}) {
|
|
119
120
|
const root = options.cwd || process.cwd();
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
label: "Express",
|
|
136
|
-
hint: detected.framework === "express" ? "detected" : void 0
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
value: "fastify",
|
|
140
|
-
label: "Fastify",
|
|
141
|
-
hint: detected.framework === "fastify" ? "detected" : void 0
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
value: "koa",
|
|
145
|
-
label: "Koa",
|
|
146
|
-
hint: detected.framework === "koa" ? "detected" : void 0
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
value: "nestjs",
|
|
150
|
-
label: "NestJS",
|
|
151
|
-
hint: detected.framework === "nestjs" ? "detected" : void 0
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
value: "next",
|
|
155
|
-
label: "Next.js",
|
|
156
|
-
hint: detected.framework === "next" ? "detected" : void 0
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
value: "react",
|
|
160
|
-
label: "React / Vite",
|
|
161
|
-
hint: detected.framework === "react" ? "detected" : void 0
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
value: "elysia",
|
|
165
|
-
label: "Elysia",
|
|
166
|
-
hint: detected.framework === "elysia" ? "detected" : void 0
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
value: "sveltekit",
|
|
170
|
-
label: "SvelteKit",
|
|
171
|
-
hint: detected.framework === "sveltekit" ? "detected" : void 0
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
value: "astro",
|
|
175
|
-
label: "Astro",
|
|
176
|
-
hint: detected.framework === "astro" ? "detected" : void 0
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
value: "vue",
|
|
180
|
-
label: "Vue / Vite",
|
|
181
|
-
hint: detected.framework === "vue" ? "detected" : void 0
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
value: "nuxt",
|
|
185
|
-
label: "Nuxt",
|
|
186
|
-
hint: detected.framework === "nuxt" ? "detected" : void 0
|
|
187
|
-
}
|
|
188
|
-
];
|
|
189
|
-
const selectedFramework = await p.select({
|
|
190
|
-
message: `Framework detected as ${pc.bold(detected.framework)} (${detected.matchedRule}). Select framework:`,
|
|
191
|
-
options: frameworkOptions,
|
|
192
|
-
initialValue: detected.framework
|
|
193
|
-
});
|
|
194
|
-
if (p.isCancel(selectedFramework)) {
|
|
195
|
-
p.cancel("Operation cancelled.");
|
|
196
|
-
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}".`);
|
|
197
136
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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);
|
|
217
176
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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);
|
|
225
190
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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}`);
|
|
240
223
|
}
|
|
241
|
-
|
|
242
|
-
const mod = parseModule(`import { defineConfig } from "dialexjs";
|
|
243
|
-
|
|
244
|
-
export default defineConfig({});
|
|
245
|
-
`);
|
|
246
|
-
mod.exports.default.$args[0].defaultLocale = defaultLocale;
|
|
247
|
-
mod.exports.default.$args[0].locales = localeList;
|
|
248
|
-
await writeFile(mod, configPath);
|
|
249
|
-
logger.success(`Created ${pc.bold(configFileName)} via Magicast`);
|
|
224
|
+
logger.log("");
|
|
250
225
|
}
|
|
251
|
-
} catch (err) {
|
|
252
|
-
logger.warn(`Magicast config update notice: ${err.message || String(err)}`);
|
|
253
|
-
const fallback = `import { defineConfig } from "dialexjs";
|
|
254
|
-
|
|
255
|
-
export default defineConfig({
|
|
256
|
-
defaultLocale: "${defaultLocale}",
|
|
257
|
-
locales: [${localeList.map((l) => `"${l}"`).join(", ")}],
|
|
258
|
-
});
|
|
259
|
-
`;
|
|
260
|
-
fs.writeFileSync(configPath, fallback, "utf-8");
|
|
261
226
|
}
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
path.join(root, "vite.config.js"),
|
|
266
|
-
path.join(root, "vite.config.mjs")
|
|
267
|
-
];
|
|
268
|
-
for (const vConfig of possibleViteConfigs) if (fs.existsSync(vConfig)) try {
|
|
269
|
-
const vMod = await loadFile(vConfig);
|
|
270
|
-
addVitePlugin(vMod, {
|
|
271
|
-
from: "dialexjs/vite",
|
|
272
|
-
imported: "i18nPlugin",
|
|
273
|
-
constructor: "i18nPlugin"
|
|
274
|
-
});
|
|
275
|
-
await writeFile(vMod, vConfig);
|
|
276
|
-
logger.success(`Injected i18nPlugin() into ${pc.bold(path.basename(vConfig))} via Magicast`);
|
|
277
|
-
break;
|
|
278
|
-
} catch {}
|
|
227
|
+
if (totalIssues > 0) {
|
|
228
|
+
if (options.exitOnError !== false) process.exit(1);
|
|
229
|
+
return result;
|
|
279
230
|
}
|
|
280
|
-
if (
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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>`;
|
|
291
286
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
`);
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
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`);
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
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;
|
|
341
|
+
}
|
|
342
|
+
if (cell !== "" || row.length > 0) {
|
|
343
|
+
row.push(cell);
|
|
344
|
+
rows.push(row);
|
|
345
|
+
}
|
|
346
|
+
return rows.filter((r) => r.some((c) => c !== ""));
|
|
347
|
+
}
|
|
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.`);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
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.
|
|
401
|
+
*/
|
|
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"}`;
|
|
354
418
|
} catch {
|
|
355
|
-
|
|
419
|
+
continue;
|
|
356
420
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function runImport(options) {
|
|
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]));
|
|
431
|
+
const quiet = options.json === true;
|
|
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;
|
|
452
|
+
}
|
|
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}`);
|
|
374
464
|
else {
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
377
472
|
}
|
|
378
|
-
logger.success("Configured AI assistant support (.agents/skills, .mcp.json, AGENTS.md)");
|
|
379
|
-
} catch (err) {
|
|
380
|
-
logger.warn(`Could not finish AI configuration: ${err.message || String(err)}`);
|
|
381
473
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const app = new Hono();
|
|
392
|
-
app.use("*", i18n({ dictionaries }));
|
|
393
|
-
|
|
394
|
-
app.get("/", (c) => {
|
|
395
|
-
const dict = c.var.getDictionary("home");
|
|
396
|
-
return c.json({ title: dict.title });
|
|
397
|
-
});
|
|
398
|
-
`);
|
|
399
|
-
break;
|
|
400
|
-
case "fastify":
|
|
401
|
-
logger.log(`
|
|
402
|
-
import Fastify from "fastify";
|
|
403
|
-
import { i18nPlugin } from "dialexjs/fastify";
|
|
404
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
405
|
-
|
|
406
|
-
const app = Fastify();
|
|
407
|
-
await app.register(i18nPlugin, { dictionaries });
|
|
408
|
-
|
|
409
|
-
app.get("/", (req) => {
|
|
410
|
-
const dict = req.getDictionary("home");
|
|
411
|
-
return { title: dict.title };
|
|
412
|
-
});
|
|
413
|
-
`);
|
|
414
|
-
break;
|
|
415
|
-
case "express":
|
|
416
|
-
logger.log(`
|
|
417
|
-
import express from "express";
|
|
418
|
-
import { i18n } from "dialexjs/express";
|
|
419
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
420
|
-
|
|
421
|
-
const app = express();
|
|
422
|
-
app.use(i18n({ dictionaries }));
|
|
423
|
-
|
|
424
|
-
app.get("/", (req, res) => {
|
|
425
|
-
const dict = req.getDictionary("home");
|
|
426
|
-
res.json({ title: dict.title });
|
|
427
|
-
});
|
|
428
|
-
`);
|
|
429
|
-
break;
|
|
430
|
-
case "koa":
|
|
431
|
-
logger.log(`
|
|
432
|
-
import Koa from "koa";
|
|
433
|
-
import { i18n } from "dialexjs/koa";
|
|
434
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
435
|
-
|
|
436
|
-
const app = new Koa();
|
|
437
|
-
app.use(i18n({ dictionaries }));
|
|
438
|
-
|
|
439
|
-
app.use((ctx) => {
|
|
440
|
-
const dict = ctx.getDictionary("home");
|
|
441
|
-
ctx.body = { title: dict.title };
|
|
442
|
-
});
|
|
443
|
-
`);
|
|
444
|
-
break;
|
|
445
|
-
case "nestjs":
|
|
446
|
-
logger.log(`
|
|
447
|
-
import { Module } from "@nestjs/common";
|
|
448
|
-
import { I18nModule } from "dialexjs/nestjs";
|
|
449
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
450
|
-
|
|
451
|
-
@Module({
|
|
452
|
-
imports: [I18nModule.forRoot({ dictionaries })],
|
|
453
|
-
})
|
|
454
|
-
export class AppModule {}
|
|
455
|
-
`);
|
|
456
|
-
break;
|
|
457
|
-
case "next":
|
|
458
|
-
logger.log(`
|
|
459
|
-
import { getDictionary } from "dialexjs/server";
|
|
460
|
-
|
|
461
|
-
export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
|
|
462
|
-
const { locale = "en" } = await params;
|
|
463
|
-
const dict = getDictionary("home", locale);
|
|
464
|
-
return <h1>{dict.title}</h1>;
|
|
465
|
-
}
|
|
466
|
-
`);
|
|
467
|
-
break;
|
|
468
|
-
case "elysia":
|
|
469
|
-
logger.log(`
|
|
470
|
-
import { Elysia } from "elysia";
|
|
471
|
-
import { i18n } from "dialexjs/elysia";
|
|
472
|
-
import dictionaries from "./src/dialex.generated.js";
|
|
473
|
-
|
|
474
|
-
new Elysia()
|
|
475
|
-
.use(i18n({ dictionaries }))
|
|
476
|
-
.get("/", ({ getDictionary }) => ({ title: getDictionary("home").title }))
|
|
477
|
-
.listen(3000);
|
|
478
|
-
`);
|
|
479
|
-
break;
|
|
480
|
-
case "sveltekit":
|
|
481
|
-
logger.log(`
|
|
482
|
-
// src/hooks.server.ts
|
|
483
|
-
import { i18nHandle } from "dialexjs/sveltekit";
|
|
484
|
-
import dictionaries from "./dialex.generated.js";
|
|
485
|
-
|
|
486
|
-
export const handle = i18nHandle({ dictionaries });
|
|
487
|
-
|
|
488
|
-
// src/app.html: <html lang="%dialex.lang%">
|
|
489
|
-
// In load functions: locals.getDictionary("home")
|
|
490
|
-
`);
|
|
491
|
-
break;
|
|
492
|
-
case "astro":
|
|
493
|
-
logger.log(`
|
|
494
|
-
// src/middleware.ts
|
|
495
|
-
import { i18n } from "dialexjs/astro";
|
|
496
|
-
import dictionaries from "./dialex.generated.js";
|
|
497
|
-
|
|
498
|
-
export const onRequest = i18n({ dictionaries });
|
|
499
|
-
|
|
500
|
-
// In pages: Astro.locals.getDictionary("home")
|
|
501
|
-
`);
|
|
502
|
-
break;
|
|
503
|
-
case "vue":
|
|
504
|
-
logger.log(`
|
|
505
|
-
// src/main.ts
|
|
506
|
-
import { createApp } from "vue";
|
|
507
|
-
import { createI18n } from "dialexjs/vue";
|
|
508
|
-
import App from "./App.vue";
|
|
509
|
-
|
|
510
|
-
createApp(App).use(createI18n()).mount("#app");
|
|
511
|
-
|
|
512
|
-
// In components: const dict = useDictionary("home")
|
|
513
|
-
`);
|
|
514
|
-
break;
|
|
515
|
-
case "nuxt":
|
|
516
|
-
logger.log(`
|
|
517
|
-
// nuxt.config.ts
|
|
518
|
-
export default defineNuxtConfig({
|
|
519
|
-
modules: ["dialexjs/nuxt"],
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
// In components: const dict = useDictionary("home")
|
|
523
|
-
`);
|
|
524
|
-
break;
|
|
525
|
-
default: logger.log(`
|
|
526
|
-
Import dictionaries and register your framework middleware to begin!
|
|
527
|
-
Run \`npm run dx:generate\` whenever you add new dictionary files.
|
|
528
|
-
`);
|
|
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(", ")}`);
|
|
529
483
|
}
|
|
530
|
-
|
|
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)"
|
|
554
|
+
};
|
|
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"
|
|
565
|
+
};
|
|
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
|
+
};
|
|
531
589
|
}
|
|
532
590
|
//#endregion
|
|
533
|
-
//#region src/
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
message: issue.message,
|
|
545
|
-
line: issue.range ? issue.range.start.line + 1 : void 0
|
|
546
|
-
});
|
|
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);
|
|
601
|
+
}
|
|
547
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)));
|
|
548
612
|
/**
|
|
549
|
-
*
|
|
550
|
-
* (
|
|
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.
|
|
551
616
|
*/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (source && copyLeaf(df, source, target, leafPath, (v) => `[TODO] ${v}`)) {
|
|
567
|
-
fixed++;
|
|
568
|
-
changed = true;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
if (changed) await saveDictionaryFile(df);
|
|
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");
|
|
572
631
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
632
|
+
if (!listed("@dialexjs/cli")) {
|
|
633
|
+
pkg.devDependencies = sorted({
|
|
634
|
+
...pkg.devDependencies,
|
|
635
|
+
"@dialexjs/cli": range
|
|
636
|
+
});
|
|
637
|
+
added.push("@dialexjs/cli");
|
|
638
|
+
}
|
|
639
|
+
return added;
|
|
577
640
|
}
|
|
578
|
-
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/commands/init.ts
|
|
643
|
+
async function runInit(options = {}) {
|
|
579
644
|
const root = options.cwd || process.cwd();
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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;
|
|
595
722
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
let fixed = 0;
|
|
607
|
-
if (options.fix) {
|
|
608
|
-
fixed = await applyFixes(files, configuredLocales, defaultLocale);
|
|
609
|
-
if (fixed > 0 && !quiet) logger.success(`Inserted ${fixed} missing key${fixed === 1 ? "" : "s"} marked ${TODO_PREFIX.trim()}`);
|
|
610
|
-
}
|
|
611
|
-
const analysis = await analyzeProject({
|
|
612
|
-
root,
|
|
613
|
-
config,
|
|
614
|
-
src: options.src && options.src.length > 0 ? options.src : void 0,
|
|
615
|
-
runtime: true
|
|
616
|
-
});
|
|
617
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
618
|
-
const dictionaryFiles = new Set(analysis.files);
|
|
619
|
-
for (const issue of analysis.issues) {
|
|
620
|
-
if (issue.level === "info") continue;
|
|
621
|
-
const rel = path.relative(root, issue.file);
|
|
622
|
-
let diag = byFile.get(rel);
|
|
623
|
-
if (!diag) {
|
|
624
|
-
diag = {
|
|
625
|
-
file: rel,
|
|
626
|
-
errors: [],
|
|
627
|
-
warnings: [],
|
|
628
|
-
annotations: []
|
|
629
|
-
};
|
|
630
|
-
byFile.set(rel, diag);
|
|
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;
|
|
631
732
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
diagnostics: reported,
|
|
642
|
-
totalIssues,
|
|
643
|
-
totalWarnings,
|
|
644
|
-
fixed
|
|
645
|
-
};
|
|
646
|
-
if (options.github) for (const d of reported) for (const a of d.annotations) {
|
|
647
|
-
const location = `file=${d.file}${a.line ? `,line=${a.line}` : ""}`;
|
|
648
|
-
console.log(`::${a.level} ${location},title=dialex::${escapeAnnotation(a.message)}`);
|
|
649
|
-
}
|
|
650
|
-
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
651
|
-
else if (reported.length > 0) {
|
|
652
|
-
logger.log("");
|
|
653
|
-
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"}:`);
|
|
654
|
-
logger.log("");
|
|
655
|
-
for (const d of reported) {
|
|
656
|
-
logger.log(` ${pc.underline(pc.bold(d.file))} ${d.name ? pc.dim(`(${d.name})`) : ""}:`);
|
|
657
|
-
for (const a of d.annotations) {
|
|
658
|
-
const where = a.line ? pc.dim(`:${a.line}`) : "";
|
|
659
|
-
const mark = a.level === "error" ? pc.red("✖") : pc.yellow("⚠");
|
|
660
|
-
logger.log(` ${mark} ${a.message}${where}`);
|
|
661
|
-
}
|
|
662
|
-
logger.log("");
|
|
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;
|
|
663
742
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (!quiet) {
|
|
670
|
-
const warningNote = totalWarnings > 0 ? ` (${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})` : "";
|
|
671
|
-
logger.success(`All ${files.length} dictionary file${files.length === 1 ? "" : "s"} passed checks with 0 issues${warningNote}!`);
|
|
672
|
-
}
|
|
673
|
-
return result;
|
|
674
|
-
}
|
|
675
|
-
//#endregion
|
|
676
|
-
//#region src/commands/export.ts
|
|
677
|
-
const escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
678
|
-
const csvCell = (value) => /[",\r\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
|
|
679
|
-
async function runExport(options = {}) {
|
|
680
|
-
const root = options.cwd || process.cwd();
|
|
681
|
-
const format = options.format || "json";
|
|
682
|
-
if (![
|
|
683
|
-
"json",
|
|
684
|
-
"csv",
|
|
685
|
-
"xliff"
|
|
686
|
-
].includes(format)) throw new Error(`Unsupported format "${format}". Use json, csv, or xliff.`);
|
|
687
|
-
const outDir = path.resolve(root, options.out || "i18n-export");
|
|
688
|
-
const project = await loadProject(root, options.config);
|
|
689
|
-
const defaultLocale = project.config.defaultLocale || "en";
|
|
690
|
-
const table = /* @__PURE__ */ new Map();
|
|
691
|
-
const orderedKeys = /* @__PURE__ */ new Set();
|
|
692
|
-
let skippedFunctions = 0;
|
|
693
|
-
const localeSet = new Set(project.config.locales || []);
|
|
694
|
-
for (const { df } of project.dictionaries) listLocales(df).forEach((l) => localeSet.add(l));
|
|
695
|
-
let locales = Array.from(localeSet);
|
|
696
|
-
if (options.locale && options.locale.length > 0) locales = locales.filter((l) => options.locale.includes(l) || l === defaultLocale);
|
|
697
|
-
for (const locale of locales) table.set(locale, /* @__PURE__ */ new Map());
|
|
698
|
-
for (const { name, df } of project.dictionaries) for (const locale of listLocales(df)) {
|
|
699
|
-
if (!table.has(locale)) continue;
|
|
700
|
-
for (const leaf of listLeaves(df, locale)) if (leaf.kind === "string" && leaf.value !== void 0) {
|
|
701
|
-
const key = toKey(name, leaf.path);
|
|
702
|
-
table.get(locale).set(key, leaf.value);
|
|
703
|
-
orderedKeys.add(key);
|
|
704
|
-
} else if (locale === defaultLocale) skippedFunctions++;
|
|
705
|
-
}
|
|
706
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
707
|
-
const files = [];
|
|
708
|
-
const write = (name, content) => {
|
|
709
|
-
const file = path.join(outDir, name);
|
|
710
|
-
fs.writeFileSync(file, content, "utf-8");
|
|
711
|
-
files.push(path.relative(root, file));
|
|
712
|
-
};
|
|
713
|
-
if (format === "json") for (const [locale, values] of table) write(`${locale}.json`, JSON.stringify(Object.fromEntries(values), null, 2) + "\n");
|
|
714
|
-
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");
|
|
715
|
-
else {
|
|
716
|
-
const source = table.get(defaultLocale);
|
|
717
|
-
for (const [locale, values] of table) {
|
|
718
|
-
if (locale === defaultLocale) continue;
|
|
719
|
-
const units = [...orderedKeys].map((key) => {
|
|
720
|
-
const src = source?.get(key) ?? "";
|
|
721
|
-
const target = values.get(key);
|
|
722
|
-
const targetXml = target === void 0 ? "" : `<target>${escapeXml(target)}</target>`;
|
|
723
|
-
return ` <trans-unit id="${escapeXml(key)}"><source>${escapeXml(src)}</source>${targetXml}</trans-unit>`;
|
|
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
|
|
724
748
|
});
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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({});
|
|
733
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`);
|
|
734
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");
|
|
735
786
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
return result;
|
|
751
|
-
}
|
|
752
|
-
//#endregion
|
|
753
|
-
//#region src/commands/import.ts
|
|
754
|
-
const unescapeXml = (value) => value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/g, "&");
|
|
755
|
-
function parseCsv(text) {
|
|
756
|
-
const rows = [];
|
|
757
|
-
let row = [];
|
|
758
|
-
let cell = "";
|
|
759
|
-
let quoted = false;
|
|
760
|
-
for (let i = 0; i < text.length; i++) {
|
|
761
|
-
const ch = text[i];
|
|
762
|
-
if (quoted) {
|
|
763
|
-
if (ch === "\"" && text[i + 1] === "\"") {
|
|
764
|
-
cell += "\"";
|
|
765
|
-
i++;
|
|
766
|
-
} else if (ch === "\"") quoted = false;
|
|
767
|
-
else cell += ch;
|
|
768
|
-
} else if (ch === "\"") quoted = true;
|
|
769
|
-
else if (ch === ",") {
|
|
770
|
-
row.push(cell);
|
|
771
|
-
cell = "";
|
|
772
|
-
} else if (ch === "\n" || ch === "\r") {
|
|
773
|
-
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
774
|
-
row.push(cell);
|
|
775
|
-
rows.push(row);
|
|
776
|
-
row = [];
|
|
777
|
-
cell = "";
|
|
778
|
-
} else cell += ch;
|
|
779
|
-
}
|
|
780
|
-
if (cell !== "" || row.length > 0) {
|
|
781
|
-
row.push(cell);
|
|
782
|
-
rows.push(row);
|
|
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
|
+
}
|
|
783
801
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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.`);
|
|
803
831
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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.");
|
|
835
|
+
}
|
|
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 = {};
|
|
867
|
+
}
|
|
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");
|
|
825
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)}`);
|
|
826
892
|
}
|
|
827
|
-
|
|
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>;
|
|
828
983
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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!"));
|
|
835
1074
|
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/commands/lock.ts
|
|
836
1077
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
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.
|
|
839
1081
|
*/
|
|
840
|
-
function
|
|
841
|
-
if (!value.includes("{")) return void 0;
|
|
842
|
-
let signature;
|
|
843
|
-
try {
|
|
844
|
-
signature = argumentSignature(parseMessage(value));
|
|
845
|
-
} catch (error) {
|
|
846
|
-
return `invalid ICU: ${error.message}`;
|
|
847
|
-
}
|
|
848
|
-
const locales = listLocales(df);
|
|
849
|
-
for (const other of [defaultLocale, ...locales.filter((l) => l !== defaultLocale)]) {
|
|
850
|
-
if (other === locale) continue;
|
|
851
|
-
const reference = getString(df, other, keyPath);
|
|
852
|
-
if (!reference?.includes("{")) continue;
|
|
853
|
-
try {
|
|
854
|
-
const expected = argumentSignature(parseMessage(reference));
|
|
855
|
-
return expected === signature ? void 0 : `arguments differ from "${other}": expected ${expected || "none"}, found ${signature || "none"}`;
|
|
856
|
-
} catch {
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
async function runImport(options) {
|
|
1082
|
+
async function runLock(options = {}) {
|
|
862
1083
|
const root = options.cwd || process.cwd();
|
|
863
|
-
const file = path.resolve(root, options.file);
|
|
864
|
-
const format = options.format || inferFormat(file);
|
|
865
|
-
const fallbackLocale = options.locale ?? path.basename(file, path.extname(file));
|
|
866
|
-
const entries = parseEntries(fs.readFileSync(file, "utf-8"), format, options.locale ?? fallbackLocale);
|
|
867
1084
|
const project = await loadProject(root, options.config);
|
|
868
|
-
const
|
|
869
|
-
const
|
|
1085
|
+
const source = options.source ?? project.config.translate?.sourceLocale ?? project.config.defaultLocale ?? "en";
|
|
1086
|
+
const { lock } = lockFor(root, source);
|
|
870
1087
|
const result = {
|
|
871
1088
|
success: true,
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
rejected: [],
|
|
876
|
-
unknown: [],
|
|
877
|
-
errors: []
|
|
1089
|
+
sourceLocale: source,
|
|
1090
|
+
recorded: 0,
|
|
1091
|
+
stale: []
|
|
878
1092
|
};
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
result[outcome]++;
|
|
900
|
-
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
|
+
}
|
|
901
1113
|
}
|
|
902
1114
|
}
|
|
903
|
-
|
|
904
|
-
if (
|
|
905
|
-
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));
|
|
906
1117
|
else {
|
|
907
|
-
logger.success(`
|
|
908
|
-
if (result.
|
|
909
|
-
|
|
910
|
-
|
|
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
|
+
}
|
|
911
1123
|
}
|
|
912
1124
|
return result;
|
|
913
1125
|
}
|
|
@@ -960,6 +1172,8 @@ async function runTranslate(options = {}) {
|
|
|
960
1172
|
const everyLocale = new Set(project.config.locales ?? []);
|
|
961
1173
|
for (const { df } of project.dictionaries) listLocales(df).forEach((l) => everyLocale.add(l));
|
|
962
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}".`);
|
|
963
1177
|
const tasks = [];
|
|
964
1178
|
for (const dict of project.dictionaries) {
|
|
965
1179
|
const sourceLeaves = listLeaves(dict.df, source).filter((leaf) => leaf.kind === "string" && leaf.value !== void 0);
|
|
@@ -967,13 +1181,17 @@ async function runTranslate(options = {}) {
|
|
|
967
1181
|
const existing = new Map(listLeaves(dict.df, locale).map((leaf) => [leaf.path.join("\0"), leaf]));
|
|
968
1182
|
for (const leaf of sourceLeaves) {
|
|
969
1183
|
const current = existing.get(leaf.path.join("\0"));
|
|
970
|
-
|
|
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({
|
|
971
1188
|
dict,
|
|
972
1189
|
path: leaf.path,
|
|
973
1190
|
file: dict.rel,
|
|
974
1191
|
locale,
|
|
975
|
-
key
|
|
976
|
-
source: leaf.value
|
|
1192
|
+
key,
|
|
1193
|
+
source: leaf.value,
|
|
1194
|
+
...stale ? { stale: true } : {}
|
|
977
1195
|
});
|
|
978
1196
|
}
|
|
979
1197
|
}
|
|
@@ -995,7 +1213,7 @@ async function runTranslate(options = {}) {
|
|
|
995
1213
|
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
996
1214
|
else {
|
|
997
1215
|
logger.info(`Would translate ${tasks.length} string${tasks.length === 1 ? "" : "s"} from "${source}":`);
|
|
998
|
-
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)") : ""}`);
|
|
999
1217
|
}
|
|
1000
1218
|
return result;
|
|
1001
1219
|
}
|
|
@@ -1029,11 +1247,13 @@ async function runTranslate(options = {}) {
|
|
|
1029
1247
|
continue;
|
|
1030
1248
|
}
|
|
1031
1249
|
task.translated = translated;
|
|
1250
|
+
record(lock, locale, task.key, task.source);
|
|
1032
1251
|
result.translated++;
|
|
1033
1252
|
touched.add(task.dict);
|
|
1034
1253
|
}
|
|
1035
1254
|
}
|
|
1036
1255
|
for (const dict of touched) await saveDictionaryFile(dict.df);
|
|
1256
|
+
if (result.translated > 0) writeLock(root, lock);
|
|
1037
1257
|
if (result.rejected.length > 0) result.success = false;
|
|
1038
1258
|
if (quiet) console.log(JSON.stringify(result, null, 2));
|
|
1039
1259
|
else {
|
|
@@ -1045,17 +1265,17 @@ async function runTranslate(options = {}) {
|
|
|
1045
1265
|
//#endregion
|
|
1046
1266
|
//#region src/index.ts
|
|
1047
1267
|
const program = new Command();
|
|
1048
|
-
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);
|
|
1049
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) => {
|
|
1050
1270
|
await runGenerate(opts);
|
|
1051
1271
|
});
|
|
1052
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) => {
|
|
1053
1273
|
await runInit(opts);
|
|
1054
1274
|
});
|
|
1055
|
-
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) => {
|
|
1056
1276
|
await runCheck(opts);
|
|
1057
1277
|
});
|
|
1058
|
-
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", "
|
|
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) => {
|
|
1059
1279
|
await runExport(opts);
|
|
1060
1280
|
});
|
|
1061
1281
|
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").option("--json", "Print machine-readable JSON").action(async (file, opts) => {
|
|
@@ -1064,7 +1284,7 @@ program.command("import <file>").description("Write translated strings from a js
|
|
|
1064
1284
|
...opts
|
|
1065
1285
|
});
|
|
1066
1286
|
});
|
|
1067
|
-
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) => {
|
|
1068
1288
|
try {
|
|
1069
1289
|
await runTranslate(opts);
|
|
1070
1290
|
} catch (err) {
|
|
@@ -1076,6 +1296,14 @@ program.command("translate").description("Fill missing translations with the pro
|
|
|
1076
1296
|
process.exit(1);
|
|
1077
1297
|
}
|
|
1078
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
|
+
});
|
|
1079
1307
|
program.parse();
|
|
1080
1308
|
//#endregion
|
|
1081
1309
|
export {};
|