dialekt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/TESTING.md +66 -0
- package/dist/cli/main.d.mts +1 -0
- package/dist/cli/main.mjs +412 -0
- package/dist/formatters-De4Q-X1d.mjs +577 -0
- package/dist/index.d.mts +329 -0
- package/dist/index.mjs +60 -0
- package/package.json +39 -0
- package/pnpm-workspace.yaml +7 -0
- package/src/adapter/types.test.ts +98 -0
- package/src/adapter/types.ts +73 -0
- package/src/benchmark/metrics.test.ts +180 -0
- package/src/benchmark/metrics.ts +69 -0
- package/src/benchmark/report.test.ts +129 -0
- package/src/benchmark/report.ts +21 -0
- package/src/benchmark/runner.test.ts +162 -0
- package/src/benchmark/runner.ts +27 -0
- package/src/cli/commands/add.test.ts +267 -0
- package/src/cli/commands/add.ts +123 -0
- package/src/cli/commands/benchmark.test.ts +346 -0
- package/src/cli/commands/benchmark.ts +148 -0
- package/src/cli/commands/languages.test.ts +127 -0
- package/src/cli/commands/languages.ts +42 -0
- package/src/cli/commands/missing.test.ts +256 -0
- package/src/cli/commands/missing.ts +88 -0
- package/src/cli/commands/translate.test.ts +384 -0
- package/src/cli/commands/translate.ts +106 -0
- package/src/cli/commands/unused.test.ts +192 -0
- package/src/cli/commands/unused.ts +87 -0
- package/src/cli/commands/validate.test.ts +245 -0
- package/src/cli/commands/validate.ts +96 -0
- package/src/cli/config-resolution.test.ts +99 -0
- package/src/cli/config-resolution.ts +29 -0
- package/src/cli/format.test.ts +117 -0
- package/src/cli/format.ts +205 -0
- package/src/cli/formatters.test.ts +186 -0
- package/src/cli/formatters.ts +350 -0
- package/src/cli/main.ts +31 -0
- package/src/config/define-config.test.ts +66 -0
- package/src/config/define-config.ts +5 -0
- package/src/config/load-config.test.ts +35 -0
- package/src/config/load-config.ts +21 -0
- package/src/config/types.test.ts +101 -0
- package/src/config/types.ts +28 -0
- package/src/index.ts +56 -0
- package/src/keys/flatten.test.ts +111 -0
- package/src/keys/flatten.ts +41 -0
- package/src/sdk/file-io.test.ts +139 -0
- package/src/sdk/file-io.ts +21 -0
- package/src/sdk/node-layer.test.ts +54 -0
- package/src/sdk/node-layer.ts +10 -0
- package/src/sdk/php-array-reader.test.ts +114 -0
- package/src/sdk/php-array-reader.ts +26 -0
- package/src/translation/chunking.test.ts +118 -0
- package/src/translation/chunking.ts +57 -0
- package/src/translation/missing-keys.test.ts +179 -0
- package/src/translation/missing-keys.ts +36 -0
- package/src/translation/model-registry.test.ts +54 -0
- package/src/translation/model-registry.ts +43 -0
- package/src/translation/one-shot-strategy.test.ts +259 -0
- package/src/translation/one-shot-strategy.ts +48 -0
- package/src/translation/orchestrator.test.ts +276 -0
- package/src/translation/orchestrator.ts +83 -0
- package/src/translation/prompt.test.ts +149 -0
- package/src/translation/prompt.ts +42 -0
- package/src/translation/tool-loop-strategy.test.ts +279 -0
- package/src/translation/tool-loop-strategy.ts +68 -0
- package/src/translation/types.test.ts +37 -0
- package/src/translation/types.ts +21 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.ts +7 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { Data, Effect, Schedule } from "effect";
|
|
2
|
+
import { Output, ToolLoopAgent, generateText, hasToolCall, tool } from "ai";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
//#region src/keys/flatten.ts
|
|
7
|
+
function flattenObject(input, prefix = "") {
|
|
8
|
+
const output = {};
|
|
9
|
+
for (const [key, value] of Object.entries(input)) {
|
|
10
|
+
const fullKey = prefix === "" ? key : `${prefix}.${key}`;
|
|
11
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) Object.assign(output, flattenObject(value, fullKey));
|
|
12
|
+
else if (typeof value === "string") output[fullKey] = value;
|
|
13
|
+
}
|
|
14
|
+
return output;
|
|
15
|
+
}
|
|
16
|
+
function unflattenObject(input) {
|
|
17
|
+
const output = {};
|
|
18
|
+
for (const [dottedKey, value] of Object.entries(input)) {
|
|
19
|
+
const segments = dottedKey.split(".");
|
|
20
|
+
let cursor = output;
|
|
21
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
22
|
+
const segment = segments[i];
|
|
23
|
+
if (typeof cursor[segment] !== "object" || cursor[segment] === null) cursor[segment] = {};
|
|
24
|
+
cursor = cursor[segment];
|
|
25
|
+
}
|
|
26
|
+
cursor[segments[segments.length - 1]] = value;
|
|
27
|
+
}
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
function diffKeys(source, target) {
|
|
31
|
+
return Object.keys(source).filter((key) => !(key in target));
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/translation/chunking.ts
|
|
35
|
+
const PROMPT_OVERHEAD = 600;
|
|
36
|
+
const ITEM_JSON_OVERHEAD = 20;
|
|
37
|
+
const MIN_EFFECTIVE_MAX_CHARS = 200;
|
|
38
|
+
function chunkKeys(keys, sourceMap, targetMap, config) {
|
|
39
|
+
const maxChars = config.maxTokens * config.charsPerToken;
|
|
40
|
+
const sourceJson = JSON.stringify(sourceMap);
|
|
41
|
+
const targetJson = JSON.stringify(targetMap);
|
|
42
|
+
const contextOverhead = (sourceJson?.length ?? 0) + (targetJson?.length ?? 0) + PROMPT_OVERHEAD;
|
|
43
|
+
const effectiveMaxChars = Math.max(MIN_EFFECTIVE_MAX_CHARS, maxChars - contextOverhead);
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let currentChunk = [];
|
|
46
|
+
let currentChars = 0;
|
|
47
|
+
for (const key of keys) {
|
|
48
|
+
const value = sourceMap[key] ?? "";
|
|
49
|
+
const itemChars = key.length + value.length + ITEM_JSON_OVERHEAD;
|
|
50
|
+
if (itemChars > effectiveMaxChars && currentChunk.length === 0) {
|
|
51
|
+
chunks.push([key]);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (currentChunk.length > 0 && currentChars + itemChars > effectiveMaxChars) {
|
|
55
|
+
chunks.push(currentChunk);
|
|
56
|
+
currentChunk = [];
|
|
57
|
+
currentChars = 0;
|
|
58
|
+
}
|
|
59
|
+
currentChunk.push(key);
|
|
60
|
+
currentChars += itemChars;
|
|
61
|
+
}
|
|
62
|
+
if (currentChunk.length > 0) chunks.push(currentChunk);
|
|
63
|
+
if (chunks.length === 0 && keys.length > 0) return keys.map((key) => [key]);
|
|
64
|
+
return chunks;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/translation/model-registry.ts
|
|
68
|
+
var UnknownProviderError = class extends Data.TaggedError("UnknownProviderError") {};
|
|
69
|
+
/**
|
|
70
|
+
* The one file in the entire codebase allowed to import AI SDK provider packages.
|
|
71
|
+
*/
|
|
72
|
+
function resolveModel(config) {
|
|
73
|
+
return Effect.tryPromise({
|
|
74
|
+
try: async () => {
|
|
75
|
+
switch (config.provider) {
|
|
76
|
+
case "openai": {
|
|
77
|
+
const { openai } = await import("@ai-sdk/openai");
|
|
78
|
+
return openai(config.modelId);
|
|
79
|
+
}
|
|
80
|
+
case "anthropic": {
|
|
81
|
+
const { anthropic } = await import("@ai-sdk/anthropic");
|
|
82
|
+
return anthropic(config.modelId);
|
|
83
|
+
}
|
|
84
|
+
case "google": {
|
|
85
|
+
const { google } = await import("@ai-sdk/google");
|
|
86
|
+
return google(config.modelId);
|
|
87
|
+
}
|
|
88
|
+
default: throw new UnknownProviderError({ provider: config.provider });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
catch: (cause) => cause instanceof UnknownProviderError ? cause : new UnknownProviderError({ provider: config.provider })
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/translation/types.ts
|
|
96
|
+
var TranslationFailedError = class extends Data.TaggedError("TranslationFailedError") {};
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/translation/prompt.ts
|
|
99
|
+
function buildSystemPrompt(from, to) {
|
|
100
|
+
return `You are a professional software translator specializing in application localization.
|
|
101
|
+
You translate language strings from ${from} to ${to}.
|
|
102
|
+
|
|
103
|
+
Rules:
|
|
104
|
+
- Maintain consistent tone, formality, and terminology with existing translations.
|
|
105
|
+
- Do not translate proper nouns, brand names, or technical identifiers unless localization is standard.
|
|
106
|
+
- Preserve placeholders like :attribute, :min, :max, etc. Do not translate them.
|
|
107
|
+
- Use the exact same placeholder format as the source string.
|
|
108
|
+
- Return ONLY the requested keys. Do not add or remove keys.
|
|
109
|
+
- Do not escape Unicode characters with \\u notation. Write them directly.
|
|
110
|
+
- Escape double quotes in translations with a backslash when needed.`;
|
|
111
|
+
}
|
|
112
|
+
function buildUserPrompt(ctx) {
|
|
113
|
+
const sourceJson = JSON.stringify(ctx.sourceMap, null, 2);
|
|
114
|
+
const targetJson = JSON.stringify(ctx.targetMap, null, 2);
|
|
115
|
+
const keysWithValues = {};
|
|
116
|
+
for (const key of ctx.keys) keysWithValues[key] = ctx.sourceMap[key] ?? "";
|
|
117
|
+
const keysJson = JSON.stringify(keysWithValues, null, 2);
|
|
118
|
+
return `Translate the following language strings from ${ctx.sourceLocale} to ${ctx.targetLocale}.
|
|
119
|
+
|
|
120
|
+
<source-file>
|
|
121
|
+
${sourceJson}
|
|
122
|
+
</source-file>
|
|
123
|
+
|
|
124
|
+
<existing-translations>
|
|
125
|
+
${targetJson}
|
|
126
|
+
</existing-translations>
|
|
127
|
+
|
|
128
|
+
<keys-to-translate>
|
|
129
|
+
${keysJson}
|
|
130
|
+
</keys-to-translate>
|
|
131
|
+
|
|
132
|
+
Translate ALL keys listed in <keys-to-translate>. Use the existing translations and source file as context for consistency.`;
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/translation/one-shot-strategy.ts
|
|
136
|
+
async function tryTranslateChunk$1(model, ctx) {
|
|
137
|
+
const schema = z.object(Object.fromEntries(ctx.keys.map((key) => [key, z.string()])));
|
|
138
|
+
const { output } = await generateText({
|
|
139
|
+
model,
|
|
140
|
+
system: buildSystemPrompt(ctx.sourceLocale, ctx.targetLocale),
|
|
141
|
+
prompt: buildUserPrompt(ctx),
|
|
142
|
+
output: Output.object({ schema })
|
|
143
|
+
});
|
|
144
|
+
const missing = ctx.keys.filter((key) => !(key in output));
|
|
145
|
+
if (missing.length > 0) throw new Error(`Model omitted keys: ${missing.join(", ")}`);
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
function createOneShotStrategy(deps) {
|
|
149
|
+
return {
|
|
150
|
+
name: "one-shot",
|
|
151
|
+
translateChunk: (ctx) => Effect.tryPromise({
|
|
152
|
+
try: () => tryTranslateChunk$1(deps.model, ctx),
|
|
153
|
+
catch: (cause) => cause
|
|
154
|
+
}).pipe(Effect.retry(Schedule.exponential(`${deps.retry.baseDelayMs} millis`).pipe(Schedule.compose(Schedule.recurs(deps.retry.maxAttempts - 1)))), Effect.mapError((cause) => new TranslationFailedError({
|
|
155
|
+
keys: ctx.keys,
|
|
156
|
+
cause
|
|
157
|
+
})))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/translation/tool-loop-strategy.ts
|
|
162
|
+
async function tryTranslateChunk(model, ctx) {
|
|
163
|
+
const schema = z.object(Object.fromEntries(ctx.keys.map((key) => [key, z.string()])));
|
|
164
|
+
let captured = null;
|
|
165
|
+
const submitTranslations = tool({
|
|
166
|
+
description: "Submit the final translations for every requested key. Call this exactly once, with every key filled in.",
|
|
167
|
+
inputSchema: schema,
|
|
168
|
+
execute: async (input) => {
|
|
169
|
+
captured = input;
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
await new ToolLoopAgent({
|
|
174
|
+
model,
|
|
175
|
+
instructions: buildSystemPrompt(ctx.sourceLocale, ctx.targetLocale),
|
|
176
|
+
tools: { submitTranslations },
|
|
177
|
+
stopWhen: hasToolCall("submitTranslations")
|
|
178
|
+
}).generate({ prompt: buildUserPrompt(ctx) });
|
|
179
|
+
if (captured === null) throw new Error("Agent finished without calling submitTranslations");
|
|
180
|
+
const missing = ctx.keys.filter((key) => !(key in captured));
|
|
181
|
+
if (missing.length > 0) throw new Error(`Model omitted keys: ${missing.join(", ")}`);
|
|
182
|
+
return captured;
|
|
183
|
+
}
|
|
184
|
+
function createToolLoopStrategy(deps) {
|
|
185
|
+
return {
|
|
186
|
+
name: "tool-loop-agent",
|
|
187
|
+
translateChunk: (ctx) => Effect.tryPromise({
|
|
188
|
+
try: () => tryTranslateChunk(deps.model, ctx),
|
|
189
|
+
catch: (cause) => cause
|
|
190
|
+
}).pipe(Effect.retry(Schedule.exponential(`${deps.retry.baseDelayMs} millis`).pipe(Schedule.compose(Schedule.recurs(deps.retry.maxAttempts - 1)))), Effect.mapError((cause) => new TranslationFailedError({
|
|
191
|
+
keys: ctx.keys,
|
|
192
|
+
cause
|
|
193
|
+
})))
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/translation/orchestrator.ts
|
|
198
|
+
function runTranslation(config) {
|
|
199
|
+
return Effect.gen(function* () {
|
|
200
|
+
const failures = [];
|
|
201
|
+
for (const adapter of config.adapters) {
|
|
202
|
+
const locales = config.targetLocales.length > 0 ? config.targetLocales : yield* adapter.listLocales();
|
|
203
|
+
const sourceLocale = config.sourceLocale;
|
|
204
|
+
const targetLocales = locales.filter((l) => l !== sourceLocale);
|
|
205
|
+
for (const locale of targetLocales) {
|
|
206
|
+
const resources = yield* adapter.listResources(sourceLocale);
|
|
207
|
+
for (const resource of resources) {
|
|
208
|
+
const sourceMap = yield* adapter.readResource(sourceLocale, resource);
|
|
209
|
+
const targetMap = yield* adapter.readResource(locale, resource);
|
|
210
|
+
const missing = diffKeys(sourceMap, targetMap);
|
|
211
|
+
if (missing.length === 0) continue;
|
|
212
|
+
const chunks = chunkKeys(missing, sourceMap, targetMap, {
|
|
213
|
+
maxTokens: config.chunking.maxTokens,
|
|
214
|
+
charsPerToken: config.chunking.charsPerToken
|
|
215
|
+
});
|
|
216
|
+
const translatedChunks = [];
|
|
217
|
+
yield* Effect.forEach(chunks, (chunkKeysArr) => Effect.gen(function* () {
|
|
218
|
+
const result = yield* config.strategy.translateChunk({
|
|
219
|
+
sourceLocale,
|
|
220
|
+
targetLocale: locale,
|
|
221
|
+
sourceMap,
|
|
222
|
+
targetMap,
|
|
223
|
+
keys: chunkKeysArr
|
|
224
|
+
});
|
|
225
|
+
translatedChunks.push(result);
|
|
226
|
+
}).pipe(Effect.catchAll((err) => {
|
|
227
|
+
failures.push(err);
|
|
228
|
+
return Effect.void;
|
|
229
|
+
})), {
|
|
230
|
+
concurrency: config.chunking.concurrency,
|
|
231
|
+
discard: true
|
|
232
|
+
});
|
|
233
|
+
const merged = { ...targetMap };
|
|
234
|
+
for (const chunk of translatedChunks) Object.assign(merged, chunk);
|
|
235
|
+
yield* adapter.writeResource(locale, resource, merged);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (failures.length > 0) return yield* Effect.fail(new TranslationFailedError({
|
|
240
|
+
keys: failures.flatMap((f) => [...f.keys]),
|
|
241
|
+
cause: failures.map((f) => f.cause)
|
|
242
|
+
}));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/translation/missing-keys.ts
|
|
247
|
+
function computeMissingKeys(adapter, sourceLocale, targetLocales) {
|
|
248
|
+
return Effect.gen(function* () {
|
|
249
|
+
const resources = yield* adapter.listResources(sourceLocale);
|
|
250
|
+
return (yield* Effect.forEach(resources, (resource) => Effect.gen(function* () {
|
|
251
|
+
const sourceMap = yield* adapter.readResource(sourceLocale, resource);
|
|
252
|
+
return (yield* Effect.forEach(targetLocales, (locale) => Effect.gen(function* () {
|
|
253
|
+
const missing = diffKeys(sourceMap, yield* adapter.readResource(locale, resource));
|
|
254
|
+
return missing.length > 0 ? [{
|
|
255
|
+
adapter: adapter.name,
|
|
256
|
+
locale,
|
|
257
|
+
resource,
|
|
258
|
+
missing
|
|
259
|
+
}] : [];
|
|
260
|
+
}))).flat();
|
|
261
|
+
}))).flat();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/config/load-config.ts
|
|
266
|
+
var ConfigLoadError = class extends Data.TaggedError("ConfigLoadError") {};
|
|
267
|
+
function loadConfig(configPath) {
|
|
268
|
+
return Effect.tryPromise({
|
|
269
|
+
try: async () => {
|
|
270
|
+
const jiti = createJiti(process.cwd());
|
|
271
|
+
const absolutePath = resolve(configPath);
|
|
272
|
+
return await jiti.import(absolutePath, { default: true });
|
|
273
|
+
},
|
|
274
|
+
catch: (cause) => new ConfigLoadError({
|
|
275
|
+
path: configPath,
|
|
276
|
+
cause
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/cli/format.ts
|
|
282
|
+
/**
|
|
283
|
+
* Environment variables that signal dialekt is running inside an AI agent.
|
|
284
|
+
* When any is set (truthy), or stdout is not a TTY, JSON mode is the default.
|
|
285
|
+
*/
|
|
286
|
+
const AGENT_ENV_VARS = [
|
|
287
|
+
"CLAUDE_CODE",
|
|
288
|
+
"CLAUDECODE",
|
|
289
|
+
"CURSOR",
|
|
290
|
+
"CURSOR_TRACE_ID",
|
|
291
|
+
"DEVIN",
|
|
292
|
+
"GEMINI_CLI",
|
|
293
|
+
"AGENT_TASK_ID",
|
|
294
|
+
"AIDER_CHAT"
|
|
295
|
+
];
|
|
296
|
+
/**
|
|
297
|
+
* Resolves the output format from explicit flag and environment.
|
|
298
|
+
* Precedence: explicit `--format` > auto-detection.
|
|
299
|
+
*
|
|
300
|
+
* Auto-detection picks `json` when stdout is not a TTY or an agent env var
|
|
301
|
+
* is present; otherwise `pretty`.
|
|
302
|
+
*/
|
|
303
|
+
function detectFormat(explicit) {
|
|
304
|
+
if (explicit !== void 0) return explicit;
|
|
305
|
+
if (!process.stdout.isTTY) return "json";
|
|
306
|
+
if (AGENT_ENV_VARS.some((k) => process.env[k])) return "json";
|
|
307
|
+
return "pretty";
|
|
308
|
+
}
|
|
309
|
+
const C$1 = {
|
|
310
|
+
reset: "\x1B[0m",
|
|
311
|
+
bold: "\x1B[1m",
|
|
312
|
+
dim: "\x1B[2m",
|
|
313
|
+
red: "\x1B[31m",
|
|
314
|
+
green: "\x1B[32m",
|
|
315
|
+
yellow: "\x1B[33m",
|
|
316
|
+
blue: "\x1B[34m",
|
|
317
|
+
cyan: "\x1B[36m",
|
|
318
|
+
white: "\x1B[37m"
|
|
319
|
+
};
|
|
320
|
+
function isTty() {
|
|
321
|
+
return process.stdout.isTTY === true;
|
|
322
|
+
}
|
|
323
|
+
/** Wraps text in ANSI codes only when stdout is a TTY; otherwise returns it bare. */
|
|
324
|
+
function color(text, ...codes) {
|
|
325
|
+
if (!isTty()) return text;
|
|
326
|
+
return `${codes.join("")}${text}${C$1.reset}`;
|
|
327
|
+
}
|
|
328
|
+
const PRETTY_GLYPHS = {
|
|
329
|
+
hLine: String.fromCharCode(9472),
|
|
330
|
+
vLine: String.fromCharCode(9474),
|
|
331
|
+
cornerTL: String.fromCharCode(9484),
|
|
332
|
+
cornerTR: String.fromCharCode(9488),
|
|
333
|
+
cornerBL: String.fromCharCode(9492),
|
|
334
|
+
cornerBR: String.fromCharCode(9496),
|
|
335
|
+
teeRight: String.fromCharCode(9500),
|
|
336
|
+
teeLeft: String.fromCharCode(9508),
|
|
337
|
+
teeDown: String.fromCharCode(9516),
|
|
338
|
+
teeUp: String.fromCharCode(9524),
|
|
339
|
+
cross: String.fromCharCode(9532),
|
|
340
|
+
bullet: String.fromCharCode(8226),
|
|
341
|
+
arrow: String.fromCharCode(8594),
|
|
342
|
+
check: String.fromCharCode(10003),
|
|
343
|
+
crossMark: String.fromCharCode(10007),
|
|
344
|
+
warn: String.fromCharCode(9888)
|
|
345
|
+
};
|
|
346
|
+
const ASCII_GLYPHS = {
|
|
347
|
+
hLine: "-",
|
|
348
|
+
vLine: "|",
|
|
349
|
+
cornerTL: "+",
|
|
350
|
+
cornerTR: "+",
|
|
351
|
+
cornerBL: "+",
|
|
352
|
+
cornerBR: "+",
|
|
353
|
+
teeRight: "+",
|
|
354
|
+
teeLeft: "+",
|
|
355
|
+
teeDown: "+",
|
|
356
|
+
teeUp: "+",
|
|
357
|
+
cross: "+",
|
|
358
|
+
bullet: "*",
|
|
359
|
+
arrow: ">",
|
|
360
|
+
check: "+",
|
|
361
|
+
crossMark: "x",
|
|
362
|
+
warn: "!"
|
|
363
|
+
};
|
|
364
|
+
function glyphs() {
|
|
365
|
+
return isTty() ? PRETTY_GLYPHS : ASCII_GLYPHS;
|
|
366
|
+
}
|
|
367
|
+
function drawTable(headers, rows) {
|
|
368
|
+
const g = glyphs();
|
|
369
|
+
const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
370
|
+
const pad = (text, width) => text.padEnd(width);
|
|
371
|
+
const hLine = g.cornerTL + colWidths.map((w) => g.hLine.repeat(w + 2)).join(g.teeDown) + g.cornerTR;
|
|
372
|
+
const headerRow = g.vLine + headers.map((h, i) => ` ${color(pad(h, colWidths[i]), C$1.bold)} `).join(g.vLine) + g.vLine;
|
|
373
|
+
const separator = g.teeRight + colWidths.map((w) => g.hLine.repeat(w + 2)).join(g.cross) + g.teeLeft;
|
|
374
|
+
const dataRows = rows.map((row) => g.vLine + row.map((cell, i) => ` ${pad(cell, colWidths[i])} `).join(g.vLine) + g.vLine);
|
|
375
|
+
const bottomLine = g.cornerBL + colWidths.map((w) => g.hLine.repeat(w + 2)).join(g.teeUp) + g.cornerBR;
|
|
376
|
+
return [
|
|
377
|
+
hLine,
|
|
378
|
+
headerRow,
|
|
379
|
+
separator,
|
|
380
|
+
...dataRows,
|
|
381
|
+
bottomLine
|
|
382
|
+
].join("\n");
|
|
383
|
+
}
|
|
384
|
+
function banner(title) {
|
|
385
|
+
const line = glyphs().hLine.repeat(Math.max(title.length + 4, 40));
|
|
386
|
+
return `${color(line, C$1.dim)}\n ${color(title, C$1.bold + C$1.cyan)}\n${color(line, C$1.dim)}`;
|
|
387
|
+
}
|
|
388
|
+
function sectionHeader(label) {
|
|
389
|
+
return `\n${color(`${glyphs().arrow} ${label}`, C$1.bold + C$1.cyan)}`;
|
|
390
|
+
}
|
|
391
|
+
function success(text) {
|
|
392
|
+
return `${color(`${glyphs().check} ${text}`, C$1.green)}`;
|
|
393
|
+
}
|
|
394
|
+
function failure(text) {
|
|
395
|
+
return `${color(`${glyphs().crossMark} ${text}`, C$1.red)}`;
|
|
396
|
+
}
|
|
397
|
+
function warning(text) {
|
|
398
|
+
return `${color(`${glyphs().warn} ${text}`, C$1.yellow)}`;
|
|
399
|
+
}
|
|
400
|
+
function info(text) {
|
|
401
|
+
return color(text, C$1.dim);
|
|
402
|
+
}
|
|
403
|
+
function keyValue(key, value) {
|
|
404
|
+
return ` ${color(key, C$1.bold)} ${value}`;
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/cli/formatters.ts
|
|
408
|
+
/**
|
|
409
|
+
* Command-specific formatters for the dialekt CLI output.
|
|
410
|
+
*
|
|
411
|
+
* Imports the core utilities from `format.ts` and builds structured
|
|
412
|
+
* pretty / JSON renderers for each dialekt command.
|
|
413
|
+
*/
|
|
414
|
+
const C = {
|
|
415
|
+
reset: "\x1B[0m",
|
|
416
|
+
bold: "\x1B[1m",
|
|
417
|
+
dim: "\x1B[2m",
|
|
418
|
+
red: "\x1B[31m",
|
|
419
|
+
green: "\x1B[32m",
|
|
420
|
+
yellow: "\x1B[33m",
|
|
421
|
+
blue: "\x1B[34m",
|
|
422
|
+
cyan: "\x1B[36m"
|
|
423
|
+
};
|
|
424
|
+
function formatMissingKeys(entries, format) {
|
|
425
|
+
if (format === "json") return JSON.stringify(entries, null, 2) + "\n";
|
|
426
|
+
if (entries.length === 0) return success("All translations are complete. No missing keys.") + "\n";
|
|
427
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const e of entries) {
|
|
429
|
+
const byAdapter = grouped.get(e.adapter) ?? /* @__PURE__ */ new Map();
|
|
430
|
+
const byLocale = byAdapter.get(e.locale) ?? /* @__PURE__ */ new Map();
|
|
431
|
+
const keys = byLocale.get(e.resource) ?? [];
|
|
432
|
+
keys.push(e.key);
|
|
433
|
+
byLocale.set(e.resource, keys);
|
|
434
|
+
byAdapter.set(e.locale, byLocale);
|
|
435
|
+
grouped.set(e.adapter, byAdapter);
|
|
436
|
+
}
|
|
437
|
+
const lines = [];
|
|
438
|
+
const g = glyphs();
|
|
439
|
+
const total = entries.length;
|
|
440
|
+
lines.push(sectionHeader(`Missing keys (${total})`));
|
|
441
|
+
for (const [adapter, byLocale] of grouped) {
|
|
442
|
+
let adapterTotal = 0;
|
|
443
|
+
for (const byResource of byLocale.values()) for (const keys of byResource.values()) adapterTotal += keys.length;
|
|
444
|
+
lines.push(`\n ${color(adapter, C.bold + C.blue)} ${color(`(${adapterTotal})`, C.dim)}`);
|
|
445
|
+
for (const [locale, byResource] of byLocale) {
|
|
446
|
+
let localeTotal = 0;
|
|
447
|
+
for (const keys of byResource.values()) localeTotal += keys.length;
|
|
448
|
+
lines.push(` ${color(`${g.arrow} ${locale}`, C.yellow)} ${color(`(${localeTotal})`, C.dim)}`);
|
|
449
|
+
for (const [resource, keys] of byResource) {
|
|
450
|
+
lines.push(` ${color(resource, C.bold)}`);
|
|
451
|
+
for (const key of keys) lines.push(` ${color(g.bullet, C.dim)} ${key}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return lines.join("\n") + "\n";
|
|
456
|
+
}
|
|
457
|
+
function formatUnusedKeys(entries, format) {
|
|
458
|
+
if (format === "json") return JSON.stringify(entries, null, 2) + "\n";
|
|
459
|
+
if (entries.length === 0) return success("All keys are referenced in source files. No unused keys.") + "\n";
|
|
460
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const e of entries) {
|
|
462
|
+
const byAdapter = grouped.get(e.adapter) ?? /* @__PURE__ */ new Map();
|
|
463
|
+
const byLocale = byAdapter.get(e.locale) ?? /* @__PURE__ */ new Map();
|
|
464
|
+
const keys = byLocale.get(e.resource) ?? [];
|
|
465
|
+
keys.push(e.key);
|
|
466
|
+
byLocale.set(e.resource, keys);
|
|
467
|
+
byAdapter.set(e.locale, byLocale);
|
|
468
|
+
grouped.set(e.adapter, byAdapter);
|
|
469
|
+
}
|
|
470
|
+
const lines = [];
|
|
471
|
+
const g = glyphs();
|
|
472
|
+
const total = entries.length;
|
|
473
|
+
lines.push(sectionHeader(`Unused keys (${total})`));
|
|
474
|
+
for (const [adapter, byLocale] of grouped) {
|
|
475
|
+
let adapterTotal = 0;
|
|
476
|
+
for (const byResource of byLocale.values()) for (const keys of byResource.values()) adapterTotal += keys.length;
|
|
477
|
+
lines.push(`\n ${color(adapter, C.bold + C.blue)} ${color(`(${adapterTotal})`, C.dim)}`);
|
|
478
|
+
for (const [locale, byResource] of byLocale) {
|
|
479
|
+
let localeTotal = 0;
|
|
480
|
+
for (const keys of byResource.values()) localeTotal += keys.length;
|
|
481
|
+
lines.push(` ${color(`${g.arrow} ${locale}`, C.yellow)} ${color(`(${localeTotal})`, C.dim)}`);
|
|
482
|
+
for (const [resource, keys] of byResource) {
|
|
483
|
+
lines.push(` ${color(resource, C.bold)}`);
|
|
484
|
+
for (const key of keys) lines.push(` ${color(g.bullet, C.dim)} ${key}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return lines.join("\n") + "\n";
|
|
489
|
+
}
|
|
490
|
+
function formatValidate(result, format) {
|
|
491
|
+
if (format === "json") return JSON.stringify(result, null, 2) + "\n";
|
|
492
|
+
if (result.passing) return "\n" + success("All translations are up to date.") + "\n";
|
|
493
|
+
const rows = result.entries.map((e) => [
|
|
494
|
+
e.adapter,
|
|
495
|
+
e.locale,
|
|
496
|
+
e.resource,
|
|
497
|
+
e.count.toString()
|
|
498
|
+
]);
|
|
499
|
+
const lines = [];
|
|
500
|
+
lines.push(failure(`Missing keys found in ${result.entries.length} resource(s)`));
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push(drawTable([
|
|
503
|
+
"Adapter",
|
|
504
|
+
"Locale",
|
|
505
|
+
"Resource",
|
|
506
|
+
"Missing"
|
|
507
|
+
], rows));
|
|
508
|
+
lines.push("");
|
|
509
|
+
lines.push(color(`Run ${color("dialekt translate", C.bold + C.cyan)} to fill missing keys.`, C.dim));
|
|
510
|
+
return lines.join("\n") + "\n";
|
|
511
|
+
}
|
|
512
|
+
function formatLanguages(entries, format) {
|
|
513
|
+
if (format === "json") return JSON.stringify(entries, null, 2) + "\n";
|
|
514
|
+
if (entries.length === 0) return warning("No adapters configured.") + "\n";
|
|
515
|
+
const lines = [];
|
|
516
|
+
const g = glyphs();
|
|
517
|
+
for (const e of entries) {
|
|
518
|
+
lines.push(` ${color(e.adapter, C.bold + C.blue)}`);
|
|
519
|
+
lines.push(` ${color(`${g.arrow}`, C.dim)} ${e.locales.join(color(", ", C.dim))}`);
|
|
520
|
+
}
|
|
521
|
+
return lines.join("\n") + "\n";
|
|
522
|
+
}
|
|
523
|
+
function formatTranslate(result, format) {
|
|
524
|
+
if (format === "json") return JSON.stringify(result, null, 2) + "\n";
|
|
525
|
+
if (result.success) {
|
|
526
|
+
const lines = [success(result.message)];
|
|
527
|
+
if (result.stats) {
|
|
528
|
+
lines.push("");
|
|
529
|
+
lines.push(keyValue("Adapters:", result.stats.adaptersProcessed.toString()));
|
|
530
|
+
lines.push(keyValue("Locales:", result.stats.localesTranslated.toString()));
|
|
531
|
+
lines.push(keyValue("Keys:", result.stats.keysTranslated.toString()));
|
|
532
|
+
}
|
|
533
|
+
return lines.join("\n") + "\n";
|
|
534
|
+
}
|
|
535
|
+
return failure(result.message) + "\n";
|
|
536
|
+
}
|
|
537
|
+
function formatAdd(result, format) {
|
|
538
|
+
if (format === "json") return JSON.stringify(result, null, 2) + "\n";
|
|
539
|
+
if (result.success) {
|
|
540
|
+
const lines = [success(result.message)];
|
|
541
|
+
if (result.addedResources && result.addedResources.length > 0) {
|
|
542
|
+
lines.push("");
|
|
543
|
+
lines.push(color("Added to:", C.dim));
|
|
544
|
+
for (const r of result.addedResources) lines.push(` ${color(glyphs().bullet, C.dim)} ${r}`);
|
|
545
|
+
}
|
|
546
|
+
return lines.join("\n") + "\n";
|
|
547
|
+
}
|
|
548
|
+
return failure(result.message) + "\n";
|
|
549
|
+
}
|
|
550
|
+
function formatBenchmark(entries, format) {
|
|
551
|
+
if (format === "json") return JSON.stringify(entries, null, 2) + "\n";
|
|
552
|
+
if (entries.length === 0) return warning("No benchmark data available.") + "\n";
|
|
553
|
+
const lines = [];
|
|
554
|
+
lines.push(banner("Benchmark Results"));
|
|
555
|
+
const rows = entries.map((e) => [
|
|
556
|
+
e.strategyName,
|
|
557
|
+
`${e.succeededChunks}/${e.totalChunks}`,
|
|
558
|
+
`${e.totalDurationMs.toFixed(0)}ms`,
|
|
559
|
+
`${e.averageDurationMsPerChunk.toFixed(1)}ms`,
|
|
560
|
+
e.totalAttempts.toString()
|
|
561
|
+
]);
|
|
562
|
+
lines.push("");
|
|
563
|
+
lines.push(drawTable([
|
|
564
|
+
"Strategy",
|
|
565
|
+
"Chunks",
|
|
566
|
+
"Total",
|
|
567
|
+
"Avg/Chunk",
|
|
568
|
+
"Attempts"
|
|
569
|
+
], rows));
|
|
570
|
+
return lines.join("\n") + "\n";
|
|
571
|
+
}
|
|
572
|
+
function formatError(message, format) {
|
|
573
|
+
if (format === "json") return JSON.stringify({ error: message }, null, 2) + "\n";
|
|
574
|
+
return failure(message) + "\n";
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
export { resolveModel as A, runTranslation as C, buildUserPrompt as D, buildSystemPrompt as E, diffKeys as M, flattenObject as N, TranslationFailedError as O, unflattenObject as P, computeMissingKeys as S, createOneShotStrategy as T, sectionHeader as _, formatMissingKeys as a, ConfigLoadError as b, formatValidate as c, detectFormat as d, drawTable as f, keyValue as g, info as h, formatLanguages as i, chunkKeys as j, UnknownProviderError as k, banner as l, glyphs as m, formatBenchmark as n, formatTranslate as o, failure as p, formatError as r, formatUnusedKeys as s, formatAdd as t, color as u, success as v, createToolLoopStrategy as w, loadConfig as x, warning as y };
|