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.
Files changed (73) hide show
  1. package/README.md +62 -0
  2. package/TESTING.md +66 -0
  3. package/dist/cli/main.d.mts +1 -0
  4. package/dist/cli/main.mjs +412 -0
  5. package/dist/formatters-De4Q-X1d.mjs +577 -0
  6. package/dist/index.d.mts +329 -0
  7. package/dist/index.mjs +60 -0
  8. package/package.json +39 -0
  9. package/pnpm-workspace.yaml +7 -0
  10. package/src/adapter/types.test.ts +98 -0
  11. package/src/adapter/types.ts +73 -0
  12. package/src/benchmark/metrics.test.ts +180 -0
  13. package/src/benchmark/metrics.ts +69 -0
  14. package/src/benchmark/report.test.ts +129 -0
  15. package/src/benchmark/report.ts +21 -0
  16. package/src/benchmark/runner.test.ts +162 -0
  17. package/src/benchmark/runner.ts +27 -0
  18. package/src/cli/commands/add.test.ts +267 -0
  19. package/src/cli/commands/add.ts +123 -0
  20. package/src/cli/commands/benchmark.test.ts +346 -0
  21. package/src/cli/commands/benchmark.ts +148 -0
  22. package/src/cli/commands/languages.test.ts +127 -0
  23. package/src/cli/commands/languages.ts +42 -0
  24. package/src/cli/commands/missing.test.ts +256 -0
  25. package/src/cli/commands/missing.ts +88 -0
  26. package/src/cli/commands/translate.test.ts +384 -0
  27. package/src/cli/commands/translate.ts +106 -0
  28. package/src/cli/commands/unused.test.ts +192 -0
  29. package/src/cli/commands/unused.ts +87 -0
  30. package/src/cli/commands/validate.test.ts +245 -0
  31. package/src/cli/commands/validate.ts +96 -0
  32. package/src/cli/config-resolution.test.ts +99 -0
  33. package/src/cli/config-resolution.ts +29 -0
  34. package/src/cli/format.test.ts +117 -0
  35. package/src/cli/format.ts +205 -0
  36. package/src/cli/formatters.test.ts +186 -0
  37. package/src/cli/formatters.ts +350 -0
  38. package/src/cli/main.ts +31 -0
  39. package/src/config/define-config.test.ts +66 -0
  40. package/src/config/define-config.ts +5 -0
  41. package/src/config/load-config.test.ts +35 -0
  42. package/src/config/load-config.ts +21 -0
  43. package/src/config/types.test.ts +101 -0
  44. package/src/config/types.ts +28 -0
  45. package/src/index.ts +56 -0
  46. package/src/keys/flatten.test.ts +111 -0
  47. package/src/keys/flatten.ts +41 -0
  48. package/src/sdk/file-io.test.ts +139 -0
  49. package/src/sdk/file-io.ts +21 -0
  50. package/src/sdk/node-layer.test.ts +54 -0
  51. package/src/sdk/node-layer.ts +10 -0
  52. package/src/sdk/php-array-reader.test.ts +114 -0
  53. package/src/sdk/php-array-reader.ts +26 -0
  54. package/src/translation/chunking.test.ts +118 -0
  55. package/src/translation/chunking.ts +57 -0
  56. package/src/translation/missing-keys.test.ts +179 -0
  57. package/src/translation/missing-keys.ts +36 -0
  58. package/src/translation/model-registry.test.ts +54 -0
  59. package/src/translation/model-registry.ts +43 -0
  60. package/src/translation/one-shot-strategy.test.ts +259 -0
  61. package/src/translation/one-shot-strategy.ts +48 -0
  62. package/src/translation/orchestrator.test.ts +276 -0
  63. package/src/translation/orchestrator.ts +83 -0
  64. package/src/translation/prompt.test.ts +149 -0
  65. package/src/translation/prompt.ts +42 -0
  66. package/src/translation/tool-loop-strategy.test.ts +279 -0
  67. package/src/translation/tool-loop-strategy.ts +68 -0
  68. package/src/translation/types.test.ts +37 -0
  69. package/src/translation/types.ts +21 -0
  70. package/tsconfig.json +9 -0
  71. package/tsconfig.tsbuildinfo +1 -0
  72. package/tsdown.config.ts +7 -0
  73. package/vitest.config.ts +8 -0
