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.
Files changed (71) hide show
  1. package/README.md +8 -10
  2. package/TESTING.md +29 -29
  3. package/dist/cli/main.d.mts +1 -1
  4. package/dist/cli/main.mjs +549 -362
  5. package/dist/formatters-De4Q-X1d.mjs +516 -435
  6. package/dist/index.d.mts +162 -25
  7. package/dist/index.mjs +119 -34
  8. package/package.json +3 -3
  9. package/pnpm-workspace.yaml +3 -3
  10. package/src/adapter/types.test.ts +57 -57
  11. package/src/adapter/types.ts +7 -4
  12. package/src/benchmark/metrics.test.ts +141 -69
  13. package/src/benchmark/metrics.ts +6 -6
  14. package/src/benchmark/report.test.ts +38 -38
  15. package/src/benchmark/report.ts +6 -6
  16. package/src/benchmark/runner.test.ts +70 -72
  17. package/src/benchmark/runner.ts +4 -4
  18. package/src/cli/commands/add.test.ts +90 -109
  19. package/src/cli/commands/add.ts +40 -28
  20. package/src/cli/commands/benchmark.test.ts +77 -64
  21. package/src/cli/commands/benchmark.ts +64 -41
  22. package/src/cli/commands/languages.test.ts +45 -42
  23. package/src/cli/commands/languages.ts +16 -12
  24. package/src/cli/commands/missing.test.ts +143 -92
  25. package/src/cli/commands/missing.ts +24 -17
  26. package/src/cli/commands/translate.test.ts +79 -79
  27. package/src/cli/commands/translate.ts +41 -31
  28. package/src/cli/commands/unused.test.ts +62 -51
  29. package/src/cli/commands/unused.ts +18 -14
  30. package/src/cli/commands/validate.test.ts +130 -72
  31. package/src/cli/commands/validate.ts +25 -20
  32. package/src/cli/config-resolution.test.ts +169 -49
  33. package/src/cli/config-resolution.ts +5 -7
  34. package/src/cli/format.test.ts +50 -50
  35. package/src/cli/format.ts +57 -60
  36. package/src/cli/formatters.test.ts +128 -106
  37. package/src/cli/formatters.ts +72 -95
  38. package/src/cli/main.ts +13 -13
  39. package/src/config/define-config.test.ts +44 -29
  40. package/src/config/define-config.ts +1 -1
  41. package/src/config/load-config.test.ts +21 -18
  42. package/src/config/load-config.ts +5 -5
  43. package/src/config/types.test.ts +50 -44
  44. package/src/config/types.ts +2 -2
  45. package/src/index.ts +22 -26
  46. package/src/keys/flatten.test.ts +52 -52
  47. package/src/keys/flatten.ts +7 -9
  48. package/src/sdk/file-io.test.ts +47 -59
  49. package/src/sdk/file-io.ts +2 -2
  50. package/src/sdk/node-layer.test.ts +18 -18
  51. package/src/sdk/node-layer.ts +2 -2
  52. package/src/sdk/php-array-reader.test.ts +49 -40
  53. package/src/sdk/php-array-reader.ts +5 -5
  54. package/src/translation/chunking.test.ts +52 -44
  55. package/src/translation/chunking.ts +1 -1
  56. package/src/translation/missing-keys.test.ts +86 -93
  57. package/src/translation/missing-keys.ts +4 -6
  58. package/src/translation/model-registry.test.ts +41 -32
  59. package/src/translation/model-registry.ts +9 -9
  60. package/src/translation/one-shot-strategy.test.ts +105 -86
  61. package/src/translation/one-shot-strategy.ts +10 -12
  62. package/src/translation/orchestrator.test.ts +90 -101
  63. package/src/translation/orchestrator.ts +26 -26
  64. package/src/translation/prompt.test.ts +76 -76
  65. package/src/translation/prompt.ts +2 -2
  66. package/src/translation/tool-loop-strategy.test.ts +134 -107
  67. package/src/translation/tool-loop-strategy.ts +14 -18
  68. package/src/translation/types.test.ts +22 -22
  69. package/src/translation/types.ts +3 -3
  70. package/tsdown.config.ts +3 -3
  71. package/vitest.config.ts +3 -3
