dialekt 0.1.0 → 0.1.1
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 +8 -10
- package/TESTING.md +29 -29
- package/dist/cli/main.d.mts +1 -1
- package/dist/cli/main.mjs +549 -362
- package/dist/formatters-De4Q-X1d.mjs +516 -435
- package/dist/index.d.mts +162 -25
- package/dist/index.mjs +119 -34
- package/package.json +3 -3
- package/pnpm-workspace.yaml +3 -3
- package/src/adapter/types.test.ts +57 -57
- package/src/adapter/types.ts +7 -4
- package/src/benchmark/metrics.test.ts +141 -69
- package/src/benchmark/metrics.ts +6 -6
- package/src/benchmark/report.test.ts +38 -38
- package/src/benchmark/report.ts +6 -6
- package/src/benchmark/runner.test.ts +70 -72
- package/src/benchmark/runner.ts +4 -4
- package/src/cli/commands/add.test.ts +90 -109
- package/src/cli/commands/add.ts +40 -28
- package/src/cli/commands/benchmark.test.ts +77 -64
- package/src/cli/commands/benchmark.ts +64 -41
- package/src/cli/commands/languages.test.ts +45 -42
- package/src/cli/commands/languages.ts +16 -12
- package/src/cli/commands/missing.test.ts +143 -92
- package/src/cli/commands/missing.ts +24 -17
- package/src/cli/commands/translate.test.ts +79 -79
- package/src/cli/commands/translate.ts +41 -31
- package/src/cli/commands/unused.test.ts +62 -51
- package/src/cli/commands/unused.ts +18 -14
- package/src/cli/commands/validate.test.ts +130 -72
- package/src/cli/commands/validate.ts +25 -20
- package/src/cli/config-resolution.test.ts +169 -49
- package/src/cli/config-resolution.ts +5 -7
- package/src/cli/format.test.ts +50 -50
- package/src/cli/format.ts +57 -60
- package/src/cli/formatters.test.ts +128 -106
- package/src/cli/formatters.ts +72 -95
- package/src/cli/main.ts +13 -13
- package/src/config/define-config.test.ts +44 -29
- package/src/config/define-config.ts +1 -1
- package/src/config/load-config.test.ts +21 -18
- package/src/config/load-config.ts +5 -5
- package/src/config/types.test.ts +50 -44
- package/src/config/types.ts +2 -2
- package/src/index.ts +22 -26
- package/src/keys/flatten.test.ts +52 -52
- package/src/keys/flatten.ts +7 -9
- package/src/sdk/file-io.test.ts +47 -59
- package/src/sdk/file-io.ts +2 -2
- package/src/sdk/node-layer.test.ts +18 -18
- package/src/sdk/node-layer.ts +2 -2
- package/src/sdk/php-array-reader.test.ts +49 -40
- package/src/sdk/php-array-reader.ts +5 -5
- package/src/translation/chunking.test.ts +52 -44
- package/src/translation/chunking.ts +1 -1
- package/src/translation/missing-keys.test.ts +86 -93
- package/src/translation/missing-keys.ts +4 -6
- package/src/translation/model-registry.test.ts +41 -32
- package/src/translation/model-registry.ts +9 -9
- package/src/translation/one-shot-strategy.test.ts +105 -86
- package/src/translation/one-shot-strategy.ts +10 -12
- package/src/translation/orchestrator.test.ts +90 -101
- package/src/translation/orchestrator.ts +26 -26
- package/src/translation/prompt.test.ts +76 -76
- package/src/translation/prompt.ts +2 -2
- package/src/translation/tool-loop-strategy.test.ts +134 -107
- package/src/translation/tool-loop-strategy.ts +14 -18
- package/src/translation/types.test.ts +22 -22
- package/src/translation/types.ts +3 -3
- package/tsdown.config.ts +3 -3
- package/vitest.config.ts +3 -3
|
@@ -12,7 +12,7 @@ Rules:
|
|
|
12
12
|
- Escape double quotes in translations with a backslash when needed.`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
import type { TranslationContext } from
|
|
15
|
+
import type { TranslationContext } from "./types.js";
|
|
16
16
|
|
|
17
17
|
export function buildUserPrompt(ctx: TranslationContext): string {
|
|
18
18
|
const sourceJson = JSON.stringify(ctx.sourceMap, null, 2);
|
|
@@ -20,7 +20,7 @@ export function buildUserPrompt(ctx: TranslationContext): string {
|
|
|
20
20
|
|
|
21
21
|
const keysWithValues: Record<string, string> = {};
|
|
22
22
|
for (const key of ctx.keys) {
|
|
23
|
-
keysWithValues[key] = ctx.sourceMap[key] ??
|
|
23
|
+
keysWithValues[key] = ctx.sourceMap[key] ?? "";
|
|
24
24
|
}
|
|
25
25
|
const keysJson = JSON.stringify(keysWithValues, null, 2);
|
|
26
26
|
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { Effect, Either } from
|
|
3
|
-
import { MockLanguageModelV3 } from
|
|
4
|
-
import { createToolLoopStrategy } from
|
|
5
|
-
import { TranslationFailedError } from
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Effect, Either } from "effect";
|
|
3
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
4
|
+
import { createToolLoopStrategy } from "./tool-loop-strategy.js";
|
|
5
|
+
import { TranslationFailedError } from "./types.js";
|
|
6
6
|
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
7
|
+
describe("createToolLoopStrategy", () => {
|
|
8
|
+
it("returns translated map when tool is called", async () => {
|
|
9
9
|
const model = new MockLanguageModelV3({
|
|
10
10
|
doGenerate: async () => ({
|
|
11
|
-
text:
|
|
11
|
+
text: "",
|
|
12
12
|
content: [
|
|
13
13
|
{
|
|
14
|
-
type:
|
|
15
|
-
toolCallId:
|
|
16
|
-
toolName:
|
|
17
|
-
input: JSON.stringify({ hello:
|
|
14
|
+
type: "tool-call",
|
|
15
|
+
toolCallId: "call-1",
|
|
16
|
+
toolName: "submitTranslations",
|
|
17
|
+
input: JSON.stringify({ hello: "Hallo", bye: "Tschüss" }),
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
|
-
finishReason: { unified:
|
|
20
|
+
finishReason: { unified: "tool-calls" as const, raw: "tool_calls" },
|
|
21
21
|
usage: {
|
|
22
22
|
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
|
|
23
23
|
outputTokens: { total: 4, text: 0, reasoning: 0 },
|
|
24
24
|
},
|
|
25
25
|
response: {
|
|
26
|
-
modelId:
|
|
26
|
+
modelId: "mock",
|
|
27
27
|
timestamp: new Date(),
|
|
28
28
|
},
|
|
29
29
|
request: { body: {} },
|
|
@@ -37,32 +37,32 @@ describe('createToolLoopStrategy', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
const ctx = {
|
|
40
|
-
sourceLocale:
|
|
41
|
-
targetLocale:
|
|
42
|
-
sourceMap: { hello:
|
|
40
|
+
sourceLocale: "en",
|
|
41
|
+
targetLocale: "de",
|
|
42
|
+
sourceMap: { hello: "Hello", bye: "Bye" },
|
|
43
43
|
targetMap: {},
|
|
44
|
-
keys: [
|
|
44
|
+
keys: ["hello", "bye"],
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const result = await Effect.runPromise(strategy.translateChunk(ctx));
|
|
48
|
-
expect(result).toEqual({ hello:
|
|
48
|
+
expect(result).toEqual({ hello: "Hallo", bye: "Tschüss" });
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
it(
|
|
51
|
+
it("retries when tool is not called and fails after exhaustion", async () => {
|
|
52
52
|
let calls = 0;
|
|
53
53
|
const model = new MockLanguageModelV3({
|
|
54
54
|
doGenerate: async () => {
|
|
55
55
|
calls++;
|
|
56
56
|
return {
|
|
57
|
-
text:
|
|
58
|
-
content: [{ type:
|
|
59
|
-
finishReason: { unified:
|
|
57
|
+
text: "",
|
|
58
|
+
content: [{ type: "text" as const, text: "I will translate now" }],
|
|
59
|
+
finishReason: { unified: "stop" as const, raw: "stop" },
|
|
60
60
|
usage: {
|
|
61
61
|
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
|
|
62
62
|
outputTokens: { total: 2, text: 2, reasoning: 0 },
|
|
63
63
|
},
|
|
64
64
|
response: {
|
|
65
|
-
modelId:
|
|
65
|
+
modelId: "mock",
|
|
66
66
|
timestamp: new Date(),
|
|
67
67
|
},
|
|
68
68
|
request: { body: {} },
|
|
@@ -77,35 +77,42 @@ describe('createToolLoopStrategy', () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
const ctx = {
|
|
80
|
-
sourceLocale:
|
|
81
|
-
targetLocale:
|
|
82
|
-
sourceMap: { hello:
|
|
80
|
+
sourceLocale: "en",
|
|
81
|
+
targetLocale: "de",
|
|
82
|
+
sourceMap: { hello: "Hello", bye: "Bye" },
|
|
83
83
|
targetMap: {},
|
|
84
|
-
keys: [
|
|
84
|
+
keys: ["hello", "bye"],
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
-
const exit = await Effect.runPromise(
|
|
87
|
+
const exit = (await Effect.runPromise(
|
|
88
|
+
Effect.either(strategy.translateChunk(ctx)),
|
|
89
|
+
)) as Either.Either<unknown, TranslationFailedError>;
|
|
88
90
|
expect(calls).toBeGreaterThan(1);
|
|
89
|
-
if (exit._tag ===
|
|
90
|
-
expect(exit.left._tag).toBe(
|
|
91
|
+
if (exit._tag === "Left") {
|
|
92
|
+
expect(exit.left._tag).toBe("TranslationFailedError");
|
|
91
93
|
} else {
|
|
92
|
-
throw new Error(
|
|
94
|
+
throw new Error("Expected Left");
|
|
93
95
|
}
|
|
94
96
|
});
|
|
95
97
|
|
|
96
|
-
it(
|
|
98
|
+
it("handles empty keys", async () => {
|
|
97
99
|
const model = new MockLanguageModelV3({
|
|
98
100
|
doGenerate: async () => ({
|
|
99
|
-
text:
|
|
100
|
-
content: [
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
text: "",
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: "tool-call",
|
|
105
|
+
toolCallId: "call-1",
|
|
106
|
+
toolName: "submitTranslations",
|
|
107
|
+
input: JSON.stringify({}),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
finishReason: { unified: "tool-calls" as const, raw: "tool_calls" },
|
|
111
|
+
usage: {
|
|
112
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
113
|
+
outputTokens: { total: 1, text: 0, reasoning: 0 },
|
|
114
|
+
},
|
|
115
|
+
response: { modelId: "mock", timestamp: new Date() },
|
|
109
116
|
request: { body: {} },
|
|
110
117
|
warnings: [],
|
|
111
118
|
}),
|
|
@@ -117,8 +124,8 @@ describe('createToolLoopStrategy', () => {
|
|
|
117
124
|
});
|
|
118
125
|
|
|
119
126
|
const ctx = {
|
|
120
|
-
sourceLocale:
|
|
121
|
-
targetLocale:
|
|
127
|
+
sourceLocale: "en",
|
|
128
|
+
targetLocale: "de",
|
|
122
129
|
sourceMap: {},
|
|
123
130
|
targetMap: {},
|
|
124
131
|
keys: [],
|
|
@@ -128,22 +135,27 @@ describe('createToolLoopStrategy', () => {
|
|
|
128
135
|
expect(result).toEqual({});
|
|
129
136
|
});
|
|
130
137
|
|
|
131
|
-
it(
|
|
138
|
+
it("handles wrong tool name by retrying", async () => {
|
|
132
139
|
let calls = 0;
|
|
133
140
|
const model = new MockLanguageModelV3({
|
|
134
141
|
doGenerate: async () => {
|
|
135
142
|
calls++;
|
|
136
143
|
return {
|
|
137
|
-
text:
|
|
138
|
-
content: [
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
text: "",
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "tool-call",
|
|
148
|
+
toolCallId: "call-1",
|
|
149
|
+
toolName: calls < 2 ? "wrongTool" : "submitTranslations",
|
|
150
|
+
input: JSON.stringify({ k: "v" }),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
finishReason: { unified: "tool-calls" as const, raw: "tool_calls" },
|
|
154
|
+
usage: {
|
|
155
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
156
|
+
outputTokens: { total: 1, text: 0, reasoning: 0 },
|
|
157
|
+
},
|
|
158
|
+
response: { modelId: "mock", timestamp: new Date() },
|
|
147
159
|
request: { body: {} },
|
|
148
160
|
warnings: [],
|
|
149
161
|
};
|
|
@@ -156,34 +168,39 @@ describe('createToolLoopStrategy', () => {
|
|
|
156
168
|
});
|
|
157
169
|
|
|
158
170
|
const ctx = {
|
|
159
|
-
sourceLocale:
|
|
160
|
-
targetLocale:
|
|
161
|
-
sourceMap: { k:
|
|
171
|
+
sourceLocale: "en",
|
|
172
|
+
targetLocale: "de",
|
|
173
|
+
sourceMap: { k: "K" },
|
|
162
174
|
targetMap: {},
|
|
163
|
-
keys: [
|
|
175
|
+
keys: ["k"],
|
|
164
176
|
};
|
|
165
177
|
|
|
166
178
|
const result = await Effect.runPromise(strategy.translateChunk(ctx));
|
|
167
|
-
expect(result).toEqual({ k:
|
|
179
|
+
expect(result).toEqual({ k: "v" });
|
|
168
180
|
expect(calls).toBeGreaterThan(1);
|
|
169
181
|
});
|
|
170
182
|
|
|
171
|
-
it(
|
|
183
|
+
it("handles malformed tool input by retrying", async () => {
|
|
172
184
|
let calls = 0;
|
|
173
185
|
const model = new MockLanguageModelV3({
|
|
174
186
|
doGenerate: async () => {
|
|
175
187
|
calls++;
|
|
176
188
|
return {
|
|
177
|
-
text:
|
|
178
|
-
content: [
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
text: "",
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "tool-call",
|
|
193
|
+
toolCallId: "call-1",
|
|
194
|
+
toolName: "submitTranslations",
|
|
195
|
+
input: calls < 2 ? "not-json" : JSON.stringify({ k: "v" }),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
finishReason: { unified: "tool-calls" as const, raw: "tool_calls" },
|
|
199
|
+
usage: {
|
|
200
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
201
|
+
outputTokens: { total: 1, text: 0, reasoning: 0 },
|
|
202
|
+
},
|
|
203
|
+
response: { modelId: "mock", timestamp: new Date() },
|
|
187
204
|
request: { body: {} },
|
|
188
205
|
warnings: [],
|
|
189
206
|
};
|
|
@@ -196,26 +213,29 @@ describe('createToolLoopStrategy', () => {
|
|
|
196
213
|
});
|
|
197
214
|
|
|
198
215
|
const ctx = {
|
|
199
|
-
sourceLocale:
|
|
200
|
-
targetLocale:
|
|
201
|
-
sourceMap: { k:
|
|
216
|
+
sourceLocale: "en",
|
|
217
|
+
targetLocale: "de",
|
|
218
|
+
sourceMap: { k: "K" },
|
|
202
219
|
targetMap: {},
|
|
203
|
-
keys: [
|
|
220
|
+
keys: ["k"],
|
|
204
221
|
};
|
|
205
222
|
|
|
206
223
|
const result = await Effect.runPromise(strategy.translateChunk(ctx));
|
|
207
|
-
expect(result).toEqual({ k:
|
|
224
|
+
expect(result).toEqual({ k: "v" });
|
|
208
225
|
expect(calls).toBeGreaterThan(1);
|
|
209
226
|
});
|
|
210
227
|
|
|
211
|
-
it(
|
|
228
|
+
it("fails after all retries when tool never called", async () => {
|
|
212
229
|
const model = new MockLanguageModelV3({
|
|
213
230
|
doGenerate: async () => ({
|
|
214
|
-
text:
|
|
215
|
-
content: [{ type:
|
|
216
|
-
finishReason: { unified:
|
|
217
|
-
usage: {
|
|
218
|
-
|
|
231
|
+
text: "",
|
|
232
|
+
content: [{ type: "text", text: "I will translate now" }],
|
|
233
|
+
finishReason: { unified: "stop" as const, raw: "stop" },
|
|
234
|
+
usage: {
|
|
235
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
236
|
+
outputTokens: { total: 1, text: 1, reasoning: 0 },
|
|
237
|
+
},
|
|
238
|
+
response: { modelId: "mock", timestamp: new Date() },
|
|
219
239
|
request: { body: {} },
|
|
220
240
|
warnings: [],
|
|
221
241
|
}),
|
|
@@ -227,34 +247,41 @@ describe('createToolLoopStrategy', () => {
|
|
|
227
247
|
});
|
|
228
248
|
|
|
229
249
|
const ctx = {
|
|
230
|
-
sourceLocale:
|
|
231
|
-
targetLocale:
|
|
232
|
-
sourceMap: { k:
|
|
250
|
+
sourceLocale: "en",
|
|
251
|
+
targetLocale: "de",
|
|
252
|
+
sourceMap: { k: "K" },
|
|
233
253
|
targetMap: {},
|
|
234
|
-
keys: [
|
|
254
|
+
keys: ["k"],
|
|
235
255
|
};
|
|
236
256
|
|
|
237
|
-
const exit = await Effect.runPromise(
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
const exit = (await Effect.runPromise(
|
|
258
|
+
Effect.either(strategy.translateChunk(ctx)),
|
|
259
|
+
)) as Either.Either<unknown, TranslationFailedError>;
|
|
260
|
+
if (exit._tag === "Left") {
|
|
261
|
+
expect(exit.left._tag).toBe("TranslationFailedError");
|
|
240
262
|
} else {
|
|
241
|
-
throw new Error(
|
|
263
|
+
throw new Error("Expected Left");
|
|
242
264
|
}
|
|
243
265
|
});
|
|
244
266
|
|
|
245
|
-
it(
|
|
267
|
+
it("handles single key", async () => {
|
|
246
268
|
const model = new MockLanguageModelV3({
|
|
247
269
|
doGenerate: async () => ({
|
|
248
|
-
text:
|
|
249
|
-
content: [
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
270
|
+
text: "",
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "tool-call",
|
|
274
|
+
toolCallId: "call-1",
|
|
275
|
+
toolName: "submitTranslations",
|
|
276
|
+
input: JSON.stringify({ greeting: "Hallo" }),
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
finishReason: { unified: "tool-calls" as const, raw: "tool_calls" },
|
|
280
|
+
usage: {
|
|
281
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
282
|
+
outputTokens: { total: 1, text: 0, reasoning: 0 },
|
|
283
|
+
},
|
|
284
|
+
response: { modelId: "mock", timestamp: new Date() },
|
|
258
285
|
request: { body: {} },
|
|
259
286
|
warnings: [],
|
|
260
287
|
}),
|
|
@@ -266,14 +293,14 @@ describe('createToolLoopStrategy', () => {
|
|
|
266
293
|
});
|
|
267
294
|
|
|
268
295
|
const ctx = {
|
|
269
|
-
sourceLocale:
|
|
270
|
-
targetLocale:
|
|
271
|
-
sourceMap: { greeting:
|
|
296
|
+
sourceLocale: "en",
|
|
297
|
+
targetLocale: "de",
|
|
298
|
+
sourceMap: { greeting: "Hello" },
|
|
272
299
|
targetMap: {},
|
|
273
|
-
keys: [
|
|
300
|
+
keys: ["greeting"],
|
|
274
301
|
};
|
|
275
302
|
|
|
276
303
|
const result = await Effect.runPromise(strategy.translateChunk(ctx));
|
|
277
|
-
expect(result).toEqual({ greeting:
|
|
304
|
+
expect(result).toEqual({ greeting: "Hallo" });
|
|
278
305
|
});
|
|
279
306
|
});
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import { ToolLoopAgent, tool, hasToolCall } from
|
|
2
|
-
import { Effect, Schedule } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import type { LanguageModel } from
|
|
5
|
-
import type { TranslationContext, TranslationStrategy } from
|
|
6
|
-
import { TranslationFailedError } from
|
|
7
|
-
import { buildSystemPrompt, buildUserPrompt } from
|
|
1
|
+
import { ToolLoopAgent, tool, hasToolCall } from "ai";
|
|
2
|
+
import { Effect, Schedule } from "effect";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { LanguageModel } from "ai";
|
|
5
|
+
import type { TranslationContext, TranslationStrategy } from "./types.js";
|
|
6
|
+
import { TranslationFailedError } from "./types.js";
|
|
7
|
+
import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
|
|
8
8
|
|
|
9
9
|
async function tryTranslateChunk(
|
|
10
10
|
model: LanguageModel,
|
|
11
11
|
ctx: TranslationContext,
|
|
12
12
|
): Promise<Record<string, string>> {
|
|
13
|
-
const schema = z.object(
|
|
14
|
-
Object.fromEntries(ctx.keys.map((key: string) => [key, z.string()])),
|
|
15
|
-
);
|
|
13
|
+
const schema = z.object(Object.fromEntries(ctx.keys.map((key: string) => [key, z.string()])));
|
|
16
14
|
|
|
17
15
|
let captured: Record<string, string> | null = null;
|
|
18
16
|
|
|
19
17
|
const submitTranslations = tool({
|
|
20
18
|
description:
|
|
21
|
-
|
|
19
|
+
"Submit the final translations for every requested key. Call this exactly once, with every key filled in.",
|
|
22
20
|
inputSchema: schema,
|
|
23
21
|
execute: async (input) => {
|
|
24
22
|
captured = input as Record<string, string>;
|
|
@@ -30,18 +28,16 @@ async function tryTranslateChunk(
|
|
|
30
28
|
model,
|
|
31
29
|
instructions: buildSystemPrompt(ctx.sourceLocale, ctx.targetLocale),
|
|
32
30
|
tools: { submitTranslations },
|
|
33
|
-
stopWhen: hasToolCall(
|
|
31
|
+
stopWhen: hasToolCall("submitTranslations"),
|
|
34
32
|
});
|
|
35
33
|
|
|
36
34
|
await agent.generate({ prompt: buildUserPrompt(ctx) });
|
|
37
35
|
if (captured === null) {
|
|
38
|
-
throw new Error(
|
|
36
|
+
throw new Error("Agent finished without calling submitTranslations");
|
|
39
37
|
}
|
|
40
|
-
const missing = ctx.keys.filter(
|
|
41
|
-
(key: string) => !(key in (captured as Record<string, string>)),
|
|
42
|
-
);
|
|
38
|
+
const missing = ctx.keys.filter((key: string) => !(key in (captured as Record<string, string>)));
|
|
43
39
|
if (missing.length > 0) {
|
|
44
|
-
throw new Error(`Model omitted keys: ${missing.join(
|
|
40
|
+
throw new Error(`Model omitted keys: ${missing.join(", ")}`);
|
|
45
41
|
}
|
|
46
42
|
return captured;
|
|
47
43
|
}
|
|
@@ -51,7 +47,7 @@ export function createToolLoopStrategy(deps: {
|
|
|
51
47
|
retry: { maxAttempts: number; baseDelayMs: number };
|
|
52
48
|
}): TranslationStrategy {
|
|
53
49
|
return {
|
|
54
|
-
name:
|
|
50
|
+
name: "tool-loop-agent",
|
|
55
51
|
translateChunk: (ctx: TranslationContext) =>
|
|
56
52
|
Effect.tryPromise({
|
|
57
53
|
try: () => tryTranslateChunk(deps.model, ctx),
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { TranslationFailedError } from
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TranslationFailedError } from "./types.js";
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
6
|
-
const err = new TranslationFailedError({ keys: [
|
|
7
|
-
expect(err._tag).toBe(
|
|
8
|
-
expect(err.keys).toEqual([
|
|
4
|
+
describe("TranslationFailedError", () => {
|
|
5
|
+
it("carries keys and cause", () => {
|
|
6
|
+
const err = new TranslationFailedError({ keys: ["a"], cause: new Error("boom") });
|
|
7
|
+
expect(err._tag).toBe("TranslationFailedError");
|
|
8
|
+
expect(err.keys).toEqual(["a"]);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
it(
|
|
12
|
-
const err = new TranslationFailedError({ keys: [
|
|
13
|
-
expect(err.keys).toEqual([
|
|
11
|
+
it("carries multiple keys", () => {
|
|
12
|
+
const err = new TranslationFailedError({ keys: ["a", "b", "c"], cause: "network timeout" });
|
|
13
|
+
expect(err.keys).toEqual(["a", "b", "c"]);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
it(
|
|
17
|
-
const err = new TranslationFailedError({ keys: [
|
|
18
|
-
expect(err._tag).toBe(
|
|
16
|
+
it("accepts string cause", () => {
|
|
17
|
+
const err = new TranslationFailedError({ keys: ["x"], cause: "rate limited" });
|
|
18
|
+
expect(err._tag).toBe("TranslationFailedError");
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it(
|
|
22
|
-
const cause = new Error(
|
|
23
|
-
const err = new TranslationFailedError({ keys: [
|
|
24
|
-
expect(err._tag).toBe(
|
|
21
|
+
it("accepts Error cause", () => {
|
|
22
|
+
const cause = new Error("model rejected");
|
|
23
|
+
const err = new TranslationFailedError({ keys: ["y"], cause });
|
|
24
|
+
expect(err._tag).toBe("TranslationFailedError");
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it(
|
|
28
|
-
const err = new TranslationFailedError({ keys: [], cause:
|
|
27
|
+
it("accepts empty keys array", () => {
|
|
28
|
+
const err = new TranslationFailedError({ keys: [], cause: "unknown" });
|
|
29
29
|
expect(err.keys).toEqual([]);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it(
|
|
33
|
-
const cause = new Error(
|
|
34
|
-
const err = new TranslationFailedError({ keys: [
|
|
32
|
+
it("preserves cause object identity when Error", () => {
|
|
33
|
+
const cause = new Error("specific");
|
|
34
|
+
const err = new TranslationFailedError({ keys: ["z"], cause });
|
|
35
35
|
expect(err.cause).toBe(cause);
|
|
36
36
|
});
|
|
37
37
|
});
|
package/src/translation/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Data, Effect } from
|
|
1
|
+
import { Data, Effect } from "effect";
|
|
2
2
|
|
|
3
3
|
export interface TranslationContext {
|
|
4
4
|
readonly sourceLocale: string;
|
|
@@ -8,13 +8,13 @@ export interface TranslationContext {
|
|
|
8
8
|
readonly keys: readonly string[]; // the specific keys to translate in this call
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export class TranslationFailedError extends Data.TaggedError(
|
|
11
|
+
export class TranslationFailedError extends Data.TaggedError("TranslationFailedError")<{
|
|
12
12
|
readonly keys: readonly string[];
|
|
13
13
|
readonly cause: unknown;
|
|
14
14
|
}> {}
|
|
15
15
|
|
|
16
16
|
export interface TranslationStrategy {
|
|
17
|
-
readonly name:
|
|
17
|
+
readonly name: "one-shot" | "tool-loop-agent";
|
|
18
18
|
translateChunk(
|
|
19
19
|
ctx: TranslationContext,
|
|
20
20
|
): Effect.Effect<Record<string, string>, TranslationFailedError>;
|
package/tsdown.config.ts
CHANGED
package/vitest.config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
test: {
|
|
5
|
-
include: [
|
|
6
|
-
environment:
|
|
5
|
+
include: ["src/**/*.test.ts"],
|
|
6
|
+
environment: "node",
|
|
7
7
|
},
|
|
8
8
|
});
|