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,128 +1,103 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Effect } from 'effect';
3
- import { runAdd, addCommand, parseAddTokens } from './add.js';
4
- import type { DialektConfig } from '../../config/types.js';
5
- import type { TranslationAdapter, ResourceRef } from '../../adapter/types.js';
6
-
7
- describe('parseAddTokens', () => {
8
- it('parses resource.key=value pairs', async () => {
1
+ import { describe, expect, it } from "vitest";
2
+ import { Effect } from "effect";
3
+ import { runAdd, addCommand, parseAddTokens } from "./add.js";
4
+ import type { DialektConfig } from "../../config/types.js";
5
+ import type { TranslationAdapter, ResourceRef } from "../../adapter/types.js";
6
+
7
+ describe("parseAddTokens", () => {
8
+ it("parses resource.key=value pairs", async () => {
9
9
  const errors: string[] = [];
10
- const program = parseAddTokens(
11
- ['messages.hello=Hello', 'validation.email=Email'],
12
- (msg) => Effect.sync(() => errors.push(msg)),
10
+ const program = parseAddTokens(["messages.hello=Hello", "validation.email=Email"], (msg) =>
11
+ Effect.sync(() => errors.push(msg)),
13
12
  );
14
13
 
15
14
  const result = await Effect.runPromise(program);
16
15
  expect(result).toEqual({
17
- messages: { hello: 'Hello' },
18
- validation: { email: 'Email' },
16
+ messages: { hello: "Hello" },
17
+ validation: { email: "Email" },
19
18
  });
20
19
  expect(errors).toHaveLength(0);
21
20
  });
22
21
 
23
- it('handles empty tokens', async () => {
24
- const program = parseAddTokens(
25
- [],
26
- () => Effect.void,
27
- );
22
+ it("handles empty tokens", async () => {
23
+ const program = parseAddTokens([], () => Effect.void);
28
24
 
29
25
  const result = await Effect.runPromise(program);
30
26
  expect(result).toEqual({});
31
27
  });
32
28
 
33
- it('logs error for tokens without =', async () => {
29
+ it("logs error for tokens without =", async () => {
34
30
  const errors: string[] = [];
35
- const program = parseAddTokens(
36
- ['invalid-token'],
37
- (msg) => Effect.sync(() => errors.push(msg)),
38
- );
31
+ const program = parseAddTokens(["invalid-token"], (msg) => Effect.sync(() => errors.push(msg)));
39
32
 
40
33
  const result = await Effect.runPromise(program);
41
34
  expect(result).toEqual({});
42
35
  expect(errors[0]).toContain("Invalid token (missing '=')");
43
- expect(errors[0]).toContain('invalid-token');
36
+ expect(errors[0]).toContain("invalid-token");
44
37
  });
45
38
 
46
- it('logs error for keys without resource segment', async () => {
39
+ it("logs error for keys without resource segment", async () => {
47
40
  const errors: string[] = [];
48
- const program = parseAddTokens(
49
- ['no_dot=value'],
50
- (msg) => Effect.sync(() => errors.push(msg)),
51
- );
41
+ const program = parseAddTokens(["no_dot=value"], (msg) => Effect.sync(() => errors.push(msg)));
52
42
 
53
43
  const result = await Effect.runPromise(program);
54
44
  expect(result).toEqual({});
55
- expect(errors[0]).toContain('Invalid key (no resource segment)');
45
+ expect(errors[0]).toContain("Invalid key (no resource segment)");
56
46
  });
57
47
 
58
- it('handles multiple keys in the same resource', async () => {
59
- const program = parseAddTokens(
60
- ['messages.hello=Hello', 'messages.bye=Bye'],
61
- () => Effect.void,
62
- );
48
+ it("handles multiple keys in the same resource", async () => {
49
+ const program = parseAddTokens(["messages.hello=Hello", "messages.bye=Bye"], () => Effect.void);
63
50
 
64
51
  const result = await Effect.runPromise(program);
65
52
  expect(result).toEqual({
66
- messages: { hello: 'Hello', bye: 'Bye' },
53
+ messages: { hello: "Hello", bye: "Bye" },
67
54
  });
68
55
  });
69
56
 
70
- it('handles values containing =', async () => {
71
- const program = parseAddTokens(
72
- ['messages.greeting=Hello=World'],
73
- () => Effect.void,
74
- );
57
+ it("handles values containing =", async () => {
58
+ const program = parseAddTokens(["messages.greeting=Hello=World"], () => Effect.void);
75
59
 
76
60
  const result = await Effect.runPromise(program);
77
61
  expect(result).toEqual({
78
- messages: { greeting: 'Hello=World' },
62
+ messages: { greeting: "Hello=World" },
79
63
  });
80
64
  });
81
65
 
82
- it('ignores tokens after first = in key portion', async () => {
83
- const program = parseAddTokens(
84
- ['messages.key=val=ue'],
85
- () => Effect.void,
86
- );
66
+ it("ignores tokens after first = in key portion", async () => {
67
+ const program = parseAddTokens(["messages.key=val=ue"], () => Effect.void);
87
68
 
88
69
  const result = await Effect.runPromise(program);
89
70
  expect(result).toEqual({
90
- messages: { key: 'val=ue' },
71
+ messages: { key: "val=ue" },
91
72
  });
92
73
  });
93
74
 
94
- it('handles empty values', async () => {
95
- const program = parseAddTokens(
96
- ['messages.empty='],
97
- () => Effect.void,
98
- );
75
+ it("handles empty values", async () => {
76
+ const program = parseAddTokens(["messages.empty="], () => Effect.void);
99
77
 
100
78
  const result = await Effect.runPromise(program);
101
79
  expect(result).toEqual({
102
- messages: { empty: '' },
80
+ messages: { empty: "" },
103
81
  });
104
82
  });
105
83
 
106
- it('handles deeply nested resource names by splitting on first dot only', async () => {
107
- const program = parseAddTokens(
108
- ['a.b.c.key=value'],
109
- () => Effect.void,
110
- );
84
+ it("handles deeply nested resource names by splitting on first dot only", async () => {
85
+ const program = parseAddTokens(["a.b.c.key=value"], () => Effect.void);
111
86
 
112
87
  const result = await Effect.runPromise(program);
113
88
  expect(result).toEqual({
114
- a: { 'b.c.key': 'value' },
89
+ a: { "b.c.key": "value" },
115
90
  });
116
91
  });
117
92
  });
118
93
 
119
- describe('runAdd', () => {
94
+ describe("runAdd", () => {
120
95
  const baseConfig: DialektConfig = {
121
- sourceLocale: 'en',
122
- targetLocales: ['de'],
123
- strategy: 'one-shot',
124
- model: { provider: 'openai', modelId: 'gpt-4o' },
125
- fastModel: { provider: 'openai', modelId: 'gpt-4o-mini' },
96
+ sourceLocale: "en",
97
+ targetLocales: ["de"],
98
+ strategy: "one-shot",
99
+ model: { provider: "openai", modelId: "gpt-4o" },
100
+ fastModel: { provider: "openai", modelId: "gpt-4o-mini" },
126
101
  chunking: { maxTokens: 3000, charsPerToken: 3.0, concurrency: 3 },
127
102
  retry: { maxAttempts: 3, baseDelayMs: 1000 },
128
103
  adapters: [],
@@ -139,27 +114,33 @@ describe('runAdd', () => {
139
114
  };
140
115
  }
141
116
 
142
- it('writes entries to all adapters and triggers translation', async () => {
143
- const writes: Array<{ adapter: string; locale: string; resource: string; entries: Record<string, string> }> = [];
117
+ it("writes entries to all adapters and triggers translation", async () => {
118
+ const writes: Array<{
119
+ adapter: string;
120
+ locale: string;
121
+ resource: string;
122
+ entries: Record<string, string>;
123
+ }> = [];
144
124
  let translated = false;
145
125
 
146
- const adapter = makeAdapter('test');
147
- const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
126
+ const adapter = makeAdapter("test");
127
+ const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig["adapters"] };
148
128
 
149
129
  const program = runAdd(
150
- { config: './config.ts', create: false },
151
- ['messages.hello=Hello'],
130
+ { config: "./config.ts", create: false },
131
+ ["messages.hello=Hello"],
152
132
  () => Effect.succeed(config),
153
133
  () => Effect.succeed({} as unknown),
154
134
  () =>
155
135
  Effect.sync(() => {
156
136
  translated = true;
157
137
  }),
158
- (msg) => Effect.sync(() => {
159
- const parsed = JSON.parse(msg);
160
- expect(parsed.success).toBe(true);
161
- expect(parsed.message).toBe('Add + translate complete.');
162
- }),
138
+ (msg) =>
139
+ Effect.sync(() => {
140
+ const parsed = JSON.parse(msg);
141
+ expect(parsed.success).toBe(true);
142
+ expect(parsed.message).toBe("Add + translate complete.");
143
+ }),
163
144
  () => Effect.void,
164
145
  );
165
146
 
@@ -167,61 +148,61 @@ describe('runAdd', () => {
167
148
  expect(translated).toBe(true);
168
149
  });
169
150
 
170
- it('fails when configLoader fails', async () => {
151
+ it("fails when configLoader fails", async () => {
171
152
  const program = runAdd(
172
- { config: './missing.ts', create: false },
173
- ['messages.hello=Hello'],
174
- () => Effect.fail(new Error('Config not found')),
153
+ { config: "./missing.ts", create: false },
154
+ ["messages.hello=Hello"],
155
+ () => Effect.fail(new Error("Config not found")),
175
156
  () => Effect.succeed({} as unknown),
176
157
  () => Effect.void,
177
158
  () => Effect.void,
178
159
  () => Effect.void,
179
160
  );
180
161
 
181
- await expect(Effect.runPromise(program)).rejects.toThrow('Config not found');
162
+ await expect(Effect.runPromise(program)).rejects.toThrow("Config not found");
182
163
  });
183
164
 
184
- it('fails when modelResolver fails', async () => {
185
- const adapter = makeAdapter('test');
186
- const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
165
+ it("fails when modelResolver fails", async () => {
166
+ const adapter = makeAdapter("test");
167
+ const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig["adapters"] };
187
168
 
188
169
  const program = runAdd(
189
- { config: './config.ts', create: false },
190
- ['messages.hello=Hello'],
170
+ { config: "./config.ts", create: false },
171
+ ["messages.hello=Hello"],
191
172
  () => Effect.succeed(config),
192
- () => Effect.fail(new Error('Model unavailable')),
173
+ () => Effect.fail(new Error("Model unavailable")),
193
174
  () => Effect.void,
194
175
  () => Effect.void,
195
176
  () => Effect.void,
196
177
  );
197
178
 
198
- await expect(Effect.runPromise(program)).rejects.toThrow('Model unavailable');
179
+ await expect(Effect.runPromise(program)).rejects.toThrow("Model unavailable");
199
180
  });
200
181
 
201
- it('fails when translationRunner fails', async () => {
202
- const adapter = makeAdapter('test');
203
- const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
182
+ it("fails when translationRunner fails", async () => {
183
+ const adapter = makeAdapter("test");
184
+ const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig["adapters"] };
204
185
 
205
186
  const program = runAdd(
206
- { config: './config.ts', create: false },
207
- ['messages.hello=Hello'],
187
+ { config: "./config.ts", create: false },
188
+ ["messages.hello=Hello"],
208
189
  () => Effect.succeed(config),
209
190
  () => Effect.succeed({} as unknown),
210
- () => Effect.fail(new Error('Translation failed')),
191
+ () => Effect.fail(new Error("Translation failed")),
211
192
  () => Effect.void,
212
193
  () => Effect.void,
213
194
  );
214
195
 
215
- await expect(Effect.runPromise(program)).rejects.toThrow('Translation failed');
196
+ await expect(Effect.runPromise(program)).rejects.toThrow("Translation failed");
216
197
  });
217
198
 
218
- it('handles empty tokens list', async () => {
199
+ it("handles empty tokens list", async () => {
219
200
  let translated = false;
220
- const adapter = makeAdapter('test');
221
- const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig['adapters'] };
201
+ const adapter = makeAdapter("test");
202
+ const config = { ...baseConfig, adapters: [adapter] as unknown as DialektConfig["adapters"] };
222
203
 
223
204
  const program = runAdd(
224
- { config: './config.ts', create: false },
205
+ { config: "./config.ts", create: false },
225
206
  [],
226
207
  () => Effect.succeed(config),
227
208
  () => Effect.succeed({} as unknown),
@@ -237,19 +218,19 @@ describe('runAdd', () => {
237
218
  expect(translated).toBe(true);
238
219
  });
239
220
 
240
- it('filters targetLocales to exclude sourceLocale', async () => {
221
+ it("filters targetLocales to exclude sourceLocale", async () => {
241
222
  let usedTargets: readonly string[] | undefined;
242
- const adapter = makeAdapter('test');
223
+ const adapter = makeAdapter("test");
243
224
  const config = {
244
225
  ...baseConfig,
245
- sourceLocale: 'en',
246
- targetLocales: ['en', 'de', 'fr'],
247
- adapters: [adapter] as unknown as DialektConfig['adapters'],
226
+ sourceLocale: "en",
227
+ targetLocales: ["en", "de", "fr"],
228
+ adapters: [adapter] as unknown as DialektConfig["adapters"],
248
229
  };
249
230
 
250
231
  const program = runAdd(
251
- { config: './config.ts', create: false },
252
- ['messages.hello=Hello'],
232
+ { config: "./config.ts", create: false },
233
+ ["messages.hello=Hello"],
253
234
  () => Effect.succeed(config),
254
235
  () => Effect.succeed({} as unknown),
255
236
  (opts) =>
@@ -261,7 +242,7 @@ describe('runAdd', () => {
261
242
  );
262
243
 
263
244
  await Effect.runPromise(program);
264
- expect(usedTargets).not.toContain('en');
265
- expect(usedTargets).toContain('de');
245
+ expect(usedTargets).not.toContain("en");
246
+ expect(usedTargets).toContain("de");
266
247
  });
267
248
  });
@@ -1,17 +1,17 @@
1
- import { Command, Options, Args } from '@effect/cli';
2
- import { Effect, Console, Option } from 'effect';
3
- import { loadConfig } from '../../config/load-config.js';
4
- import { resolveEffectiveConfig } from '../config-resolution.js';
5
- import { resolveModel } from '../../translation/model-registry.js';
6
- import { createOneShotStrategy } from '../../translation/one-shot-strategy.js';
7
- import { createToolLoopStrategy } from '../../translation/tool-loop-strategy.js';
8
- import { runTranslation } from '../../translation/orchestrator.js';
9
- import { flattenObject } from '../../keys/flatten.js';
10
- import { detectFormat, type OutputFormat } from '../format.js';
11
- import { formatAdd, formatError } from '../formatters.js';
12
- import type { DialektConfig } from '../../config/types.js';
13
- import type { TranslationAdapter, ResourceRef } from '../../adapter/types.js';
14
- import type { TranslationStrategy } from '../../translation/types.js';
1
+ import { Command, Options, Args } from "@effect/cli";
2
+ import { Effect, Console, Option } from "effect";
3
+ import { loadConfig } from "../../config/load-config.js";
4
+ import { resolveEffectiveConfig } from "../config-resolution.js";
5
+ import { resolveModel } from "../../translation/model-registry.js";
6
+ import { createOneShotStrategy } from "../../translation/one-shot-strategy.js";
7
+ import { createToolLoopStrategy } from "../../translation/tool-loop-strategy.js";
8
+ import { runTranslation } from "../../translation/orchestrator.js";
9
+ import { flattenObject } from "../../keys/flatten.js";
10
+ import { detectFormat, type OutputFormat } from "../format.js";
11
+ import { formatAdd, formatError } from "../formatters.js";
12
+ import type { DialektConfig } from "../../config/types.js";
13
+ import type { TranslationAdapter, ResourceRef } from "../../adapter/types.js";
14
+ import type { TranslationStrategy } from "../../translation/types.js";
15
15
 
16
16
  export interface AddFlags {
17
17
  readonly config: string;
@@ -27,14 +27,14 @@ export function parseAddTokens(
27
27
  const entriesByResource: Record<string, Record<string, string>> = {};
28
28
 
29
29
  for (const token of tokens) {
30
- const eqIdx = token.indexOf('=');
30
+ const eqIdx = token.indexOf("=");
31
31
  if (eqIdx === -1) {
32
32
  yield* errorLogger(`Invalid token (missing '='): ${token}`);
33
33
  continue;
34
34
  }
35
35
  const key = token.slice(0, eqIdx);
36
36
  const value = token.slice(eqIdx + 1);
37
- const dotIdx = key.indexOf('.');
37
+ const dotIdx = key.indexOf(".");
38
38
  if (dotIdx === -1) {
39
39
  yield* errorLogger(`Invalid key (no resource segment): ${key}`);
40
40
  continue;
@@ -53,7 +53,10 @@ export function runAdd(
53
53
  flags: AddFlags,
54
54
  tokens: readonly string[],
55
55
  configLoader: (path: string) => Effect.Effect<DialektConfig, unknown> = loadConfig,
56
- modelResolver: (config: { provider: string; modelId: string }) => Effect.Effect<unknown, unknown> = resolveModel,
56
+ modelResolver: (config: {
57
+ provider: string;
58
+ modelId: string;
59
+ }) => Effect.Effect<unknown, unknown> = resolveModel,
57
60
  translationRunner: (opts: {
58
61
  adapters: readonly unknown[];
59
62
  strategy: TranslationStrategy;
@@ -80,9 +83,12 @@ export function runAdd(
80
83
  }
81
84
 
82
85
  const modelConfig = effective.model;
83
- const model = yield* modelResolver(modelConfig) as Effect.Effect<import('ai').LanguageModel, unknown>;
86
+ const model = yield* modelResolver(modelConfig) as Effect.Effect<
87
+ import("ai").LanguageModel,
88
+ unknown
89
+ >;
84
90
  const translationStrategy =
85
- effective.strategy === 'tool-loop-agent'
91
+ effective.strategy === "tool-loop-agent"
86
92
  ? createToolLoopStrategy({ model, retry: effective.retry })
87
93
  : createOneShotStrategy({ model, retry: effective.retry });
88
94
 
@@ -104,7 +110,7 @@ export function runAdd(
104
110
  formatAdd(
105
111
  {
106
112
  success: true,
107
- message: 'Add + translate complete.',
113
+ message: "Add + translate complete.",
108
114
  addedResources,
109
115
  },
110
116
  format,
@@ -113,11 +119,17 @@ export function runAdd(
113
119
  });
114
120
  }
115
121
 
116
- export const addCommand = Command.make('add', {
117
- config: Options.text('config').pipe(Options.withDefault('./dialekt.config.ts')),
118
- create: Options.boolean('create'),
119
- format: Options.optional(Options.text('format')),
120
- }, ({ config, create, format }) => {
121
- const rawTokens = process.argv.slice(3).filter((t: string) => !t.startsWith('--') && !t.startsWith('-'));
122
- return runAdd({ config, create, format }, rawTokens);
123
- });
122
+ export const addCommand = Command.make(
123
+ "add",
124
+ {
125
+ config: Options.text("config").pipe(Options.withDefault("./dialekt.config.ts")),
126
+ create: Options.boolean("create"),
127
+ format: Options.optional(Options.text("format")),
128
+ },
129
+ ({ config, create, format }) => {
130
+ const rawTokens = process.argv
131
+ .slice(3)
132
+ .filter((t: string) => !t.startsWith("--") && !t.startsWith("-"));
133
+ return runAdd({ config, create, format }, rawTokens);
134
+ },
135
+ );