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,179 +1,172 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Effect } from 'effect';
3
- import { computeMissingKeys } from './missing-keys.js';
4
- import type { TranslationAdapter } from '../adapter/types.js';
1
+ import { describe, expect, it } from "vitest";
2
+ import { Effect } from "effect";
3
+ import { computeMissingKeys } from "./missing-keys.js";
4
+ import type { TranslationAdapter } from "../adapter/types.js";
5
5
 
6
- describe('computeMissingKeys', () => {
7
- it('lists missing keys per resource/locale', async () => {
6
+ describe("computeMissingKeys", () => {
7
+ it("lists missing keys per resource/locale", async () => {
8
8
  const adapter: TranslationAdapter = {
9
- name: 'test',
9
+ name: "test",
10
10
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
11
- listLocales: () => Effect.succeed(['en', 'de']),
12
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
11
+ listLocales: () => Effect.succeed(["en", "de"]),
12
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
13
13
  readResource: (locale: string) =>
14
- Effect.succeed(
15
- locale === 'en' ? { hello: 'Hello', bye: 'Bye' } : { hello: 'Hallo' },
16
- ),
14
+ Effect.succeed(locale === "en" ? { hello: "Hello", bye: "Bye" } : { hello: "Hallo" }),
17
15
  writeResource: () => Effect.void,
18
16
  };
19
17
 
20
- const result = await Effect.runPromise(
21
- computeMissingKeys(adapter, 'en', ['de']),
22
- ) as ReadonlyArray<{ missing: readonly string[] }>;
18
+ const result = (await Effect.runPromise(
19
+ computeMissingKeys(adapter, "en", ["de"]),
20
+ )) as ReadonlyArray<{ missing: readonly string[] }>;
23
21
  expect(result).toHaveLength(1);
24
- expect(result[0]!.missing).toEqual(['bye']);
22
+ expect(result[0]!.missing).toEqual(["bye"]);
25
23
  });
26
24
 
27
- it('returns empty when nothing is missing', async () => {
25
+ it("returns empty when nothing is missing", async () => {
28
26
  const adapter: TranslationAdapter = {
29
- name: 'test',
27
+ name: "test",
30
28
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
31
- listLocales: () => Effect.succeed(['en', 'de']),
32
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
33
- readResource: () => Effect.succeed({ hello: 'Hello' }),
29
+ listLocales: () => Effect.succeed(["en", "de"]),
30
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
31
+ readResource: () => Effect.succeed({ hello: "Hello" }),
34
32
  writeResource: () => Effect.void,
35
33
  };
36
34
 
37
- const result = await Effect.runPromise(
38
- computeMissingKeys(adapter, 'en', ['de']),
39
- ) as ReadonlyArray<{ missing: readonly string[] }>;
35
+ const result = (await Effect.runPromise(
36
+ computeMissingKeys(adapter, "en", ["de"]),
37
+ )) as ReadonlyArray<{ missing: readonly string[] }>;
40
38
  expect(result).toHaveLength(0);
41
39
  });
42
40
 
43
- it('handles multiple resources', async () => {
41
+ it("handles multiple resources", async () => {
44
42
  const adapter: TranslationAdapter = {
45
- name: 'test',
43
+ name: "test",
46
44
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
47
- listLocales: () => Effect.succeed(['en', 'de']),
45
+ listLocales: () => Effect.succeed(["en", "de"]),
48
46
  listResources: () =>
49
47
  Effect.succeed([
50
- { key: 'auth', label: 'auth' },
51
- { key: 'validation', label: 'validation' },
48
+ { key: "auth", label: "auth" },
49
+ { key: "validation", label: "validation" },
52
50
  ]),
53
51
  readResource: (locale: string, resource) =>
54
52
  Effect.succeed(
55
- locale === 'en'
56
- ? resource.key === 'auth'
57
- ? { login: 'Login' }
58
- : { email: 'Email' }
59
- : resource.key === 'auth'
60
- ? { login: 'Anmelden' }
53
+ locale === "en"
54
+ ? resource.key === "auth"
55
+ ? { login: "Login" }
56
+ : { email: "Email" }
57
+ : resource.key === "auth"
58
+ ? { login: "Anmelden" }
61
59
  : {},
62
60
  ),
63
61
  writeResource: () => Effect.void,
64
62
  };
65
63
 
66
- const result = await Effect.runPromise(
67
- computeMissingKeys(adapter, 'en', ['de']),
68
- ) as ReadonlyArray<{ resource: { label: string }; missing: readonly string[] }>;
64
+ const result = (await Effect.runPromise(
65
+ computeMissingKeys(adapter, "en", ["de"]),
66
+ )) as ReadonlyArray<{ resource: { label: string }; missing: readonly string[] }>;
69
67
  expect(result).toHaveLength(1);
70
- expect(result[0]!.resource.label).toBe('validation');
71
- expect(result[0]!.missing).toEqual(['email']);
68
+ expect(result[0]!.resource.label).toBe("validation");
69
+ expect(result[0]!.missing).toEqual(["email"]);
72
70
  });
73
71
 
74
- it('handles multiple target locales', async () => {
72
+ it("handles multiple target locales", async () => {
75
73
  const adapter: TranslationAdapter = {
76
- name: 'test',
74
+ name: "test",
77
75
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
78
- listLocales: () => Effect.succeed(['en', 'de', 'fr']),
79
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
80
- readResource: (locale: string) =>
81
- Effect.succeed(
82
- locale === 'en' ? { hello: 'Hello' } : {},
83
- ),
76
+ listLocales: () => Effect.succeed(["en", "de", "fr"]),
77
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
78
+ readResource: (locale: string) => Effect.succeed(locale === "en" ? { hello: "Hello" } : {}),
84
79
  writeResource: () => Effect.void,
85
80
  };
86
81
 
87
- const result = await Effect.runPromise(
88
- computeMissingKeys(adapter, 'en', ['de', 'fr']),
89
- ) as ReadonlyArray<{ locale: string; missing: readonly string[] }>;
82
+ const result = (await Effect.runPromise(
83
+ computeMissingKeys(adapter, "en", ["de", "fr"]),
84
+ )) as ReadonlyArray<{ locale: string; missing: readonly string[] }>;
90
85
  expect(result).toHaveLength(2);
91
86
  const locales = result.map((r) => r.locale);
92
- expect(locales).toContain('de');
93
- expect(locales).toContain('fr');
87
+ expect(locales).toContain("de");
88
+ expect(locales).toContain("fr");
94
89
  });
95
90
 
96
- it('handles empty target locales', async () => {
91
+ it("handles empty target locales", async () => {
97
92
  const adapter: TranslationAdapter = {
98
- name: 'test',
93
+ name: "test",
99
94
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
100
- listLocales: () => Effect.succeed(['en']),
101
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
95
+ listLocales: () => Effect.succeed(["en"]),
96
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
102
97
  readResource: () => Effect.succeed({}),
103
98
  writeResource: () => Effect.void,
104
99
  };
105
100
 
106
- const result = await Effect.runPromise(
107
- computeMissingKeys(adapter, 'en', []),
108
- ) as ReadonlyArray<{ missing: readonly string[] }>;
101
+ const result = (await Effect.runPromise(
102
+ computeMissingKeys(adapter, "en", []),
103
+ )) as ReadonlyArray<{ missing: readonly string[] }>;
109
104
  expect(result).toHaveLength(0);
110
105
  });
111
106
 
112
- it('handles empty resources', async () => {
107
+ it("handles empty resources", async () => {
113
108
  const adapter: TranslationAdapter = {
114
- name: 'test',
109
+ name: "test",
115
110
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
116
- listLocales: () => Effect.succeed(['en', 'de']),
111
+ listLocales: () => Effect.succeed(["en", "de"]),
117
112
  listResources: () => Effect.succeed([]),
118
113
  readResource: () => Effect.succeed({}),
119
114
  writeResource: () => Effect.void,
120
115
  };
121
116
 
122
- const result = await Effect.runPromise(
123
- computeMissingKeys(adapter, 'en', ['de']),
124
- ) as ReadonlyArray<{ missing: readonly string[] }>;
117
+ const result = (await Effect.runPromise(
118
+ computeMissingKeys(adapter, "en", ["de"]),
119
+ )) as ReadonlyArray<{ missing: readonly string[] }>;
125
120
  expect(result).toHaveLength(0);
126
121
  });
127
122
 
128
- it('handles adapter readResource failure', async () => {
123
+ it("handles adapter readResource failure", async () => {
129
124
  const adapter: TranslationAdapter = {
130
- name: 'broken',
125
+ name: "broken",
131
126
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
132
- listLocales: () => Effect.succeed(['en', 'de']),
133
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
134
- readResource: () => Effect.fail(new Error('read failed') as never),
127
+ listLocales: () => Effect.succeed(["en", "de"]),
128
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
129
+ readResource: () => Effect.fail(new Error("read failed") as never),
135
130
  writeResource: () => Effect.void,
136
131
  };
137
132
 
138
- await expect(Effect.runPromise(computeMissingKeys(adapter, 'en', ['de']))).rejects.toThrow('read failed');
133
+ await expect(Effect.runPromise(computeMissingKeys(adapter, "en", ["de"]))).rejects.toThrow(
134
+ "read failed",
135
+ );
139
136
  });
140
137
 
141
- it('handles all keys missing', async () => {
138
+ it("handles all keys missing", async () => {
142
139
  const adapter: TranslationAdapter = {
143
- name: 'test',
140
+ name: "test",
144
141
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
145
- listLocales: () => Effect.succeed(['en', 'de']),
146
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
142
+ listLocales: () => Effect.succeed(["en", "de"]),
143
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
147
144
  readResource: (locale: string) =>
148
- Effect.succeed(
149
- locale === 'en' ? { a: 'A', b: 'B', c: 'C' } : {},
150
- ),
145
+ Effect.succeed(locale === "en" ? { a: "A", b: "B", c: "C" } : {}),
151
146
  writeResource: () => Effect.void,
152
147
  };
153
148
 
154
- const result = await Effect.runPromise(
155
- computeMissingKeys(adapter, 'en', ['de']),
156
- ) as ReadonlyArray<{ missing: readonly string[] }>;
149
+ const result = (await Effect.runPromise(
150
+ computeMissingKeys(adapter, "en", ["de"]),
151
+ )) as ReadonlyArray<{ missing: readonly string[] }>;
157
152
  expect(result).toHaveLength(1);
158
- expect(result[0]!.missing).toEqual(['a', 'b', 'c']);
153
+ expect(result[0]!.missing).toEqual(["a", "b", "c"]);
159
154
  });
160
155
 
161
- it('ignores target keys not present in source', async () => {
156
+ it("ignores target keys not present in source", async () => {
162
157
  const adapter: TranslationAdapter = {
163
- name: 'test',
158
+ name: "test",
164
159
  capabilities: { canCreateResource: true, unusedKeyDetection: false },
165
- listLocales: () => Effect.succeed(['en', 'de']),
166
- listResources: () => Effect.succeed([{ key: 'messages', label: 'messages' }]),
160
+ listLocales: () => Effect.succeed(["en", "de"]),
161
+ listResources: () => Effect.succeed([{ key: "messages", label: "messages" }]),
167
162
  readResource: (locale: string) =>
168
- Effect.succeed(
169
- locale === 'en' ? { a: 'A' } : { a: 'A-de', b: 'B-de' },
170
- ),
163
+ Effect.succeed(locale === "en" ? { a: "A" } : { a: "A-de", b: "B-de" }),
171
164
  writeResource: () => Effect.void,
172
165
  };
173
166
 
174
- const result = await Effect.runPromise(
175
- computeMissingKeys(adapter, 'en', ['de']),
176
- ) as ReadonlyArray<{ missing: readonly string[] }>;
167
+ const result = (await Effect.runPromise(
168
+ computeMissingKeys(adapter, "en", ["de"]),
169
+ )) as ReadonlyArray<{ missing: readonly string[] }>;
177
170
  expect(result).toHaveLength(0);
178
171
  });
179
172
  });
@@ -1,6 +1,6 @@
1
- import { Effect } from 'effect';
2
- import type { TranslationAdapter, ResourceRef, AdapterReadError } from '../adapter/types.js';
3
- import { diffKeys } from '../keys/flatten.js';
1
+ import { Effect } from "effect";
2
+ import type { TranslationAdapter, ResourceRef, AdapterReadError } from "../adapter/types.js";
3
+ import { diffKeys } from "../keys/flatten.js";
4
4
 
5
5
  export interface MissingKeyEntry {
6
6
  readonly adapter: string;
@@ -23,9 +23,7 @@ export function computeMissingKeys(
23
23
  Effect.gen(function* () {
24
24
  const targetMap = yield* adapter.readResource(locale, resource);
25
25
  const missing = diffKeys(sourceMap, targetMap);
26
- return missing.length > 0
27
- ? [{ adapter: adapter.name, locale, resource, missing }]
28
- : [];
26
+ return missing.length > 0 ? [{ adapter: adapter.name, locale, resource, missing }] : [];
29
27
  }),
30
28
  );
31
29
  return localeEntries.flat();
@@ -1,54 +1,63 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Effect, Either } from 'effect';
3
- import { resolveModel, UnknownProviderError } from './model-registry.js';
1
+ import { describe, expect, it } from "vitest";
2
+ import { Effect, Either } from "effect";
3
+ import { resolveModel, UnknownProviderError } from "./model-registry.js";
4
4
 
5
- describe('resolveModel', () => {
6
- it('returns UnknownProviderError for unknown provider', async () => {
7
- const program = resolveModel({ provider: 'unknown', modelId: 'x' });
8
- const exit = await Effect.runPromise(Effect.either(program)) as Either.Either<unknown, UnknownProviderError>;
9
- if (exit._tag === 'Left') {
10
- expect(exit.left._tag).toBe('UnknownProviderError');
11
- expect(exit.left.provider).toBe('unknown');
5
+ describe("resolveModel", () => {
6
+ it("returns UnknownProviderError for unknown provider", async () => {
7
+ const program = resolveModel({ provider: "unknown", modelId: "x" });
8
+ const exit = (await Effect.runPromise(Effect.either(program))) as Either.Either<
9
+ unknown,
10
+ UnknownProviderError
11
+ >;
12
+ if (exit._tag === "Left") {
13
+ expect(exit.left._tag).toBe("UnknownProviderError");
14
+ expect(exit.left.provider).toBe("unknown");
12
15
  } else {
13
- throw new Error('Expected Left');
16
+ throw new Error("Expected Left");
14
17
  }
15
18
  });
16
19
 
17
- it('returns UnknownProviderError for empty provider string', async () => {
18
- const program = resolveModel({ provider: '', modelId: 'x' });
19
- const exit = await Effect.runPromise(Effect.either(program)) as Either.Either<unknown, UnknownProviderError>;
20
- if (exit._tag === 'Left') {
21
- expect(exit.left._tag).toBe('UnknownProviderError');
22
- expect(exit.left.provider).toBe('');
20
+ it("returns UnknownProviderError for empty provider string", async () => {
21
+ const program = resolveModel({ provider: "", modelId: "x" });
22
+ const exit = (await Effect.runPromise(Effect.either(program))) as Either.Either<
23
+ unknown,
24
+ UnknownProviderError
25
+ >;
26
+ if (exit._tag === "Left") {
27
+ expect(exit.left._tag).toBe("UnknownProviderError");
28
+ expect(exit.left.provider).toBe("");
23
29
  } else {
24
- throw new Error('Expected Left');
30
+ throw new Error("Expected Left");
25
31
  }
26
32
  });
27
33
 
28
- it('resolves openai provider when package is available', async () => {
29
- const program = resolveModel({ provider: 'openai', modelId: 'gpt-4o' });
30
- const exit = await Effect.runPromise(Effect.either(program)) as Either.Either<unknown, UnknownProviderError>;
34
+ it("resolves openai provider when package is available", async () => {
35
+ const program = resolveModel({ provider: "openai", modelId: "gpt-4o" });
36
+ const exit = (await Effect.runPromise(Effect.either(program))) as Either.Either<
37
+ unknown,
38
+ UnknownProviderError
39
+ >;
31
40
  // Package is installed in this repo, so it should succeed
32
- expect(exit._tag).toBe('Right');
41
+ expect(exit._tag).toBe("Right");
33
42
  });
34
43
 
35
- it('UnknownProviderError preserves provider in message', () => {
36
- const err = new UnknownProviderError({ provider: 'foo' });
37
- expect(err.provider).toBe('foo');
38
- expect(err._tag).toBe('UnknownProviderError');
44
+ it("UnknownProviderError preserves provider in message", () => {
45
+ const err = new UnknownProviderError({ provider: "foo" });
46
+ expect(err.provider).toBe("foo");
47
+ expect(err._tag).toBe("UnknownProviderError");
39
48
  });
40
49
 
41
- it('accepts all known providers without type error', () => {
42
- const providers = ['openai', 'anthropic', 'google'] as const;
50
+ it("accepts all known providers without type error", () => {
51
+ const providers = ["openai", "anthropic", "google"] as const;
43
52
  for (const provider of providers) {
44
53
  // Should compile without error
45
- const config = { provider, modelId: 'test' };
54
+ const config = { provider, modelId: "test" };
46
55
  expect(config.provider).toBe(provider);
47
56
  }
48
57
  });
49
58
 
50
- it('modelId is passed through', () => {
51
- const config = { provider: 'openai' as const, modelId: 'gpt-4o' };
52
- expect(config.modelId).toBe('gpt-4o');
59
+ it("modelId is passed through", () => {
60
+ const config = { provider: "openai" as const, modelId: "gpt-4o" };
61
+ expect(config.modelId).toBe("gpt-4o");
53
62
  });
54
63
  });
@@ -1,7 +1,7 @@
1
- import { Effect, Data } from 'effect';
2
- import type { LanguageModel } from 'ai';
1
+ import { Effect, Data } from "effect";
2
+ import type { LanguageModel } from "ai";
3
3
 
4
- export class UnknownProviderError extends Data.TaggedError('UnknownProviderError')<{
4
+ export class UnknownProviderError extends Data.TaggedError("UnknownProviderError")<{
5
5
  readonly provider: string;
6
6
  }> {}
7
7
 
@@ -19,16 +19,16 @@ export function resolveModel(
19
19
  return Effect.tryPromise({
20
20
  try: async () => {
21
21
  switch (config.provider) {
22
- case 'openai': {
23
- const { openai } = await import('@ai-sdk/openai');
22
+ case "openai": {
23
+ const { openai } = await import("@ai-sdk/openai");
24
24
  return openai(config.modelId);
25
25
  }
26
- case 'anthropic': {
27
- const { anthropic } = await import('@ai-sdk/anthropic');
26
+ case "anthropic": {
27
+ const { anthropic } = await import("@ai-sdk/anthropic");
28
28
  return anthropic(config.modelId);
29
29
  }
30
- case 'google': {
31
- const { google } = await import('@ai-sdk/google');
30
+ case "google": {
31
+ const { google } = await import("@ai-sdk/google");
32
32
  return google(config.modelId);
33
33
  }
34
34
  default: