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
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { Effect, Either } from
|
|
3
|
-
import { runTranslation } from
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Effect, Either } from "effect";
|
|
3
|
+
import { runTranslation } from "./orchestrator.js";
|
|
4
|
+
import type {
|
|
5
|
+
TranslationAdapter,
|
|
6
|
+
ResourceRef,
|
|
7
|
+
AdapterReadError,
|
|
8
|
+
AdapterWriteError,
|
|
9
|
+
} from "../adapter/types.js";
|
|
10
|
+
import { TranslationFailedError } from "./types.js";
|
|
11
|
+
|
|
12
|
+
describe("runTranslation", () => {
|
|
13
|
+
it("translates missing keys across adapters and writes merged results", async () => {
|
|
9
14
|
const writes: Array<{ locale: string; resource: string; entries: Record<string, string> }> = [];
|
|
10
15
|
|
|
11
16
|
const adapter: TranslationAdapter = {
|
|
12
|
-
name:
|
|
17
|
+
name: "test",
|
|
13
18
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
14
|
-
listLocales: () => Effect.succeed([
|
|
15
|
-
listResources: () =>
|
|
16
|
-
Effect.succeed([{ key: 'messages', label: 'messages' }]),
|
|
19
|
+
listLocales: () => Effect.succeed(["en", "de"]),
|
|
20
|
+
listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
|
|
17
21
|
readResource: (locale: string) =>
|
|
18
|
-
Effect.succeed(
|
|
19
|
-
locale === 'en'
|
|
20
|
-
? { hello: 'Hello', bye: 'Bye' }
|
|
21
|
-
: { hello: 'Hallo' },
|
|
22
|
-
),
|
|
22
|
+
Effect.succeed(locale === "en" ? { hello: "Hello", bye: "Bye" } : { hello: "Hallo" }),
|
|
23
23
|
writeResource: (locale: string, resource: ResourceRef, entries: Record<string, string>) =>
|
|
24
24
|
Effect.sync(() => {
|
|
25
25
|
writes.push({ locale, resource: resource.key, entries });
|
|
@@ -27,7 +27,7 @@ describe('runTranslation', () => {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const strategy = {
|
|
30
|
-
name:
|
|
30
|
+
name: "one-shot" as const,
|
|
31
31
|
translateChunk: (ctx: { keys: readonly string[]; targetLocale: string }) =>
|
|
32
32
|
Effect.succeed(
|
|
33
33
|
Object.fromEntries(ctx.keys.map((k: string) => [k, `${ctx.targetLocale}:${k}`])),
|
|
@@ -37,43 +37,38 @@ describe('runTranslation', () => {
|
|
|
37
37
|
const program = runTranslation({
|
|
38
38
|
adapters: [adapter],
|
|
39
39
|
strategy,
|
|
40
|
-
sourceLocale:
|
|
41
|
-
targetLocales: [
|
|
40
|
+
sourceLocale: "en",
|
|
41
|
+
targetLocales: ["de"],
|
|
42
42
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
await Effect.runPromise(program);
|
|
46
46
|
expect(writes).toHaveLength(1);
|
|
47
|
-
expect(writes[0]!.locale).toBe(
|
|
48
|
-
expect(writes[0]!.entries).toEqual({ hello:
|
|
47
|
+
expect(writes[0]!.locale).toBe("de");
|
|
48
|
+
expect(writes[0]!.entries).toEqual({ hello: "Hallo", bye: "de:bye" });
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
it(
|
|
51
|
+
it("respects concurrency limit", async () => {
|
|
52
52
|
let maxConcurrent = 0;
|
|
53
53
|
let current = 0;
|
|
54
54
|
|
|
55
55
|
const adapter: TranslationAdapter = {
|
|
56
|
-
name:
|
|
56
|
+
name: "test",
|
|
57
57
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
58
|
-
listLocales: () => Effect.succeed([
|
|
59
|
-
listResources: () =>
|
|
60
|
-
Effect.succeed([{ key: 'messages', label: 'messages' }]),
|
|
58
|
+
listLocales: () => Effect.succeed(["en", "de"]),
|
|
59
|
+
listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
|
|
61
60
|
readResource: (locale: string) =>
|
|
62
|
-
Effect.succeed(
|
|
63
|
-
locale === 'en'
|
|
64
|
-
? { a: 'A', b: 'B', c: 'C', d: 'D' }
|
|
65
|
-
: {},
|
|
66
|
-
),
|
|
61
|
+
Effect.succeed(locale === "en" ? { a: "A", b: "B", c: "C", d: "D" } : {}),
|
|
67
62
|
writeResource: () => Effect.void,
|
|
68
63
|
};
|
|
69
64
|
|
|
70
65
|
const strategy = {
|
|
71
|
-
name:
|
|
66
|
+
name: "one-shot" as const,
|
|
72
67
|
translateChunk: (ctx: { keys: readonly string[] }) =>
|
|
73
68
|
Effect.gen(function* () {
|
|
74
69
|
current++;
|
|
75
70
|
maxConcurrent = Math.max(maxConcurrent, current);
|
|
76
|
-
yield* Effect.sleep(
|
|
71
|
+
yield* Effect.sleep("50 millis");
|
|
77
72
|
current--;
|
|
78
73
|
return Object.fromEntries(ctx.keys.map((k: string) => [k, k]));
|
|
79
74
|
}),
|
|
@@ -82,8 +77,8 @@ describe('runTranslation', () => {
|
|
|
82
77
|
const program = runTranslation({
|
|
83
78
|
adapters: [adapter],
|
|
84
79
|
strategy,
|
|
85
|
-
sourceLocale:
|
|
86
|
-
targetLocales: [
|
|
80
|
+
sourceLocale: "en",
|
|
81
|
+
targetLocales: ["de"],
|
|
87
82
|
chunking: { maxTokens: 10, charsPerToken: 3.0, concurrency: 2 },
|
|
88
83
|
});
|
|
89
84
|
|
|
@@ -91,55 +86,51 @@ describe('runTranslation', () => {
|
|
|
91
86
|
expect(maxConcurrent).toBeLessThanOrEqual(2);
|
|
92
87
|
});
|
|
93
88
|
|
|
94
|
-
it(
|
|
89
|
+
it("collects failures and reports them at the end", async () => {
|
|
95
90
|
const adapter: TranslationAdapter = {
|
|
96
|
-
name:
|
|
91
|
+
name: "test",
|
|
97
92
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
98
|
-
listLocales: () => Effect.succeed([
|
|
99
|
-
listResources: () =>
|
|
100
|
-
|
|
101
|
-
readResource: (locale: string) =>
|
|
102
|
-
Effect.succeed(
|
|
103
|
-
locale === 'en' ? { a: 'A' } : {},
|
|
104
|
-
),
|
|
93
|
+
listLocales: () => Effect.succeed(["en", "de"]),
|
|
94
|
+
listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
|
|
95
|
+
readResource: (locale: string) => Effect.succeed(locale === "en" ? { a: "A" } : {}),
|
|
105
96
|
writeResource: () => Effect.void,
|
|
106
97
|
};
|
|
107
98
|
|
|
108
99
|
const strategy = {
|
|
109
|
-
name:
|
|
110
|
-
translateChunk: () =>
|
|
111
|
-
Effect.fail(
|
|
112
|
-
new TranslationFailedError({ keys: ['a'], cause: 'boom' }),
|
|
113
|
-
),
|
|
100
|
+
name: "one-shot" as const,
|
|
101
|
+
translateChunk: () => Effect.fail(new TranslationFailedError({ keys: ["a"], cause: "boom" })),
|
|
114
102
|
};
|
|
115
103
|
|
|
116
104
|
const program = runTranslation({
|
|
117
105
|
adapters: [adapter],
|
|
118
106
|
strategy,
|
|
119
|
-
sourceLocale:
|
|
120
|
-
targetLocales: [
|
|
107
|
+
sourceLocale: "en",
|
|
108
|
+
targetLocales: ["de"],
|
|
121
109
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
122
110
|
});
|
|
123
111
|
|
|
124
|
-
const exit = await Effect.runPromise(Effect.either(program)) as Either.Either<
|
|
125
|
-
|
|
126
|
-
|
|
112
|
+
const exit = (await Effect.runPromise(Effect.either(program))) as Either.Either<
|
|
113
|
+
void,
|
|
114
|
+
TranslationFailedError
|
|
115
|
+
>;
|
|
116
|
+
if (exit._tag === "Left") {
|
|
117
|
+
expect((exit.left as TranslationFailedError)._tag).toBe("TranslationFailedError");
|
|
127
118
|
} else {
|
|
128
|
-
throw new Error(
|
|
119
|
+
throw new Error("Expected Left");
|
|
129
120
|
}
|
|
130
121
|
});
|
|
131
122
|
|
|
132
|
-
it(
|
|
123
|
+
it("handles empty adapters list", async () => {
|
|
133
124
|
const strategy = {
|
|
134
|
-
name:
|
|
125
|
+
name: "one-shot" as const,
|
|
135
126
|
translateChunk: () => Effect.succeed({}),
|
|
136
127
|
};
|
|
137
128
|
|
|
138
129
|
const program = runTranslation({
|
|
139
130
|
adapters: [],
|
|
140
131
|
strategy,
|
|
141
|
-
sourceLocale:
|
|
142
|
-
targetLocales: [
|
|
132
|
+
sourceLocale: "en",
|
|
133
|
+
targetLocales: ["de"],
|
|
143
134
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
144
135
|
});
|
|
145
136
|
|
|
@@ -147,26 +138,26 @@ describe('runTranslation', () => {
|
|
|
147
138
|
expect(true).toBe(true); // should not throw
|
|
148
139
|
});
|
|
149
140
|
|
|
150
|
-
it(
|
|
141
|
+
it("handles single-locale adapter (no targets)", async () => {
|
|
151
142
|
const adapter: TranslationAdapter = {
|
|
152
|
-
name:
|
|
143
|
+
name: "mono",
|
|
153
144
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
154
|
-
listLocales: () => Effect.succeed([
|
|
155
|
-
listResources: () => Effect.succeed([{ key:
|
|
156
|
-
readResource: () => Effect.succeed({ hello:
|
|
145
|
+
listLocales: () => Effect.succeed(["en"]),
|
|
146
|
+
listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
|
|
147
|
+
readResource: () => Effect.succeed({ hello: "Hello" }),
|
|
157
148
|
writeResource: () => Effect.void,
|
|
158
149
|
};
|
|
159
150
|
|
|
160
151
|
const strategy = {
|
|
161
|
-
name:
|
|
152
|
+
name: "one-shot" as const,
|
|
162
153
|
translateChunk: () => Effect.succeed({}),
|
|
163
154
|
};
|
|
164
155
|
|
|
165
156
|
const program = runTranslation({
|
|
166
157
|
adapters: [adapter],
|
|
167
158
|
strategy,
|
|
168
|
-
sourceLocale:
|
|
169
|
-
targetLocales: [
|
|
159
|
+
sourceLocale: "en",
|
|
160
|
+
targetLocales: ["de"],
|
|
170
161
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
171
162
|
});
|
|
172
163
|
|
|
@@ -174,21 +165,18 @@ describe('runTranslation', () => {
|
|
|
174
165
|
expect(true).toBe(true); // should not throw
|
|
175
166
|
});
|
|
176
167
|
|
|
177
|
-
it(
|
|
168
|
+
it("handles multiple resources", async () => {
|
|
178
169
|
const writes: Array<{ locale: string; resource: string }> = [];
|
|
179
170
|
const adapter: TranslationAdapter = {
|
|
180
|
-
name:
|
|
171
|
+
name: "multi",
|
|
181
172
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
182
|
-
listLocales: () => Effect.succeed([
|
|
173
|
+
listLocales: () => Effect.succeed(["en", "de"]),
|
|
183
174
|
listResources: () =>
|
|
184
175
|
Effect.succeed([
|
|
185
|
-
{ key:
|
|
186
|
-
{ key:
|
|
176
|
+
{ key: "auth", label: "auth" },
|
|
177
|
+
{ key: "validation", label: "validation" },
|
|
187
178
|
]),
|
|
188
|
-
readResource: (locale: string) =>
|
|
189
|
-
Effect.succeed(
|
|
190
|
-
locale === 'en' ? { k1: 'V1' } : {},
|
|
191
|
-
),
|
|
179
|
+
readResource: (locale: string) => Effect.succeed(locale === "en" ? { k1: "V1" } : {}),
|
|
192
180
|
writeResource: (locale: string, resource: ResourceRef) =>
|
|
193
181
|
Effect.sync(() => {
|
|
194
182
|
writes.push({ locale, resource: resource.key });
|
|
@@ -196,7 +184,7 @@ describe('runTranslation', () => {
|
|
|
196
184
|
};
|
|
197
185
|
|
|
198
186
|
const strategy = {
|
|
199
|
-
name:
|
|
187
|
+
name: "one-shot" as const,
|
|
200
188
|
translateChunk: (ctx: { keys: readonly string[] }) =>
|
|
201
189
|
Effect.succeed(Object.fromEntries(ctx.keys.map((k: string) => [k, k]))),
|
|
202
190
|
};
|
|
@@ -204,8 +192,8 @@ describe('runTranslation', () => {
|
|
|
204
192
|
const program = runTranslation({
|
|
205
193
|
adapters: [adapter],
|
|
206
194
|
strategy,
|
|
207
|
-
sourceLocale:
|
|
208
|
-
targetLocales: [
|
|
195
|
+
sourceLocale: "en",
|
|
196
|
+
targetLocales: ["de"],
|
|
209
197
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
210
198
|
});
|
|
211
199
|
|
|
@@ -213,41 +201,40 @@ describe('runTranslation', () => {
|
|
|
213
201
|
expect(writes.length).toBeGreaterThanOrEqual(2);
|
|
214
202
|
});
|
|
215
203
|
|
|
216
|
-
it(
|
|
204
|
+
it("handles adapter readResource failure", async () => {
|
|
217
205
|
const adapter: TranslationAdapter = {
|
|
218
|
-
name:
|
|
206
|
+
name: "broken",
|
|
219
207
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
220
|
-
listLocales: () => Effect.succeed([
|
|
221
|
-
listResources: () => Effect.succeed([{ key:
|
|
222
|
-
readResource: () => Effect.fail(new Error(
|
|
208
|
+
listLocales: () => Effect.succeed(["en", "de"]),
|
|
209
|
+
listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
|
|
210
|
+
readResource: () => Effect.fail(new Error("read failed") as AdapterReadError),
|
|
223
211
|
writeResource: () => Effect.void,
|
|
224
212
|
};
|
|
225
213
|
|
|
226
214
|
const strategy = {
|
|
227
|
-
name:
|
|
215
|
+
name: "one-shot" as const,
|
|
228
216
|
translateChunk: () => Effect.succeed({}),
|
|
229
217
|
};
|
|
230
218
|
|
|
231
219
|
const program = runTranslation({
|
|
232
220
|
adapters: [adapter],
|
|
233
221
|
strategy,
|
|
234
|
-
sourceLocale:
|
|
235
|
-
targetLocales: [
|
|
222
|
+
sourceLocale: "en",
|
|
223
|
+
targetLocales: ["de"],
|
|
236
224
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
237
225
|
});
|
|
238
226
|
|
|
239
|
-
await expect(Effect.runPromise(program)).rejects.toThrow(
|
|
227
|
+
await expect(Effect.runPromise(program)).rejects.toThrow("read failed");
|
|
240
228
|
});
|
|
241
229
|
|
|
242
|
-
it(
|
|
230
|
+
it("handles multiple target locales", async () => {
|
|
243
231
|
const writes: Array<{ locale: string }> = [];
|
|
244
232
|
const adapter: TranslationAdapter = {
|
|
245
|
-
name:
|
|
233
|
+
name: "multi-locale",
|
|
246
234
|
capabilities: { canCreateResource: true, unusedKeyDetection: false },
|
|
247
|
-
listLocales: () => Effect.succeed([
|
|
248
|
-
listResources: () => Effect.succeed([{ key:
|
|
249
|
-
readResource: (locale: string) =>
|
|
250
|
-
Effect.succeed(locale === 'en' ? { hello: 'Hello' } : {}),
|
|
235
|
+
listLocales: () => Effect.succeed(["en", "de", "fr"]),
|
|
236
|
+
listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
|
|
237
|
+
readResource: (locale: string) => Effect.succeed(locale === "en" ? { hello: "Hello" } : {}),
|
|
251
238
|
writeResource: (locale: string) =>
|
|
252
239
|
Effect.sync(() => {
|
|
253
240
|
writes.push({ locale });
|
|
@@ -255,22 +242,24 @@ describe('runTranslation', () => {
|
|
|
255
242
|
};
|
|
256
243
|
|
|
257
244
|
const strategy = {
|
|
258
|
-
name:
|
|
245
|
+
name: "one-shot" as const,
|
|
259
246
|
translateChunk: (ctx: { keys: readonly string[]; targetLocale: string }) =>
|
|
260
|
-
Effect.succeed(
|
|
247
|
+
Effect.succeed(
|
|
248
|
+
Object.fromEntries(ctx.keys.map((k: string) => [k, `${ctx.targetLocale}:${k}`])),
|
|
249
|
+
),
|
|
261
250
|
};
|
|
262
251
|
|
|
263
252
|
const program = runTranslation({
|
|
264
253
|
adapters: [adapter],
|
|
265
254
|
strategy,
|
|
266
|
-
sourceLocale:
|
|
267
|
-
targetLocales: [
|
|
255
|
+
sourceLocale: "en",
|
|
256
|
+
targetLocales: ["de", "fr"],
|
|
268
257
|
chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
|
|
269
258
|
});
|
|
270
259
|
|
|
271
260
|
await Effect.runPromise(program);
|
|
272
261
|
const locales = writes.map((w) => w.locale);
|
|
273
|
-
expect(locales).toContain(
|
|
274
|
-
expect(locales).toContain(
|
|
262
|
+
expect(locales).toContain("de");
|
|
263
|
+
expect(locales).toContain("fr");
|
|
275
264
|
});
|
|
276
265
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Effect } from
|
|
2
|
-
import { chunkKeys } from
|
|
3
|
-
import { diffKeys } from
|
|
4
|
-
import type { TranslationAdapter } from
|
|
5
|
-
import type { TranslationStrategy } from
|
|
6
|
-
import { TranslationFailedError } from
|
|
7
|
-
import type { ChunkingConfig } from
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { chunkKeys } from "./chunking.js";
|
|
3
|
+
import { diffKeys } from "../keys/flatten.js";
|
|
4
|
+
import type { TranslationAdapter } from "../adapter/types.js";
|
|
5
|
+
import type { TranslationStrategy } from "./types.js";
|
|
6
|
+
import { TranslationFailedError } from "./types.js";
|
|
7
|
+
import type { ChunkingConfig } from "../config/types.js";
|
|
8
8
|
|
|
9
9
|
export interface TranslationRunConfig {
|
|
10
10
|
readonly adapters: readonly TranslationAdapter[];
|
|
@@ -20,9 +20,7 @@ export function runTranslation(config: TranslationRunConfig) {
|
|
|
20
20
|
|
|
21
21
|
for (const adapter of config.adapters) {
|
|
22
22
|
const locales =
|
|
23
|
-
config.targetLocales.length > 0
|
|
24
|
-
? config.targetLocales
|
|
25
|
-
: yield* adapter.listLocales();
|
|
23
|
+
config.targetLocales.length > 0 ? config.targetLocales : yield* adapter.listLocales();
|
|
26
24
|
const sourceLocale = config.sourceLocale;
|
|
27
25
|
const targetLocales = locales.filter((l: string) => l !== sourceLocale);
|
|
28
26
|
|
|
@@ -41,22 +39,24 @@ export function runTranslation(config: TranslationRunConfig) {
|
|
|
41
39
|
|
|
42
40
|
const translatedChunks: Record<string, string>[] = [];
|
|
43
41
|
|
|
44
|
-
yield* Effect.forEach(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
yield* Effect.forEach(
|
|
43
|
+
chunks,
|
|
44
|
+
(chunkKeysArr) =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const result = yield* config.strategy.translateChunk({
|
|
47
|
+
sourceLocale,
|
|
48
|
+
targetLocale: locale,
|
|
49
|
+
sourceMap,
|
|
50
|
+
targetMap,
|
|
51
|
+
keys: chunkKeysArr,
|
|
52
|
+
});
|
|
53
|
+
translatedChunks.push(result);
|
|
54
|
+
}).pipe(
|
|
55
|
+
Effect.catchAll((err) => {
|
|
56
|
+
failures.push(err);
|
|
57
|
+
return Effect.void;
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
60
|
{ concurrency: config.chunking.concurrency, discard: true },
|
|
61
61
|
);
|
|
62
62
|
|
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { buildSystemPrompt, buildUserPrompt } from
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
6
|
-
const prompt = buildSystemPrompt(
|
|
7
|
-
expect(prompt).toContain(
|
|
8
|
-
expect(prompt).toContain(
|
|
9
|
-
expect(prompt).toContain(
|
|
4
|
+
describe("buildSystemPrompt", () => {
|
|
5
|
+
it("contains from and to language", () => {
|
|
6
|
+
const prompt = buildSystemPrompt("en", "de");
|
|
7
|
+
expect(prompt).toContain("en");
|
|
8
|
+
expect(prompt).toContain("de");
|
|
9
|
+
expect(prompt).toContain("Do not escape Unicode");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it(
|
|
13
|
-
const prompt = buildSystemPrompt(
|
|
14
|
-
expect(prompt).toContain(
|
|
15
|
-
expect(prompt).toContain(
|
|
16
|
-
expect(prompt).toContain(
|
|
12
|
+
it("mentions placeholder preservation", () => {
|
|
13
|
+
const prompt = buildSystemPrompt("en", "ja");
|
|
14
|
+
expect(prompt).toContain(":attribute");
|
|
15
|
+
expect(prompt).toContain(":min");
|
|
16
|
+
expect(prompt).toContain(":max");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it(
|
|
20
|
-
const prompt = buildSystemPrompt(
|
|
21
|
-
expect(prompt).toContain(
|
|
19
|
+
it("instructs to not add or remove keys", () => {
|
|
20
|
+
const prompt = buildSystemPrompt("en", "fr");
|
|
21
|
+
expect(prompt).toContain("Do not add or remove keys");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it(
|
|
25
|
-
const prompt = buildSystemPrompt(
|
|
26
|
-
expect(prompt).toContain(
|
|
24
|
+
it("mentions double quote escaping", () => {
|
|
25
|
+
const prompt = buildSystemPrompt("en", "es");
|
|
26
|
+
expect(prompt).toContain("double quotes");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it(
|
|
30
|
-
const prompt = buildSystemPrompt(
|
|
31
|
-
expect(prompt).toContain(
|
|
29
|
+
it("handles same-language translation edge case", () => {
|
|
30
|
+
const prompt = buildSystemPrompt("en", "en");
|
|
31
|
+
expect(prompt).toContain("en");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
it(
|
|
35
|
-
const prompt = buildSystemPrompt(
|
|
36
|
-
expect(prompt).toContain(
|
|
37
|
-
expect(prompt).toContain(
|
|
34
|
+
it("handles long locale codes", () => {
|
|
35
|
+
const prompt = buildSystemPrompt("zh-Hans", "zh-Hant");
|
|
36
|
+
expect(prompt).toContain("zh-Hans");
|
|
37
|
+
expect(prompt).toContain("zh-Hant");
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
describe(
|
|
42
|
-
it(
|
|
41
|
+
describe("buildUserPrompt", () => {
|
|
42
|
+
it("contains XML tags and key list", () => {
|
|
43
43
|
const ctx = {
|
|
44
|
-
sourceLocale:
|
|
45
|
-
targetLocale:
|
|
46
|
-
sourceMap: { hello:
|
|
44
|
+
sourceLocale: "en",
|
|
45
|
+
targetLocale: "de",
|
|
46
|
+
sourceMap: { hello: "Hello" },
|
|
47
47
|
targetMap: {},
|
|
48
|
-
keys: [
|
|
48
|
+
keys: ["hello"],
|
|
49
49
|
};
|
|
50
50
|
const prompt = buildUserPrompt(ctx);
|
|
51
|
-
expect(prompt).toContain(
|
|
52
|
-
expect(prompt).toContain(
|
|
53
|
-
expect(prompt).toContain(
|
|
54
|
-
expect(prompt).toContain(
|
|
55
|
-
expect(prompt).toContain(
|
|
51
|
+
expect(prompt).toContain("<source-file>");
|
|
52
|
+
expect(prompt).toContain("<existing-translations>");
|
|
53
|
+
expect(prompt).toContain("<keys-to-translate>");
|
|
54
|
+
expect(prompt).toContain("hello");
|
|
55
|
+
expect(prompt).toContain("Translate ALL keys");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
it(
|
|
58
|
+
it("includes existing translations for context", () => {
|
|
59
59
|
const ctx = {
|
|
60
|
-
sourceLocale:
|
|
61
|
-
targetLocale:
|
|
62
|
-
sourceMap: { a:
|
|
63
|
-
targetMap: { a:
|
|
64
|
-
keys: [
|
|
60
|
+
sourceLocale: "en",
|
|
61
|
+
targetLocale: "de",
|
|
62
|
+
sourceMap: { a: "A", b: "B" },
|
|
63
|
+
targetMap: { a: "already translated" },
|
|
64
|
+
keys: ["b"],
|
|
65
65
|
};
|
|
66
66
|
const prompt = buildUserPrompt(ctx);
|
|
67
|
-
expect(prompt).toContain(
|
|
67
|
+
expect(prompt).toContain("already translated");
|
|
68
68
|
expect(prompt).toContain('"b"');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it(
|
|
71
|
+
it("handles empty keys list", () => {
|
|
72
72
|
const ctx = {
|
|
73
|
-
sourceLocale:
|
|
74
|
-
targetLocale:
|
|
75
|
-
sourceMap: { hello:
|
|
73
|
+
sourceLocale: "en",
|
|
74
|
+
targetLocale: "de",
|
|
75
|
+
sourceMap: { hello: "Hello" },
|
|
76
76
|
targetMap: {},
|
|
77
77
|
keys: [],
|
|
78
78
|
};
|
|
79
79
|
const prompt = buildUserPrompt(ctx);
|
|
80
|
-
expect(prompt).toContain(
|
|
81
|
-
expect(prompt).toContain(
|
|
80
|
+
expect(prompt).toContain("<keys-to-translate>");
|
|
81
|
+
expect(prompt).toContain("{}");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
it(
|
|
84
|
+
it("handles keys with missing values in sourceMap", () => {
|
|
85
85
|
const ctx = {
|
|
86
|
-
sourceLocale:
|
|
87
|
-
targetLocale:
|
|
88
|
-
sourceMap: { hello:
|
|
86
|
+
sourceLocale: "en",
|
|
87
|
+
targetLocale: "de",
|
|
88
|
+
sourceMap: { hello: "Hello" },
|
|
89
89
|
targetMap: {},
|
|
90
|
-
keys: [
|
|
90
|
+
keys: ["hello", "missing"],
|
|
91
91
|
};
|
|
92
92
|
const prompt = buildUserPrompt(ctx);
|
|
93
93
|
expect(prompt).toContain('"missing"');
|
|
94
94
|
expect(prompt).toContain('""');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it(
|
|
97
|
+
it("handles special characters in values", () => {
|
|
98
98
|
const ctx = {
|
|
99
|
-
sourceLocale:
|
|
100
|
-
targetLocale:
|
|
101
|
-
sourceMap: { greeting:
|
|
99
|
+
sourceLocale: "en",
|
|
100
|
+
targetLocale: "ja",
|
|
101
|
+
sourceMap: { greeting: "Hello \n World \t!" },
|
|
102
102
|
targetMap: {},
|
|
103
|
-
keys: [
|
|
103
|
+
keys: ["greeting"],
|
|
104
104
|
};
|
|
105
105
|
const prompt = buildUserPrompt(ctx);
|
|
106
|
-
expect(prompt).toContain(
|
|
106
|
+
expect(prompt).toContain("Hello");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it(
|
|
109
|
+
it("handles unicode values", () => {
|
|
110
110
|
const ctx = {
|
|
111
|
-
sourceLocale:
|
|
112
|
-
targetLocale:
|
|
113
|
-
sourceMap: { emoji:
|
|
111
|
+
sourceLocale: "en",
|
|
112
|
+
targetLocale: "ja",
|
|
113
|
+
sourceMap: { emoji: "Hello 🌍" },
|
|
114
114
|
targetMap: {},
|
|
115
|
-
keys: [
|
|
115
|
+
keys: ["emoji"],
|
|
116
116
|
};
|
|
117
117
|
const prompt = buildUserPrompt(ctx);
|
|
118
|
-
expect(prompt).toContain(
|
|
118
|
+
expect(prompt).toContain("🌍");
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
it(
|
|
121
|
+
it("includes multiple keys in keys-to-translate", () => {
|
|
122
122
|
const ctx = {
|
|
123
|
-
sourceLocale:
|
|
124
|
-
targetLocale:
|
|
125
|
-
sourceMap: { a:
|
|
123
|
+
sourceLocale: "en",
|
|
124
|
+
targetLocale: "de",
|
|
125
|
+
sourceMap: { a: "A", b: "B", c: "C" },
|
|
126
126
|
targetMap: {},
|
|
127
|
-
keys: [
|
|
127
|
+
keys: ["a", "b", "c"],
|
|
128
128
|
};
|
|
129
129
|
const prompt = buildUserPrompt(ctx);
|
|
130
130
|
expect(prompt).toContain('"a"');
|
|
@@ -132,14 +132,14 @@ describe('buildUserPrompt', () => {
|
|
|
132
132
|
expect(prompt).toContain('"c"');
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it(
|
|
135
|
+
it("round-trips JSON sourceMap without corruption", () => {
|
|
136
136
|
const sourceMap = { key: 'value with "quotes" and \\ backslash' };
|
|
137
137
|
const ctx = {
|
|
138
|
-
sourceLocale:
|
|
139
|
-
targetLocale:
|
|
138
|
+
sourceLocale: "en",
|
|
139
|
+
targetLocale: "de",
|
|
140
140
|
sourceMap,
|
|
141
141
|
targetMap: {},
|
|
142
|
-
keys: [
|
|
142
|
+
keys: ["key"],
|
|
143
143
|
};
|
|
144
144
|
const prompt = buildUserPrompt(ctx);
|
|
145
145
|
const jsonMatch = prompt.match(/<source-file>\n([\s\S]*?)\n<\/source-file>/)?.[1];
|