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,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-specific formatters for the dialekt CLI output.
|
|
3
|
+
*
|
|
4
|
+
* Imports the core utilities from `format.ts` and builds structured
|
|
5
|
+
* pretty / JSON renderers for each dialekt command.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
color,
|
|
10
|
+
glyphs,
|
|
11
|
+
drawTable,
|
|
12
|
+
banner,
|
|
13
|
+
sectionHeader,
|
|
14
|
+
success,
|
|
15
|
+
failure,
|
|
16
|
+
warning,
|
|
17
|
+
info,
|
|
18
|
+
keyValue,
|
|
19
|
+
} from './format.js';
|
|
20
|
+
import type { OutputFormat } from './format.js';
|
|
21
|
+
|
|
22
|
+
const C = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bold: '\x1b[1m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// ─── Missing keys formatter ──────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface MissingKeyEntry {
|
|
36
|
+
readonly adapter: string;
|
|
37
|
+
readonly locale: string;
|
|
38
|
+
readonly resource: string;
|
|
39
|
+
readonly key: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatMissingKeys(
|
|
43
|
+
entries: readonly MissingKeyEntry[],
|
|
44
|
+
format: OutputFormat,
|
|
45
|
+
): string {
|
|
46
|
+
if (format === 'json') {
|
|
47
|
+
return JSON.stringify(entries, null, 2) + '\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (entries.length === 0) {
|
|
51
|
+
return success('All translations are complete. No missing keys.') + '\n';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const grouped = new Map<string, Map<string, Map<string, string[]>>>();
|
|
55
|
+
for (const e of entries) {
|
|
56
|
+
const byAdapter = grouped.get(e.adapter) ?? new Map();
|
|
57
|
+
const byLocale = byAdapter.get(e.locale) ?? new Map();
|
|
58
|
+
const keys = byLocale.get(e.resource) ?? [];
|
|
59
|
+
keys.push(e.key);
|
|
60
|
+
byLocale.set(e.resource, keys);
|
|
61
|
+
byAdapter.set(e.locale, byLocale);
|
|
62
|
+
grouped.set(e.adapter, byAdapter);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lines: string[] = [];
|
|
66
|
+
const g = glyphs();
|
|
67
|
+
const total = entries.length;
|
|
68
|
+
|
|
69
|
+
lines.push(sectionHeader(`Missing keys (${total})`));
|
|
70
|
+
|
|
71
|
+
for (const [adapter, byLocale] of grouped) {
|
|
72
|
+
let adapterTotal = 0;
|
|
73
|
+
for (const byResource of byLocale.values()) {
|
|
74
|
+
for (const keys of byResource.values()) adapterTotal += keys.length;
|
|
75
|
+
}
|
|
76
|
+
lines.push(`\n ${color(adapter, C.bold + C.blue)} ${color(`(${adapterTotal})`, C.dim)}`);
|
|
77
|
+
|
|
78
|
+
for (const [locale, byResource] of byLocale) {
|
|
79
|
+
let localeTotal = 0;
|
|
80
|
+
for (const keys of byResource.values()) localeTotal += keys.length;
|
|
81
|
+
lines.push(` ${color(`${g.arrow} ${locale}`, C.yellow)} ${color(`(${localeTotal})`, C.dim)}`);
|
|
82
|
+
|
|
83
|
+
for (const [resource, keys] of byResource) {
|
|
84
|
+
lines.push(` ${color(resource, C.bold)}`);
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
lines.push(` ${color(g.bullet, C.dim)} ${key}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return lines.join('\n') + '\n';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Unused keys formatter ───────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface UnusedKeyEntry {
|
|
98
|
+
readonly adapter: string;
|
|
99
|
+
readonly locale: string;
|
|
100
|
+
readonly resource: string;
|
|
101
|
+
readonly key: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatUnusedKeys(
|
|
105
|
+
entries: readonly UnusedKeyEntry[],
|
|
106
|
+
format: OutputFormat,
|
|
107
|
+
): string {
|
|
108
|
+
if (format === 'json') {
|
|
109
|
+
return JSON.stringify(entries, null, 2) + '\n';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (entries.length === 0) {
|
|
113
|
+
return success('All keys are referenced in source files. No unused keys.') + '\n';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const grouped = new Map<string, Map<string, Map<string, string[]>>>();
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
const byAdapter = grouped.get(e.adapter) ?? new Map();
|
|
119
|
+
const byLocale = byAdapter.get(e.locale) ?? new Map();
|
|
120
|
+
const keys = byLocale.get(e.resource) ?? [];
|
|
121
|
+
keys.push(e.key);
|
|
122
|
+
byLocale.set(e.resource, keys);
|
|
123
|
+
byAdapter.set(e.locale, byLocale);
|
|
124
|
+
grouped.set(e.adapter, byAdapter);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
const g = glyphs();
|
|
129
|
+
const total = entries.length;
|
|
130
|
+
|
|
131
|
+
lines.push(sectionHeader(`Unused keys (${total})`));
|
|
132
|
+
|
|
133
|
+
for (const [adapter, byLocale] of grouped) {
|
|
134
|
+
let adapterTotal = 0;
|
|
135
|
+
for (const byResource of byLocale.values()) {
|
|
136
|
+
for (const keys of byResource.values()) adapterTotal += keys.length;
|
|
137
|
+
}
|
|
138
|
+
lines.push(`\n ${color(adapter, C.bold + C.blue)} ${color(`(${adapterTotal})`, C.dim)}`);
|
|
139
|
+
|
|
140
|
+
for (const [locale, byResource] of byLocale) {
|
|
141
|
+
let localeTotal = 0;
|
|
142
|
+
for (const keys of byResource.values()) localeTotal += keys.length;
|
|
143
|
+
lines.push(` ${color(`${g.arrow} ${locale}`, C.yellow)} ${color(`(${localeTotal})`, C.dim)}`);
|
|
144
|
+
|
|
145
|
+
for (const [resource, keys] of byResource) {
|
|
146
|
+
lines.push(` ${color(resource, C.bold)}`);
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
lines.push(` ${color(g.bullet, C.dim)} ${key}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return lines.join('\n') + '\n';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Validate formatter ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export interface ValidateEntry {
|
|
160
|
+
readonly adapter: string;
|
|
161
|
+
readonly locale: string;
|
|
162
|
+
readonly resource: string;
|
|
163
|
+
readonly count: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ValidateResult {
|
|
167
|
+
readonly passing: boolean;
|
|
168
|
+
readonly entries: readonly ValidateEntry[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function formatValidate(
|
|
172
|
+
result: ValidateResult,
|
|
173
|
+
format: OutputFormat,
|
|
174
|
+
): string {
|
|
175
|
+
if (format === 'json') {
|
|
176
|
+
return JSON.stringify(result, null, 2) + '\n';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (result.passing) {
|
|
180
|
+
return '\n' + success('All translations are up to date.') + '\n';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const rows = result.entries.map((e) => [
|
|
184
|
+
e.adapter,
|
|
185
|
+
e.locale,
|
|
186
|
+
e.resource,
|
|
187
|
+
e.count.toString(),
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const lines: string[] = [];
|
|
191
|
+
lines.push(failure(`Missing keys found in ${result.entries.length} resource(s)`));
|
|
192
|
+
lines.push('');
|
|
193
|
+
lines.push(drawTable(['Adapter', 'Locale', 'Resource', 'Missing'], rows));
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push(color(`Run ${color('dialekt translate', C.bold + C.cyan)} to fill missing keys.`, C.dim));
|
|
196
|
+
|
|
197
|
+
return lines.join('\n') + '\n';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Languages formatter ─────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export interface LanguageEntry {
|
|
203
|
+
readonly adapter: string;
|
|
204
|
+
readonly locales: readonly string[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function formatLanguages(
|
|
208
|
+
entries: readonly LanguageEntry[],
|
|
209
|
+
format: OutputFormat,
|
|
210
|
+
): string {
|
|
211
|
+
if (format === 'json') {
|
|
212
|
+
return JSON.stringify(entries, null, 2) + '\n';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (entries.length === 0) {
|
|
216
|
+
return warning('No adapters configured.') + '\n';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lines: string[] = [];
|
|
220
|
+
const g = glyphs();
|
|
221
|
+
|
|
222
|
+
for (const e of entries) {
|
|
223
|
+
lines.push(` ${color(e.adapter, C.bold + C.blue)}`);
|
|
224
|
+
lines.push(` ${color(`${g.arrow}`, C.dim)} ${e.locales.join(color(', ', C.dim))}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return lines.join('\n') + '\n';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Translate formatter ─────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export interface TranslateResult {
|
|
233
|
+
readonly success: boolean;
|
|
234
|
+
readonly message: string;
|
|
235
|
+
readonly stats?: {
|
|
236
|
+
readonly adaptersProcessed: number;
|
|
237
|
+
readonly localesTranslated: number;
|
|
238
|
+
readonly keysTranslated: number;
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function formatTranslate(
|
|
243
|
+
result: TranslateResult,
|
|
244
|
+
format: OutputFormat,
|
|
245
|
+
): string {
|
|
246
|
+
if (format === 'json') {
|
|
247
|
+
return JSON.stringify(result, null, 2) + '\n';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (result.success) {
|
|
251
|
+
const lines: string[] = [success(result.message)];
|
|
252
|
+
if (result.stats) {
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(keyValue('Adapters:', result.stats.adaptersProcessed.toString()));
|
|
255
|
+
lines.push(keyValue('Locales:', result.stats.localesTranslated.toString()));
|
|
256
|
+
lines.push(keyValue('Keys:', result.stats.keysTranslated.toString()));
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n') + '\n';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return failure(result.message) + '\n';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── Add formatter ───────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
export interface AddResult {
|
|
267
|
+
readonly success: boolean;
|
|
268
|
+
readonly message: string;
|
|
269
|
+
readonly addedResources?: readonly string[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function formatAdd(
|
|
273
|
+
result: AddResult,
|
|
274
|
+
format: OutputFormat,
|
|
275
|
+
): string {
|
|
276
|
+
if (format === 'json') {
|
|
277
|
+
return JSON.stringify(result, null, 2) + '\n';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (result.success) {
|
|
281
|
+
const lines: string[] = [success(result.message)];
|
|
282
|
+
if (result.addedResources && result.addedResources.length > 0) {
|
|
283
|
+
lines.push('');
|
|
284
|
+
lines.push(color('Added to:', C.dim));
|
|
285
|
+
for (const r of result.addedResources) {
|
|
286
|
+
lines.push(` ${color(glyphs().bullet, C.dim)} ${r}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return lines.join('\n') + '\n';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return failure(result.message) + '\n';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Benchmark formatter ─────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
export interface BenchmarkEntry {
|
|
298
|
+
readonly strategyName: string;
|
|
299
|
+
readonly totalChunks: number;
|
|
300
|
+
readonly succeededChunks: number;
|
|
301
|
+
readonly failedChunks: number;
|
|
302
|
+
readonly totalDurationMs: number;
|
|
303
|
+
readonly averageDurationMsPerChunk: number;
|
|
304
|
+
readonly totalAttempts: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function formatBenchmark(
|
|
308
|
+
entries: readonly BenchmarkEntry[],
|
|
309
|
+
format: OutputFormat,
|
|
310
|
+
): string {
|
|
311
|
+
if (format === 'json') {
|
|
312
|
+
return JSON.stringify(entries, null, 2) + '\n';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (entries.length === 0) {
|
|
316
|
+
return warning('No benchmark data available.') + '\n';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const lines: string[] = [];
|
|
320
|
+
|
|
321
|
+
lines.push(banner('Benchmark Results'));
|
|
322
|
+
|
|
323
|
+
const rows = entries.map((e) => [
|
|
324
|
+
e.strategyName,
|
|
325
|
+
`${e.succeededChunks}/${e.totalChunks}`,
|
|
326
|
+
`${e.totalDurationMs.toFixed(0)}ms`,
|
|
327
|
+
`${e.averageDurationMsPerChunk.toFixed(1)}ms`,
|
|
328
|
+
e.totalAttempts.toString(),
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
lines.push('');
|
|
332
|
+
lines.push(drawTable(
|
|
333
|
+
['Strategy', 'Chunks', 'Total', 'Avg/Chunk', 'Attempts'],
|
|
334
|
+
rows,
|
|
335
|
+
));
|
|
336
|
+
|
|
337
|
+
return lines.join('\n') + '\n';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Error formatter ─────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
export function formatError(
|
|
343
|
+
message: string,
|
|
344
|
+
format: OutputFormat,
|
|
345
|
+
): string {
|
|
346
|
+
if (format === 'json') {
|
|
347
|
+
return JSON.stringify({ error: message }, null, 2) + '\n';
|
|
348
|
+
}
|
|
349
|
+
return failure(message) + '\n';
|
|
350
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from '@effect/cli';
|
|
3
|
+
import { NodeContext, NodeRuntime } from '@effect/platform-node';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
import { translateCommand } from './commands/translate.js';
|
|
6
|
+
import { validateCommand } from './commands/validate.js';
|
|
7
|
+
import { addCommand } from './commands/add.js';
|
|
8
|
+
import { missingCommand } from './commands/missing.js';
|
|
9
|
+
import { unusedCommand } from './commands/unused.js';
|
|
10
|
+
import { languagesCommand } from './commands/languages.js';
|
|
11
|
+
import { benchmarkCommand } from './commands/benchmark.js';
|
|
12
|
+
|
|
13
|
+
const rootCommand = Command.make('dialekt').pipe(
|
|
14
|
+
Command.withSubcommands([
|
|
15
|
+
translateCommand,
|
|
16
|
+
validateCommand,
|
|
17
|
+
addCommand,
|
|
18
|
+
missingCommand,
|
|
19
|
+
unusedCommand,
|
|
20
|
+
languagesCommand,
|
|
21
|
+
benchmarkCommand,
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const cli = Command.run(rootCommand, {
|
|
26
|
+
name: 'dialekt',
|
|
27
|
+
version: '0.1.0',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const program = Effect.provide(cli(process.argv), NodeContext.layer);
|
|
31
|
+
NodeRuntime.runMain(program);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defineConfig } from './define-config.js';
|
|
3
|
+
import type { DialektConfig } from './types.js';
|
|
4
|
+
|
|
5
|
+
describe('defineConfig', () => {
|
|
6
|
+
it('returns its input unchanged', () => {
|
|
7
|
+
const config: DialektConfig = {
|
|
8
|
+
sourceLocale: 'en',
|
|
9
|
+
targetLocales: ['de'],
|
|
10
|
+
strategy: 'one-shot',
|
|
11
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
12
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
13
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
14
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
15
|
+
adapters: [],
|
|
16
|
+
};
|
|
17
|
+
expect(defineConfig(config)).toBe(config);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('accepts a config with adapters', () => {
|
|
21
|
+
const adapter = { name: 'test', capabilities: { canCreateResource: true, unusedKeyDetection: false }, listLocales: () => { throw new Error(); }, listResources: () => { throw new Error(); }, readResource: () => { throw new Error(); }, writeResource: () => { throw new Error(); } } as unknown as DialektConfig['adapters'][number];
|
|
22
|
+
const config: DialektConfig = {
|
|
23
|
+
sourceLocale: 'en',
|
|
24
|
+
targetLocales: ['de'],
|
|
25
|
+
strategy: 'one-shot',
|
|
26
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
27
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
28
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
29
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
30
|
+
adapters: [adapter],
|
|
31
|
+
};
|
|
32
|
+
expect(defineConfig(config)).toBe(config);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('accepts a config with tool-loop-agent strategy', () => {
|
|
36
|
+
const config: DialektConfig = {
|
|
37
|
+
sourceLocale: 'en',
|
|
38
|
+
targetLocales: ['de'],
|
|
39
|
+
strategy: 'tool-loop-agent',
|
|
40
|
+
model: { provider: 'anthropic', modelId: 'claude-3-sonnet' },
|
|
41
|
+
fastModel: { provider: 'anthropic', modelId: 'claude-3-haiku' },
|
|
42
|
+
chunking: { maxTokens: 4000, charsPerToken: 3.5, concurrency: 5 },
|
|
43
|
+
retry: { maxAttempts: 5, baseDelayMs: 500 },
|
|
44
|
+
adapters: [],
|
|
45
|
+
};
|
|
46
|
+
expect(defineConfig(config)).toBe(config);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves reference identity for nested objects', () => {
|
|
50
|
+
const model = { provider: 'openai', modelId: 'gpt-4o' };
|
|
51
|
+
const chunking = { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 };
|
|
52
|
+
const config: DialektConfig = {
|
|
53
|
+
sourceLocale: 'en',
|
|
54
|
+
targetLocales: ['de'],
|
|
55
|
+
strategy: 'one-shot',
|
|
56
|
+
model,
|
|
57
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
58
|
+
chunking,
|
|
59
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
60
|
+
adapters: [],
|
|
61
|
+
};
|
|
62
|
+
const result = defineConfig(config);
|
|
63
|
+
expect(result.model).toBe(model);
|
|
64
|
+
expect(result.chunking).toBe(chunking);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Effect, Either } from 'effect';
|
|
3
|
+
import { loadConfig, ConfigLoadError } from './load-config.js';
|
|
4
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
describe('loadConfig', () => {
|
|
9
|
+
it('loads a valid dialekt.config.ts', async () => {
|
|
10
|
+
const dir = join(tmpdir(), `dialekt-load-config-test-${Date.now()}`);
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
const configPath = join(dir, 'dialekt.config.ts');
|
|
13
|
+
writeFileSync(
|
|
14
|
+
configPath,
|
|
15
|
+
`export default { sourceLocale: 'en', targetLocales: ['de'], strategy: 'one-shot', model: { provider: 'openai', modelId: 'gpt-4o' }, fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' }, chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 }, retry: { maxAttempts: 3, baseDelayMs: 1000 }, adapters: [] };`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const result = await Effect.runPromise(loadConfig(configPath));
|
|
19
|
+
expect(result.sourceLocale).toBe('en');
|
|
20
|
+
expect(result.targetLocales).toEqual(['de']);
|
|
21
|
+
|
|
22
|
+
rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns ConfigLoadError for a nonexistent path', async () => {
|
|
26
|
+
const program = loadConfig('/nonexistent/path/config.ts');
|
|
27
|
+
const exit = await Effect.runPromise(Effect.either(program)) as Either.Either<unknown, ConfigLoadError>;
|
|
28
|
+
if (exit._tag === 'Left') {
|
|
29
|
+
expect(exit.left._tag).toBe('ConfigLoadError');
|
|
30
|
+
expect(exit.left.path).toBe('/nonexistent/path/config.ts');
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error('Expected Left');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createJiti } from 'jiti';
|
|
2
|
+
import { Effect, Data } from 'effect';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import type { DialektConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
export class ConfigLoadError extends Data.TaggedError('ConfigLoadError')<{
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly cause: unknown;
|
|
9
|
+
}> {}
|
|
10
|
+
|
|
11
|
+
export function loadConfig(configPath: string): Effect.Effect<DialektConfig, ConfigLoadError> {
|
|
12
|
+
return Effect.tryPromise({
|
|
13
|
+
try: async () => {
|
|
14
|
+
const jiti = createJiti(process.cwd());
|
|
15
|
+
const absolutePath = resolve(configPath);
|
|
16
|
+
const mod = await jiti.import(absolutePath, { default: true });
|
|
17
|
+
return mod as DialektConfig;
|
|
18
|
+
},
|
|
19
|
+
catch: (cause) => new ConfigLoadError({ path: configPath, cause }),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { DialektConfig, ModelConfig, ChunkingConfig, RetryConfig } from './types.js';
|
|
3
|
+
|
|
4
|
+
describe('config type conformance', () => {
|
|
5
|
+
it('accepts all known model providers', () => {
|
|
6
|
+
const providers: ModelConfig['provider'][] = ['openai', 'anthropic', 'google', 'mistral', 'cohere'];
|
|
7
|
+
for (const provider of providers) {
|
|
8
|
+
const m: ModelConfig = { provider, modelId: 'test-model' };
|
|
9
|
+
expect(m.provider).toBe(provider);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('accepts any string provider at compile time', () => {
|
|
14
|
+
// provider is typed as string, not a literal union
|
|
15
|
+
const valid: ModelConfig = { provider: 'unknown-provider', modelId: 'x' };
|
|
16
|
+
expect(valid.provider).toBe('unknown-provider');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('ChunkingConfig enforces positive maxTokens', () => {
|
|
20
|
+
const c: ChunkingConfig = { maxTokens: 1, charsPerToken: 1.0, concurrency: 1 };
|
|
21
|
+
expect(c.maxTokens).toBe(1);
|
|
22
|
+
expect(c.concurrency).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('RetryConfig enforces positive maxAttempts', () => {
|
|
26
|
+
const r: RetryConfig = { maxAttempts: 1, baseDelayMs: 0 };
|
|
27
|
+
expect(r.maxAttempts).toBe(1);
|
|
28
|
+
expect(r.baseDelayMs).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('DialektConfig requires sourceLocale and targetLocales', () => {
|
|
32
|
+
const minimal: DialektConfig = {
|
|
33
|
+
sourceLocale: 'en',
|
|
34
|
+
targetLocales: ['de'],
|
|
35
|
+
strategy: 'one-shot',
|
|
36
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
37
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
38
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
39
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
40
|
+
adapters: [],
|
|
41
|
+
};
|
|
42
|
+
expect(minimal.sourceLocale).toBe('en');
|
|
43
|
+
expect(minimal.targetLocales).toEqual(['de']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('DialektConfig accepts tool-loop-agent strategy', () => {
|
|
47
|
+
const config: DialektConfig = {
|
|
48
|
+
sourceLocale: 'en',
|
|
49
|
+
targetLocales: ['de'],
|
|
50
|
+
strategy: 'tool-loop-agent',
|
|
51
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
52
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
53
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
54
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
55
|
+
adapters: [],
|
|
56
|
+
};
|
|
57
|
+
expect(config.strategy).toBe('tool-loop-agent');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('targetLocales can be empty', () => {
|
|
61
|
+
const config: DialektConfig = {
|
|
62
|
+
sourceLocale: 'en',
|
|
63
|
+
targetLocales: [],
|
|
64
|
+
strategy: 'one-shot',
|
|
65
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
66
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
67
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
68
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
69
|
+
adapters: [],
|
|
70
|
+
};
|
|
71
|
+
expect(config.targetLocales).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts multiple target locales', () => {
|
|
75
|
+
const config: DialektConfig = {
|
|
76
|
+
sourceLocale: 'en',
|
|
77
|
+
targetLocales: ['de', 'fr', 'es', 'ja'],
|
|
78
|
+
strategy: 'one-shot',
|
|
79
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
80
|
+
fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
|
|
81
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
82
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
83
|
+
adapters: [],
|
|
84
|
+
};
|
|
85
|
+
expect(config.targetLocales).toHaveLength(4);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('fastModel can be different provider from model', () => {
|
|
89
|
+
const config: DialektConfig = {
|
|
90
|
+
sourceLocale: 'en',
|
|
91
|
+
targetLocales: ['de'],
|
|
92
|
+
strategy: 'one-shot',
|
|
93
|
+
model: { provider: 'openai', modelId: 'gpt-4o' },
|
|
94
|
+
fastModel: { provider: 'anthropic', modelId: 'claude-3-haiku' },
|
|
95
|
+
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
96
|
+
retry: { maxAttempts: 3, baseDelayMs: 1000 },
|
|
97
|
+
adapters: [],
|
|
98
|
+
};
|
|
99
|
+
expect(config.fastModel.provider).toBe('anthropic');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TranslationAdapter } from '../adapter/types.js';
|
|
2
|
+
|
|
3
|
+
export interface ModelConfig {
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly modelId: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ChunkingConfig {
|
|
9
|
+
readonly maxTokens: number;
|
|
10
|
+
readonly charsPerToken: number;
|
|
11
|
+
readonly concurrency: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RetryConfig {
|
|
15
|
+
readonly maxAttempts: number;
|
|
16
|
+
readonly baseDelayMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DialektConfig {
|
|
20
|
+
readonly sourceLocale: string;
|
|
21
|
+
readonly targetLocales: readonly string[] | null;
|
|
22
|
+
readonly strategy: 'one-shot' | 'tool-loop-agent';
|
|
23
|
+
readonly model: ModelConfig;
|
|
24
|
+
readonly fastModel: ModelConfig;
|
|
25
|
+
readonly chunking: ChunkingConfig;
|
|
26
|
+
readonly retry: RetryConfig;
|
|
27
|
+
readonly adapters: readonly TranslationAdapter[];
|
|
28
|
+
}
|