@@ -0,0 +1,180 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Effect } from 'effect';
3
+ import { summarizeBenchmarkResults, runBenchmarkedChunk } from './metrics.js';
4
+ import type { ChunkBenchmarkResult } from './metrics.js';
5
+ import type { TranslationStrategy, TranslationContext } from '../translation/types.js';
6
+ import { TranslationFailedError } from '../translation/types.js';
7
+
8
+ describe('summarizeBenchmarkResults', () => {
9
+ it('computes totals and averages correctly', () => {
10
+ const results = [
11
+ { strategyName: 'one-shot' as const, chunkKeyCount: 2, durationMs: 100, attemptCount: 1, succeeded: true },
12
+ { strategyName: 'one-shot' as const, chunkKeyCount: 2, durationMs: 200, attemptCount: 1, succeeded: true },
13
+ { strategyName: 'one-shot' as const, chunkKeyCount: 1, durationMs: 300, attemptCount: 1, succeeded: false, errorMessage: 'oops' },
14
+ ];
15
+ const summary = summarizeBenchmarkResults(results);
16
+ expect(summary.totalChunks).toBe(3);
17
+ expect(summary.succeededChunks).toBe(2);
18
+ expect(summary.failedChunks).toBe(1);
19
+ expect(summary.totalDurationMs).toBe(600);
20
+ expect(summary.averageDurationMsPerChunk).toBe(200);
21
+ expect(summary.totalAttempts).toBe(3);
22
+ });
23
+
24
+ it('handles empty results', () => {
25
+ const summary = summarizeBenchmarkResults([]);
26
+ expect(summary.totalChunks).toBe(0);
27
+ expect(summary.succeededChunks).toBe(0);
28
+ expect(summary.failedChunks).toBe(0);
29
+ expect(summary.totalDurationMs).toBe(0);
30
+ expect(summary.averageDurationMsPerChunk).toBe(0);
31
+ expect(summary.totalAttempts).toBe(0);
32
+ expect(summary.strategyName).toBe('one-shot');
33
+ });
34
+
35
+ it('handles single result', () => {
36
+ const results = [
37
+ { strategyName: 'tool-loop-agent' as const, chunkKeyCount: 5, durationMs: 150, attemptCount: 2, succeeded: true },
38
+ ];
39
+ const summary = summarizeBenchmarkResults(results);
40
+ expect(summary.totalChunks).toBe(1);
41
+ expect(summary.succeededChunks).toBe(1);
42
+ expect(summary.failedChunks).toBe(0);
43
+ expect(summary.averageDurationMsPerChunk).toBe(150);
44
+ expect(summary.totalAttempts).toBe(2);
45
+ });
46
+
47
+ it('handles all failures', () => {
48
+ const results = [
49
+ { strategyName: 'one-shot' as const, chunkKeyCount: 1, durationMs: 50, attemptCount: 1, succeeded: false, errorMessage: 'a' },
50
+ { strategyName: 'one-shot' as const, chunkKeyCount: 1, durationMs: 60, attemptCount: 1, succeeded: false, errorMessage: 'b' },
51
+ ];
52
+ const summary = summarizeBenchmarkResults(results);
53
+ expect(summary.succeededChunks).toBe(0);
54
+ expect(summary.failedChunks).toBe(2);
55
+ expect(summary.totalDurationMs).toBe(110);
56
+ });
57
+
58
+ it('handles zero-duration results', () => {
59
+ const results = [
60
+ { strategyName: 'one-shot' as const, chunkKeyCount: 1, durationMs: 0, attemptCount: 1, succeeded: true },
61
+ ];
62
+ const summary = summarizeBenchmarkResults(results);
63
+ expect(summary.totalDurationMs).toBe(0);
64
+ expect(summary.averageDurationMsPerChunk).toBe(0);
65
+ });
66
+
67
+ it('preserves strategyName from first result', () => {
68
+ const results = [
69
+ { strategyName: 'tool-loop-agent' as const, chunkKeyCount: 1, durationMs: 100, attemptCount: 1, succeeded: true },
70
+ ];
71
+ const summary = summarizeBenchmarkResults(results);
72
+ expect(summary.strategyName).toBe('tool-loop-agent');
73
+ });
74
+
75
+ it('handles very large durations without overflow', () => {
76
+ const results = [
77
+ { strategyName: 'one-shot' as const, chunkKeyCount: 1, durationMs: 1_000_000, attemptCount: 1, succeeded: true },
78
+ { strategyName: 'one-shot' as const, chunkKeyCount: 1, durationMs: 2_000_000, attemptCount: 1, succeeded: true },
79
+ ];
80
+ const summary = summarizeBenchmarkResults(results);
81
+ expect(summary.totalDurationMs).toBe(3_000_000);
82
+ expect(summary.averageDurationMsPerChunk).toBe(1_500_000);
83
+ });
84
+ });
85
+
86
+ describe('runBenchmarkedChunk', () => {
87
+ it('measures duration and returns success', async () => {
88
+ const strategy: TranslationStrategy = {
89
+ name: 'one-shot',
90
+ translateChunk: () => Effect.succeed({ hello: 'Hallo' }),
91
+ };
92
+ const ctx: TranslationContext = {
93
+ sourceLocale: 'en',
94
+ targetLocale: 'de',
95
+ sourceMap: { hello: 'Hello' },
96
+ targetMap: {},
97
+ keys: ['hello'],
98
+ };
99
+ const result = await Effect.runPromise(runBenchmarkedChunk(strategy, ctx)) as ChunkBenchmarkResult;
100
+ expect(result.succeeded).toBe(true);
101
+ expect(result.chunkKeyCount).toBe(1);
102
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
103
+ expect(result.strategyName).toBe('one-shot');
104
+ });
105
+
106
+ it('records failure without propagating', async () => {
107
+ const strategy: TranslationStrategy = {
108
+ name: 'tool-loop-agent',
109
+ translateChunk: () =>
110
+ Effect.fail(new TranslationFailedError({ keys: ['a'], cause: 'boom' })),
111
+ };
112
+ const ctx: TranslationContext = {
113
+ sourceLocale: 'en',
114
+ targetLocale: 'de',
115
+ sourceMap: { a: 'A' },
116
+ targetMap: {},
117
+ keys: ['a'],
118
+ };
119
+ const result = await Effect.runPromise(runBenchmarkedChunk(strategy, ctx)) as ChunkBenchmarkResult;
120
+ expect(result.succeeded).toBe(false);
121
+ expect(result.errorMessage).toBe('boom');
122
+ expect(result.chunkKeyCount).toBe(1);
123
+ });
124
+
125
+ it('handles empty key list', async () => {
126
+ const strategy: TranslationStrategy = {
127
+ name: 'one-shot',
128
+ translateChunk: () => Effect.succeed({}),
129
+ };
130
+ const ctx: TranslationContext = {
131
+ sourceLocale: 'en',
132
+ targetLocale: 'de',
133
+ sourceMap: {},
134
+ targetMap: {},
135
+ keys: [],
136
+ };
137
+ const result = await Effect.runPromise(runBenchmarkedChunk(strategy, ctx)) as ChunkBenchmarkResult;
138
+ expect(result.succeeded).toBe(true);
139
+ expect(result.chunkKeyCount).toBe(0);
140
+ });
141
+
142
+ it('measures duration for slow strategies', async () => {
143
+ const strategy: TranslationStrategy = {
144
+ name: 'tool-loop-agent',
145
+ translateChunk: () =>
146
+ Effect.gen(function* () {
147
+ yield* Effect.sleep('50 millis');
148
+ return { k: 'v' };
149
+ }),
150
+ };
151
+ const ctx: TranslationContext = {
152
+ sourceLocale: 'en',
153
+ targetLocale: 'de',
154
+ sourceMap: { k: 'K' },
155
+ targetMap: {},
156
+ keys: ['k'],
157
+ };
158
+ const result = await Effect.runPromise(runBenchmarkedChunk(strategy, ctx)) as ChunkBenchmarkResult;
159
+ expect(result.succeeded).toBe(true);
160
+ expect(result.durationMs).toBeGreaterThanOrEqual(30);
161
+ });
162
+
163
+ it('captures Error cause in errorMessage', async () => {
164
+ const strategy: TranslationStrategy = {
165
+ name: 'one-shot',
166
+ translateChunk: () =>
167
+ Effect.fail(new TranslationFailedError({ keys: ['x'], cause: new Error('deep error') })),
168
+ };
169
+ const ctx: TranslationContext = {
170
+ sourceLocale: 'en',
171
+ targetLocale: 'de',
172
+ sourceMap: { x: 'X' },
173
+ targetMap: {},
174
+ keys: ['x'],
175
+ };
176
+ const result = await Effect.runPromise(runBenchmarkedChunk(strategy, ctx)) as ChunkBenchmarkResult;
177
+ expect(result.succeeded).toBe(false);
178
+ expect(result.errorMessage).toContain('deep error');
179
+ });
180
+ });
@@ -0,0 +1,69 @@
1
+ import { Effect } from 'effect';
2
+ import type { TranslationStrategy, TranslationContext } from '../translation/types.js';
3
+
4
+ export interface ChunkBenchmarkResult {
5
+ readonly strategyName: 'one-shot' | 'tool-loop-agent';
6
+ readonly chunkKeyCount: number;
7
+ readonly durationMs: number;
8
+ readonly attemptCount: number;
9
+ readonly succeeded: boolean;
10
+ readonly errorMessage?: string | undefined;
11
+ }
12
+
13
+ export interface StrategyBenchmarkSummary {
14
+ readonly strategyName: 'one-shot' | 'tool-loop-agent';
15
+ readonly totalChunks: number;
16
+ readonly succeededChunks: number;
17
+ readonly failedChunks: number;
18
+ readonly totalDurationMs: number;
19
+ readonly averageDurationMsPerChunk: number;
20
+ readonly totalAttempts: number;
21
+ }
22
+
23
+ export function summarizeBenchmarkResults(
24
+ results: readonly ChunkBenchmarkResult[],
25
+ ): StrategyBenchmarkSummary {
26
+ const totalChunks = results.length;
27
+ const succeededChunks = results.filter((r) => r.succeeded).length;
28
+ const failedChunks = totalChunks - succeededChunks;
29
+ const totalDurationMs = results.reduce((sum, r) => sum + r.durationMs, 0);
30
+ const totalAttempts = results.reduce((sum, r) => sum + r.attemptCount, 0);
31
+ return {
32
+ strategyName: results[0]?.strategyName ?? 'one-shot',
33
+ totalChunks,
34
+ succeededChunks,
35
+ failedChunks,
36
+ totalDurationMs,
37
+ averageDurationMsPerChunk: totalChunks > 0 ? totalDurationMs / totalChunks : 0,
38
+ totalAttempts,
39
+ };
40
+ }
41
+
42
+ export function runBenchmarkedChunk(
43
+ strategy: TranslationStrategy,
44
+ ctx: TranslationContext,
45
+ ): Effect.Effect<ChunkBenchmarkResult, never> {
46
+ return Effect.gen(function* () {
47
+ const start = Date.now();
48
+ const result = yield* Effect.either(strategy.translateChunk(ctx));
49
+ const durationMs = Date.now() - start;
50
+ if (result._tag === 'Right') {
51
+ return {
52
+ strategyName: strategy.name,
53
+ chunkKeyCount: ctx.keys.length,
54
+ durationMs,
55
+ attemptCount: 1,
56
+ succeeded: true as const,
57
+ errorMessage: undefined,
58
+ };
59
+ }
60
+ return {
61
+ strategyName: strategy.name,
62
+ chunkKeyCount: ctx.keys.length,
63
+ durationMs,
64
+ attemptCount: 1,
65
+ succeeded: false as const,
66
+ errorMessage: String((result.left as { cause?: unknown }).cause),
67
+ };
68
+ });
69
+ }
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatBenchmarkReport } from './report.js';
3
+ import type { StrategyBenchmarkSummary } from './metrics.js';
4
+
5
+ describe('formatBenchmarkReport', () => {
6
+ it('produces valid JSON round-trip', () => {
7
+ const summaries: StrategyBenchmarkSummary[] = [
8
+ {
9
+ strategyName: 'one-shot',
10
+ totalChunks: 3,
11
+ succeededChunks: 3,
12
+ failedChunks: 0,
13
+ totalDurationMs: 300,
14
+ averageDurationMsPerChunk: 100,
15
+ totalAttempts: 3,
16
+ },
17
+ ];
18
+ const json = formatBenchmarkReport(summaries, 'json');
19
+ expect(JSON.parse(json)).toEqual(summaries);
20
+ });
21
+
22
+ it('produces human-readable table with key metrics', () => {
23
+ const summaries: StrategyBenchmarkSummary[] = [
24
+ {
25
+ strategyName: 'one-shot',
26
+ totalChunks: 3,
27
+ succeededChunks: 3,
28
+ failedChunks: 0,
29
+ totalDurationMs: 300,
30
+ averageDurationMsPerChunk: 100,
31
+ totalAttempts: 3,
32
+ },
33
+ ];
34
+ const table = formatBenchmarkReport(summaries, 'table');
35
+ expect(table).toContain('one-shot');
36
+ expect(table).toContain('300');
37
+ expect(table).toContain('100.0');
38
+ expect(table).toContain('3');
39
+ });
40
+
41
+ it('handles multiple strategies in table format', () => {
42
+ const summaries: StrategyBenchmarkSummary[] = [
43
+ {
44
+ strategyName: 'one-shot',
45
+ totalChunks: 5,
46
+ succeededChunks: 5,
47
+ failedChunks: 0,
48
+ totalDurationMs: 500,
49
+ averageDurationMsPerChunk: 100,
50
+ totalAttempts: 5,
51
+ },
52
+ {
53
+ strategyName: 'tool-loop-agent',
54
+ totalChunks: 5,
55
+ succeededChunks: 4,
56
+ failedChunks: 1,
57
+ totalDurationMs: 750,
58
+ averageDurationMsPerChunk: 150,
59
+ totalAttempts: 6,
60
+ },
61
+ ];
62
+ const table = formatBenchmarkReport(summaries, 'table');
63
+ expect(table).toContain('one-shot');
64
+ expect(table).toContain('tool-loop-agent');
65
+ expect(table).toContain('4 ok, 1 failed');
66
+ });
67
+
68
+ it('handles empty summaries in JSON format', () => {
69
+ const json = formatBenchmarkReport([], 'json');
70
+ expect(JSON.parse(json)).toEqual([]);
71
+ });
72
+
73
+ it('handles empty summaries in table format', () => {
74
+ const table = formatBenchmarkReport([], 'table');
75
+ expect(table).toContain('Benchmark Results');
76
+ });
77
+
78
+ it('handles all-failed strategy in table', () => {
79
+ const summaries: StrategyBenchmarkSummary[] = [
80
+ {
81
+ strategyName: 'one-shot',
82
+ totalChunks: 3,
83
+ succeededChunks: 0,
84
+ failedChunks: 3,
85
+ totalDurationMs: 300,
86
+ averageDurationMsPerChunk: 100,
87
+ totalAttempts: 3,
88
+ },
89
+ ];
90
+ const table = formatBenchmarkReport(summaries, 'table');
91
+ expect(table).toContain('0 ok, 3 failed');
92
+ });
93
+
94
+ it('handles zero-duration strategy', () => {
95
+ const summaries: StrategyBenchmarkSummary[] = [
96
+ {
97
+ strategyName: 'one-shot',
98
+ totalChunks: 1,
99
+ succeededChunks: 1,
100
+ failedChunks: 0,
101
+ totalDurationMs: 0,
102
+ averageDurationMsPerChunk: 0,
103
+ totalAttempts: 1,
104
+ },
105
+ ];
106
+ const table = formatBenchmarkReport(summaries, 'table');
107
+ expect(table).toContain('0ms');
108
+ expect(table).toContain('0.0ms');
109
+ });
110
+
111
+ it('JSON format includes all numeric fields', () => {
112
+ const summaries: StrategyBenchmarkSummary[] = [
113
+ {
114
+ strategyName: 'tool-loop-agent',
115
+ totalChunks: 2,
116
+ succeededChunks: 1,
117
+ failedChunks: 1,
118
+ totalDurationMs: 1234,
119
+ averageDurationMsPerChunk: 617,
120
+ totalAttempts: 4,
121
+ },
122
+ ];
123
+ const json = formatBenchmarkReport(summaries, 'json');
124
+ const parsed = JSON.parse(json) as StrategyBenchmarkSummary[];
125
+ expect(parsed[0]!.totalChunks).toBe(2);
126
+ expect(parsed[0]!.totalAttempts).toBe(4);
127
+ expect(parsed[0]!.averageDurationMsPerChunk).toBe(617);
128
+ });
129
+ });
@@ -0,0 +1,21 @@
1
+ import type { StrategyBenchmarkSummary } from './metrics.js';
2
+
3
+ export function formatBenchmarkReport(
4
+ summaries: readonly StrategyBenchmarkSummary[],
5
+ format: 'table' | 'json',
6
+ ): string {
7
+ if (format === 'json') {
8
+ return JSON.stringify(summaries, null, 2);
9
+ }
10
+
11
+ const lines: string[] = ['Benchmark Results', '================='];
12
+ for (const s of summaries) {
13
+ lines.push(`Strategy: ${s.strategyName}`);
14
+ lines.push(` Chunks: ${s.totalChunks} (${s.succeededChunks} ok, ${s.failedChunks} failed)`);
15
+ lines.push(` Total duration: ${s.totalDurationMs.toFixed(0)}ms`);
16
+ lines.push(` Avg per chunk: ${s.averageDurationMsPerChunk.toFixed(1)}ms`);
17
+ lines.push(` Total attempts: ${s.totalAttempts}`);
18
+ lines.push('');
19
+ }
20
+ return lines.join('\n');
21
+ }
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Effect } from 'effect';
3
+ import { runBenchmark } from './runner.js';
4
+ import type { StrategyBenchmarkSummary } from './metrics.js';
5
+ import type { TranslationStrategy, TranslationContext } from '../translation/types.js';
6
+ import { TranslationFailedError } from '../translation/types.js';
7
+
8
+ describe('runBenchmark', () => {
9
+ it('returns summaries for both strategies', async () => {
10
+ const a: TranslationStrategy = {
11
+ name: 'one-shot',
12
+ translateChunk: () => Effect.succeed({ k: 'a' }),
13
+ };
14
+ const b: TranslationStrategy = {
15
+ name: 'tool-loop-agent',
16
+ translateChunk: () => Effect.succeed({ k: 'b' }),
17
+ };
18
+ const chunks: TranslationContext[] = [
19
+ {
20
+ sourceLocale: 'en',
21
+ targetLocale: 'de',
22
+ sourceMap: { k: 'K' },
23
+ targetMap: {},
24
+ keys: ['k'],
25
+ },
26
+ ];
27
+ const result = await Effect.runPromise(
28
+ runBenchmark({ strategies: [a, b], chunks, concurrency: 1 }),
29
+ ) as StrategyBenchmarkSummary[];
30
+ expect(result).toHaveLength(2);
31
+ expect(result[0]!.strategyName).toBe('one-shot');
32
+ expect(result[1]!.strategyName).toBe('tool-loop-agent');
33
+ });
34
+
35
+ it('does not abort when one strategy fails every chunk', async () => {
36
+ const a: TranslationStrategy = {
37
+ name: 'one-shot',
38
+ translateChunk: () => Effect.succeed({ k: 'a' }),
39
+ };
40
+ const b: TranslationStrategy = {
41
+ name: 'tool-loop-agent',
42
+ translateChunk: () =>
43
+ Effect.fail(new TranslationFailedError({ keys: ['k'], cause: 'boom' })),
44
+ };
45
+ const chunks: TranslationContext[] = [
46
+ {
47
+ sourceLocale: 'en',
48
+ targetLocale: 'de',
49
+ sourceMap: { k: 'K' },
50
+ targetMap: {},
51
+ keys: ['k'],
52
+ },
53
+ ];
54
+ const result = await Effect.runPromise(
55
+ runBenchmark({ strategies: [a, b], chunks, concurrency: 1 }),
56
+ ) as StrategyBenchmarkSummary[];
57
+ expect(result[0]!.succeededChunks).toBe(1);
58
+ expect(result[1]!.failedChunks).toBe(1);
59
+ });
60
+
61
+ it('handles empty chunks', async () => {
62
+ const a: TranslationStrategy = {
63
+ name: 'one-shot',
64
+ translateChunk: () => Effect.succeed({}),
65
+ };
66
+ const result = await Effect.runPromise(
67
+ runBenchmark({ strategies: [a], chunks: [], concurrency: 1 }),
68
+ ) as StrategyBenchmarkSummary[];
69
+ expect(result).toHaveLength(1);
70
+ expect(result[0]!.totalChunks).toBe(0);
71
+ expect(result[0]!.succeededChunks).toBe(0);
72
+ });
73
+
74
+ it('handles single strategy', async () => {
75
+ const a: TranslationStrategy = {
76
+ name: 'one-shot',
77
+ translateChunk: () => Effect.succeed({ a: 'A', b: 'B' }),
78
+ };
79
+ const chunks: TranslationContext[] = [
80
+ {
81
+ sourceLocale: 'en',
82
+ targetLocale: 'de',
83
+ sourceMap: { a: 'A', b: 'B' },
84
+ targetMap: {},
85
+ keys: ['a', 'b'],
86
+ },
87
+ ];
88
+ const result = await Effect.runPromise(
89
+ runBenchmark({ strategies: [a], chunks, concurrency: 1 }),
90
+ ) as StrategyBenchmarkSummary[];
91
+ expect(result).toHaveLength(1);
92
+ expect(result[0]!.totalChunks).toBe(1);
93
+ expect(result[0]!.succeededChunks).toBe(1);
94
+ });
95
+
96
+ it('handles multiple chunks with concurrency', async () => {
97
+ const a: TranslationStrategy = {
98
+ name: 'one-shot',
99
+ translateChunk: () => Effect.succeed({ k: 'v' }),
100
+ };
101
+ const chunks: TranslationContext[] = Array.from({ length: 5 }, (_, i) => ({
102
+ sourceLocale: 'en',
103
+ targetLocale: 'de',
104
+ sourceMap: { [`k${i}`]: `V${i}` },
105
+ targetMap: {},
106
+ keys: [`k${i}`],
107
+ }));
108
+ const result = await Effect.runPromise(
109
+ runBenchmark({ strategies: [a], chunks, concurrency: 3 }),
110
+ ) as StrategyBenchmarkSummary[];
111
+ expect(result[0]!.totalChunks).toBe(5);
112
+ expect(result[0]!.succeededChunks).toBe(5);
113
+ });
114
+
115
+ it('handles all strategies failing', async () => {
116
+ const a: TranslationStrategy = {
117
+ name: 'one-shot',
118
+ translateChunk: () =>
119
+ Effect.fail(new TranslationFailedError({ keys: ['k'], cause: 'fail' })),
120
+ };
121
+ const chunks: TranslationContext[] = [
122
+ {
123
+ sourceLocale: 'en',
124
+ targetLocale: 'de',
125
+ sourceMap: { k: 'K' },
126
+ targetMap: {},
127
+ keys: ['k'],
128
+ },
129
+ ];
130
+ const result = await Effect.runPromise(
131
+ runBenchmark({ strategies: [a], chunks, concurrency: 1 }),
132
+ ) as StrategyBenchmarkSummary[];
133
+ expect(result[0]!.failedChunks).toBe(1);
134
+ expect(result[0]!.succeededChunks).toBe(0);
135
+ });
136
+
137
+ it('handles mixed success and failure across chunks', async () => {
138
+ let callCount = 0;
139
+ const a: TranslationStrategy = {
140
+ name: 'one-shot',
141
+ translateChunk: () => {
142
+ callCount++;
143
+ return callCount % 2 === 1
144
+ ? Effect.succeed({ k: 'v' })
145
+ : Effect.fail(new TranslationFailedError({ keys: ['k'], cause: 'odd' }));
146
+ },
147
+ };
148
+ const chunks: TranslationContext[] = Array.from({ length: 4 }, () => ({
149
+ sourceLocale: 'en',
150
+ targetLocale: 'de',
151
+ sourceMap: { k: 'K' },
152
+ targetMap: {},
153
+ keys: ['k'],
154
+ }));
155
+ const result = await Effect.runPromise(
156
+ runBenchmark({ strategies: [a], chunks, concurrency: 1 }),
157
+ ) as StrategyBenchmarkSummary[];
158
+ expect(result[0]!.totalChunks).toBe(4);
159
+ expect(result[0]!.succeededChunks).toBe(2);
160
+ expect(result[0]!.failedChunks).toBe(2);
161
+ });
162
+ });
@@ -0,0 +1,27 @@
1
+ import { Effect } from 'effect';
2
+ import type { TranslationStrategy, TranslationContext } from '../translation/types.js';
3
+ import type { StrategyBenchmarkSummary } from './metrics.js';
4
+ import { runBenchmarkedChunk, summarizeBenchmarkResults } from './metrics.js';
5
+
6
+ export interface BenchmarkConfig {
7
+ readonly strategies: readonly TranslationStrategy[];
8
+ readonly chunks: readonly TranslationContext[];
9
+ readonly concurrency: number;
10
+ }
11
+
12
+ export function runBenchmark(
13
+ config: BenchmarkConfig,
14
+ ): Effect.Effect<readonly StrategyBenchmarkSummary[], never> {
15
+ return Effect.gen(function* () {
16
+ const summaries: StrategyBenchmarkSummary[] = [];
17
+ for (const strategy of config.strategies) {
18
+ const results = yield* Effect.forEach(
19
+ config.chunks,
20
+ (chunk: TranslationContext) => runBenchmarkedChunk(strategy, chunk),
21
+ { concurrency: config.concurrency },
22
+ );
23
+ summaries.push(summarizeBenchmarkResults(results));
24
+ }
25
+ return summaries;
26
+ });
27
+ }