@@ -1,25 +1,25 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Effect, Either } from 'effect';
3
- import { runTranslation } from './orchestrator.js';
4
- import type { TranslationAdapter, ResourceRef, AdapterReadError, AdapterWriteError } from '../adapter/types.js';
5
- import { TranslationFailedError } from './types.js';
6
-
7
- describe('runTranslation', () => {
8
- it('translates missing keys across adapters and writes merged results', async () => {
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: 'test',
17
+ name: "test",
13
18
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
14
- listLocales: () => Effect.succeed(['en', 'de']),
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: 'one-shot' as const,
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: 'en',
41
- targetLocales: ['de'],
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('de');
48
- expect(writes[0]!.entries).toEqual({ hello: 'Hallo', bye: 'de:bye' });
47
+ expect(writes[0]!.locale).toBe("de");
48
+ expect(writes[0]!.entries).toEqual({ hello: "Hallo", bye: "de:bye" });
49
49
  });
50
50
 
51
- it('respects concurrency limit', async () => {
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: 'test',
56
+ name: "test",
57
57
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
58
- listLocales: () => Effect.succeed(['en', 'de']),
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: 'one-shot' as const,
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('50 millis');
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: 'en',
86
- targetLocales: ['de'],
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('collects failures and reports them at the end', async () => {
89
+ it("collects failures and reports them at the end", async () => {
95
90
  const adapter: TranslationAdapter = {
96
- name: 'test',
91
+ name: "test",
97
92
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
98
- listLocales: () => Effect.succeed(['en', 'de']),
99
- listResources: () =>
100
- Effect.succeed([{ key: 'messages', label: 'messages' }]),
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: 'one-shot' as const,
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: 'en',
120
- targetLocales: ['de'],
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<void, TranslationFailedError>;
125
- if (exit._tag === 'Left') {
126
- expect((exit.left as TranslationFailedError)._tag).toBe('TranslationFailedError');
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('Expected Left');
119
+ throw new Error("Expected Left");
129
120
  }
130
121
  });
131
122
 
132
- it('handles empty adapters list', async () => {
123
+ it("handles empty adapters list", async () => {
133
124
  const strategy = {
134
- name: 'one-shot' as const,
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: 'en',
142
- targetLocales: ['de'],
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('handles single-locale adapter (no targets)', async () => {
141
+ it("handles single-locale adapter (no targets)", async () => {
151
142
  const adapter: TranslationAdapter = {
152
- name: 'mono',
143
+ name: "mono",
153
144
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
154
- listLocales: () => Effect.succeed(['en']),
155
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
156
- readResource: () => Effect.succeed({ hello: '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: 'one-shot' as const,
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: 'en',
169
- targetLocales: ['de'],
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('handles multiple resources', async () => {
168
+ it("handles multiple resources", async () => {
178
169
  const writes: Array<{ locale: string; resource: string }> = [];
179
170
  const adapter: TranslationAdapter = {
180
- name: 'multi',
171
+ name: "multi",
181
172
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
182
- listLocales: () => Effect.succeed(['en', 'de']),
173
+ listLocales: () => Effect.succeed(["en", "de"]),
183
174
  listResources: () =>
184
175
  Effect.succeed([
185
- { key: 'auth', label: 'auth' },
186
- { key: 'validation', label: 'validation' },
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: 'one-shot' as const,
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: 'en',
208
- targetLocales: ['de'],
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('handles adapter readResource failure', async () => {
204
+ it("handles adapter readResource failure", async () => {
217
205
  const adapter: TranslationAdapter = {
218
- name: 'broken',
206
+ name: "broken",
219
207
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
220
- listLocales: () => Effect.succeed(['en', 'de']),
221
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
222
- readResource: () => Effect.fail(new Error('read failed') as AdapterReadError),
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: 'one-shot' as const,
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: 'en',
235
- targetLocales: ['de'],
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('read failed');
227
+ await expect(Effect.runPromise(program)).rejects.toThrow("read failed");
240
228
  });
241
229
 
242
- it('handles multiple target locales', async () => {
230
+ it("handles multiple target locales", async () => {
243
231
  const writes: Array<{ locale: string }> = [];
244
232
  const adapter: TranslationAdapter = {
245
- name: 'multi-locale',
233
+ name: "multi-locale",
246
234
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
247
- listLocales: () => Effect.succeed(['en', 'de', 'fr']),
248
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
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: 'one-shot' as const,
245
+ name: "one-shot" as const,
259
246
  translateChunk: (ctx: { keys: readonly string[]; targetLocale: string }) =>
260
- Effect.succeed(Object.fromEntries(ctx.keys.map((k: string) => [k, `${ctx.targetLocale}:${k}`]))),
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: 'en',
267
- targetLocales: ['de', 'fr'],
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('de');
274
- expect(locales).toContain('fr');
262
+ expect(locales).toContain("de");
263
+ expect(locales).toContain("fr");
275
264
  });
276
265
  });
@@ -1,10 +1,10 @@
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';
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(chunks, (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
- ),
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 'vitest';
2
- import { buildSystemPrompt, buildUserPrompt } from './prompt.js';
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSystemPrompt, buildUserPrompt } from "./prompt.js";
3
3
 
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');
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('mentions placeholder preservation', () => {
13
- const prompt = buildSystemPrompt('en', 'ja');
14
- expect(prompt).toContain(':attribute');
15
- expect(prompt).toContain(':min');
16
- expect(prompt).toContain(':max');
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('instructs to not add or remove keys', () => {
20
- const prompt = buildSystemPrompt('en', 'fr');
21
- expect(prompt).toContain('Do not add or remove keys');
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('mentions double quote escaping', () => {
25
- const prompt = buildSystemPrompt('en', 'es');
26
- expect(prompt).toContain('double quotes');
24
+ it("mentions double quote escaping", () => {
25
+ const prompt = buildSystemPrompt("en", "es");
26
+ expect(prompt).toContain("double quotes");
27
27
  });
28
28
 
29
- it('handles same-language translation edge case', () => {
30
- const prompt = buildSystemPrompt('en', 'en');
31
- expect(prompt).toContain('en');
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('handles long locale codes', () => {
35
- const prompt = buildSystemPrompt('zh-Hans', 'zh-Hant');
36
- expect(prompt).toContain('zh-Hans');
37
- expect(prompt).toContain('zh-Hant');
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('buildUserPrompt', () => {
42
- it('contains XML tags and key list', () => {
41
+ describe("buildUserPrompt", () => {
42
+ it("contains XML tags and key list", () => {
43
43
  const ctx = {
44
- sourceLocale: 'en',
45
- targetLocale: 'de',
46
- sourceMap: { hello: 'Hello' },
44
+ sourceLocale: "en",
45
+ targetLocale: "de",
46
+ sourceMap: { hello: "Hello" },
47
47
  targetMap: {},
48
- keys: ['hello'],
48
+ keys: ["hello"],
49
49
  };
50
50
  const prompt = buildUserPrompt(ctx);
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');
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('includes existing translations for context', () => {
58
+ it("includes existing translations for context", () => {
59
59
  const ctx = {
60
- sourceLocale: 'en',
61
- targetLocale: 'de',
62
- sourceMap: { a: 'A', b: 'B' },
63
- targetMap: { a: 'already translated' },
64
- keys: ['b'],
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('already translated');
67
+ expect(prompt).toContain("already translated");
68
68
  expect(prompt).toContain('"b"');
69
69
  });
70
70
 
71
- it('handles empty keys list', () => {
71
+ it("handles empty keys list", () => {
72
72
  const ctx = {
73
- sourceLocale: 'en',
74
- targetLocale: 'de',
75
- sourceMap: { hello: '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('<keys-to-translate>');
81
- expect(prompt).toContain('{}');
80
+ expect(prompt).toContain("<keys-to-translate>");
81
+ expect(prompt).toContain("{}");
82
82
  });
83
83
 
84
- it('handles keys with missing values in sourceMap', () => {
84
+ it("handles keys with missing values in sourceMap", () => {
85
85
  const ctx = {
86
- sourceLocale: 'en',
87
- targetLocale: 'de',
88
- sourceMap: { hello: 'Hello' },
86
+ sourceLocale: "en",
87
+ targetLocale: "de",
88
+ sourceMap: { hello: "Hello" },
89
89
  targetMap: {},
90
- keys: ['hello', 'missing'],
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('handles special characters in values', () => {
97
+ it("handles special characters in values", () => {
98
98
  const ctx = {
99
- sourceLocale: 'en',
100
- targetLocale: 'ja',
101
- sourceMap: { greeting: 'Hello \n World \t!' },
99
+ sourceLocale: "en",
100
+ targetLocale: "ja",
101
+ sourceMap: { greeting: "Hello \n World \t!" },
102
102
  targetMap: {},
103
- keys: ['greeting'],
103
+ keys: ["greeting"],
104
104
  };
105
105
  const prompt = buildUserPrompt(ctx);
106
- expect(prompt).toContain('Hello');
106
+ expect(prompt).toContain("Hello");
107
107
  });
108
108
 
109
- it('handles unicode values', () => {
109
+ it("handles unicode values", () => {
110
110
  const ctx = {
111
- sourceLocale: 'en',
112
- targetLocale: 'ja',
113
- sourceMap: { emoji: 'Hello 🌍' },
111
+ sourceLocale: "en",
112
+ targetLocale: "ja",
113
+ sourceMap: { emoji: "Hello 🌍" },
114
114
  targetMap: {},
115
- keys: ['emoji'],
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('includes multiple keys in keys-to-translate', () => {
121
+ it("includes multiple keys in keys-to-translate", () => {
122
122
  const ctx = {
123
- sourceLocale: 'en',
124
- targetLocale: 'de',
125
- sourceMap: { a: 'A', b: 'B', c: 'C' },
123
+ sourceLocale: "en",
124
+ targetLocale: "de",
125
+ sourceMap: { a: "A", b: "B", c: "C" },
126
126
  targetMap: {},
127
- keys: ['a', 'b', 'c'],
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('round-trips JSON sourceMap without corruption', () => {
135
+ it("round-trips JSON sourceMap without corruption", () => {
136
136
  const sourceMap = { key: 'value with "quotes" and \\ backslash' };
137
137
  const ctx = {
138
- sourceLocale: 'en',
139
- targetLocale: 'de',
138
+ sourceLocale: "en",
139
+ targetLocale: "de",
140
140
  sourceMap,
141
141
  targetMap: {},
142
- keys: ['key'],
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];