dialekt 0.1.0

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 (73) hide show
  1. package/README.md +62 -0
  2. package/TESTING.md +66 -0
  3. package/dist/cli/main.d.mts +1 -0
  4. package/dist/cli/main.mjs +412 -0
  5. package/dist/formatters-De4Q-X1d.mjs +577 -0
  6. package/dist/index.d.mts +329 -0
  7. package/dist/index.mjs +60 -0
  8. package/package.json +39 -0
  9. package/pnpm-workspace.yaml +7 -0
  10. package/src/adapter/types.test.ts +98 -0
  11. package/src/adapter/types.ts +73 -0
  12. package/src/benchmark/metrics.test.ts +180 -0
  13. package/src/benchmark/metrics.ts +69 -0
  14. package/src/benchmark/report.test.ts +129 -0
  15. package/src/benchmark/report.ts +21 -0
  16. package/src/benchmark/runner.test.ts +162 -0
  17. package/src/benchmark/runner.ts +27 -0
  18. package/src/cli/commands/add.test.ts +267 -0
  19. package/src/cli/commands/add.ts +123 -0
  20. package/src/cli/commands/benchmark.test.ts +346 -0
  21. package/src/cli/commands/benchmark.ts +148 -0
  22. package/src/cli/commands/languages.test.ts +127 -0
  23. package/src/cli/commands/languages.ts +42 -0
  24. package/src/cli/commands/missing.test.ts +256 -0
  25. package/src/cli/commands/missing.ts +88 -0
  26. package/src/cli/commands/translate.test.ts +384 -0
  27. package/src/cli/commands/translate.ts +106 -0
  28. package/src/cli/commands/unused.test.ts +192 -0
  29. package/src/cli/commands/unused.ts +87 -0
  30. package/src/cli/commands/validate.test.ts +245 -0
  31. package/src/cli/commands/validate.ts +96 -0
  32. package/src/cli/config-resolution.test.ts +99 -0
  33. package/src/cli/config-resolution.ts +29 -0
  34. package/src/cli/format.test.ts +117 -0
  35. package/src/cli/format.ts +205 -0
  36. package/src/cli/formatters.test.ts +186 -0
  37. package/src/cli/formatters.ts +350 -0
  38. package/src/cli/main.ts +31 -0
  39. package/src/config/define-config.test.ts +66 -0
  40. package/src/config/define-config.ts +5 -0
  41. package/src/config/load-config.test.ts +35 -0
  42. package/src/config/load-config.ts +21 -0
  43. package/src/config/types.test.ts +101 -0
  44. package/src/config/types.ts +28 -0
  45. package/src/index.ts +56 -0
  46. package/src/keys/flatten.test.ts +111 -0
  47. package/src/keys/flatten.ts +41 -0
  48. package/src/sdk/file-io.test.ts +139 -0
  49. package/src/sdk/file-io.ts +21 -0
  50. package/src/sdk/node-layer.test.ts +54 -0
  51. package/src/sdk/node-layer.ts +10 -0
  52. package/src/sdk/php-array-reader.test.ts +114 -0
  53. package/src/sdk/php-array-reader.ts +26 -0
  54. package/src/translation/chunking.test.ts +118 -0
  55. package/src/translation/chunking.ts +57 -0
  56. package/src/translation/missing-keys.test.ts +179 -0
  57. package/src/translation/missing-keys.ts +36 -0
  58. package/src/translation/model-registry.test.ts +54 -0
  59. package/src/translation/model-registry.ts +43 -0
  60. package/src/translation/one-shot-strategy.test.ts +259 -0
  61. package/src/translation/one-shot-strategy.ts +48 -0
  62. package/src/translation/orchestrator.test.ts +276 -0
  63. package/src/translation/orchestrator.ts +83 -0
  64. package/src/translation/prompt.test.ts +149 -0
  65. package/src/translation/prompt.ts +42 -0
  66. package/src/translation/tool-loop-strategy.test.ts +279 -0
  67. package/src/translation/tool-loop-strategy.ts +68 -0
  68. package/src/translation/types.test.ts +37 -0
  69. package/src/translation/types.ts +21 -0
  70. package/tsconfig.json +9 -0
  71. package/tsconfig.tsbuildinfo +1 -0
  72. package/tsdown.config.ts +7 -0
  73. package/vitest.config.ts +8 -0
@@ -0,0 +1,329 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { NodeContext } from "@effect/platform-node";
3
+ import { FileSystem, Path } from "@effect/platform";
4
+ import { LanguageModel } from "ai";
5
+ import { CommandExecutor } from "@effect/platform/CommandExecutor";
6
+
7
+ //#region src/adapter/types.d.ts
8
+ /** Opaque adapter-specific identifier for one resource within a locale. */
9
+ interface ResourceRef {
10
+ readonly key: string;
11
+ readonly label: string;
12
+ }
13
+ declare const AdapterReadError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
14
+ readonly _tag: "AdapterReadError";
15
+ } & Readonly<A>;
16
+ declare class AdapterReadError extends AdapterReadError_base<{
17
+ readonly adapter: string;
18
+ readonly locale: string;
19
+ readonly resource: string;
20
+ readonly cause: unknown;
21
+ }> {}
22
+ declare const AdapterWriteError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
23
+ readonly _tag: "AdapterWriteError";
24
+ } & Readonly<A>;
25
+ declare class AdapterWriteError extends AdapterWriteError_base<{
26
+ readonly adapter: string;
27
+ readonly locale: string;
28
+ readonly resource: string;
29
+ readonly cause: unknown;
30
+ }> {}
31
+ interface TranslationAdapter {
32
+ /** Stable adapter name, e.g. "laravel", "paraglide". Used in CLI --adapter flag and error messages. */
33
+ readonly name: string;
34
+ /** Which optional features this adapter instance supports (see Feature flags below). */
35
+ readonly capabilities: AdapterCapabilities;
36
+ /** Auto-detect configured locales (e.g. subdirectories of a lang dir), or return the user-configured list. */
37
+ listLocales(): Effect.Effect<readonly string[], AdapterReadError>;
38
+ /** List the resources available for a given locale (e.g. domain files present for "en"). */
39
+ listResources(locale: string): Effect.Effect<readonly ResourceRef[], AdapterReadError>;
40
+ /** Read one resource, flattened to dot-notation key → string value. Returns {} if the resource does not exist. */
41
+ readResource(locale: string, resource: ResourceRef): Effect.Effect<Record<string, string>, AdapterReadError>;
42
+ /** Write a full flattened key→value map back to a resource, unflattening as needed. Creates the resource if absent and `create` capability allows it. */
43
+ writeResource(locale: string, resource: ResourceRef, entries: Record<string, string>): Effect.Effect<void, AdapterWriteError>;
44
+ /**
45
+ * Returns translation keys present in the resource but never referenced
46
+ * anywhere in the project's source code. Only called by the CLI's `unused`
47
+ * command when `capabilities.unusedKeyDetection` is `true` — present as an
48
+ * optional method (not required on every adapter) because some future
49
+ * adapter format may have no reliable "is this key referenced" heuristic
50
+ * (e.g. a flat gettext catalog with no consistent call-site convention).
51
+ *
52
+ * Deliberately minimal contract: the adapter receives no scan-path
53
+ * guidance, no shared "grep helper", and no hint about what a "reference"
54
+ * looks like in its ecosystem. It owns the entire strategy internally —
55
+ * Laravel scans for `__('domain.key')`-shaped calls in PHP/Blade files;
56
+ * Paraglide scans for `m.messageName(...)` calls in JS/TS files. Each
57
+ * adapter's own constructor options (`LaravelAdapterOptions`,
58
+ * `ParaglideAdapterOptions`) carry whatever scan-path configuration that
59
+ * adapter's own heuristic needs — core never sees or validates those
60
+ * options.
61
+ */
62
+ findUnusedKeys?(locale: string, resource: ResourceRef): Effect.Effect<readonly string[], AdapterReadError>;
63
+ }
64
+ interface AdapterCapabilities {
65
+ readonly canCreateResource: boolean;
66
+ readonly unusedKeyDetection: boolean;
67
+ }
68
+ //#endregion
69
+ //#region src/config/types.d.ts
70
+ interface ModelConfig {
71
+ readonly provider: string;
72
+ readonly modelId: string;
73
+ }
74
+ interface ChunkingConfig {
75
+ readonly maxTokens: number;
76
+ readonly charsPerToken: number;
77
+ readonly concurrency: number;
78
+ }
79
+ interface RetryConfig {
80
+ readonly maxAttempts: number;
81
+ readonly baseDelayMs: number;
82
+ }
83
+ interface DialektConfig {
84
+ readonly sourceLocale: string;
85
+ readonly targetLocales: readonly string[] | null;
86
+ readonly strategy: 'one-shot' | 'tool-loop-agent';
87
+ readonly model: ModelConfig;
88
+ readonly fastModel: ModelConfig;
89
+ readonly chunking: ChunkingConfig;
90
+ readonly retry: RetryConfig;
91
+ readonly adapters: readonly TranslationAdapter[];
92
+ }
93
+ //#endregion
94
+ //#region src/config/define-config.d.ts
95
+ declare function defineConfig(config: DialektConfig): DialektConfig;
96
+ //#endregion
97
+ //#region src/keys/flatten.d.ts
98
+ declare function flattenObject(input: Readonly<Record<string, unknown>>, prefix?: string): Record<string, string>;
99
+ declare function unflattenObject(input: Readonly<Record<string, string>>): Record<string, unknown>;
100
+ declare function diffKeys(source: Readonly<Record<string, string>>, target: Readonly<Record<string, string>>): string[];
101
+ //#endregion
102
+ //#region src/translation/chunking.d.ts
103
+ interface ChunkingConfig$1 {
104
+ readonly maxTokens: number;
105
+ readonly charsPerToken: number;
106
+ }
107
+ declare function chunkKeys(keys: readonly string[], sourceMap: Readonly<Record<string, string>>, targetMap: Readonly<Record<string, string>>, config: ChunkingConfig$1): string[][];
108
+ //#endregion
109
+ //#region src/sdk/node-layer.d.ts
110
+ /**
111
+ * The only file in this package (besides cli/main.ts) permitted to know
112
+ * this is running on Node.js. Provides FileSystem, Path, and
113
+ * CommandExecutor. Swapping to Bun/Deno later means swapping this one
114
+ * import for @effect/platform-bun's equivalent — nothing else changes.
115
+ */
116
+ declare const NodePlatformLayer: Layer.Layer<NodeContext.NodeContext, never, never>;
117
+ //#endregion
118
+ //#region src/sdk/file-io.d.ts
119
+ declare function readFileIfExists(path: string): Effect.Effect<string | null, import("@effect/platform/Error").PlatformError, FileSystem.FileSystem>;
120
+ declare function writeFileEnsuringDir(path: string, content: string): Effect.Effect<void, import("@effect/platform/Error").PlatformError, FileSystem.FileSystem | Path.Path>;
121
+ //#endregion
122
+ //#region src/sdk/php-array-reader.d.ts
123
+ declare const PhpExecutionError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
124
+ readonly _tag: "PhpExecutionError";
125
+ } & Readonly<A>;
126
+ declare class PhpExecutionError extends PhpExecutionError_base<{
127
+ readonly path: string;
128
+ readonly cause: unknown;
129
+ }> {}
130
+ declare function readPhpArrayAsJson(absolutePath: string): Effect.Effect<Record<string, unknown>, PhpExecutionError, CommandExecutor>;
131
+ //#endregion
132
+ //#region src/translation/model-registry.d.ts
133
+ declare const UnknownProviderError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
134
+ readonly _tag: "UnknownProviderError";
135
+ } & Readonly<A>;
136
+ declare class UnknownProviderError extends UnknownProviderError_base<{
137
+ readonly provider: string;
138
+ }> {}
139
+ interface ModelConfig$1 {
140
+ readonly provider: string;
141
+ readonly modelId: string;
142
+ }
143
+ /**
144
+ * The one file in the entire codebase allowed to import AI SDK provider packages.
145
+ */
146
+ declare function resolveModel(config: ModelConfig$1): Effect.Effect<LanguageModel, UnknownProviderError>;
147
+ //#endregion
148
+ //#region src/translation/types.d.ts
149
+ interface TranslationContext {
150
+ readonly sourceLocale: string;
151
+ readonly targetLocale: string;
152
+ readonly sourceMap: Record<string, string>;
153
+ readonly targetMap: Record<string, string>;
154
+ readonly keys: readonly string[];
155
+ }
156
+ declare const TranslationFailedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
157
+ readonly _tag: "TranslationFailedError";
158
+ } & Readonly<A>;
159
+ declare class TranslationFailedError extends TranslationFailedError_base<{
160
+ readonly keys: readonly string[];
161
+ readonly cause: unknown;
162
+ }> {}
163
+ interface TranslationStrategy {
164
+ readonly name: 'one-shot' | 'tool-loop-agent';
165
+ translateChunk(ctx: TranslationContext): Effect.Effect<Record<string, string>, TranslationFailedError>;
166
+ }
167
+ //#endregion
168
+ //#region src/translation/one-shot-strategy.d.ts
169
+ declare function createOneShotStrategy(deps: {
170
+ model: LanguageModel;
171
+ retry: {
172
+ maxAttempts: number;
173
+ baseDelayMs: number;
174
+ };
175
+ }): TranslationStrategy;
176
+ //#endregion
177
+ //#region src/translation/tool-loop-strategy.d.ts
178
+ declare function createToolLoopStrategy(deps: {
179
+ model: LanguageModel;
180
+ retry: {
181
+ maxAttempts: number;
182
+ baseDelayMs: number;
183
+ };
184
+ }): TranslationStrategy;
185
+ //#endregion
186
+ //#region src/translation/orchestrator.d.ts
187
+ interface TranslationRunConfig {
188
+ readonly adapters: readonly TranslationAdapter[];
189
+ readonly strategy: TranslationStrategy;
190
+ readonly sourceLocale: string;
191
+ readonly targetLocales: readonly string[];
192
+ readonly chunking: ChunkingConfig;
193
+ }
194
+ declare function runTranslation(config: TranslationRunConfig): Effect.Effect<undefined, AdapterReadError | AdapterWriteError | TranslationFailedError, never>;
195
+ //#endregion
196
+ //#region src/translation/prompt.d.ts
197
+ declare function buildSystemPrompt(from: string, to: string): string;
198
+ declare function buildUserPrompt(ctx: TranslationContext): string;
199
+ //#endregion
200
+ //#region src/translation/missing-keys.d.ts
201
+ interface MissingKeyEntry$1 {
202
+ readonly adapter: string;
203
+ readonly locale: string;
204
+ readonly resource: ResourceRef;
205
+ readonly missing: readonly string[];
206
+ }
207
+ declare function computeMissingKeys(adapter: TranslationAdapter, sourceLocale: string, targetLocales: readonly string[]): Effect.Effect<readonly MissingKeyEntry$1[], AdapterReadError>;
208
+ //#endregion
209
+ //#region src/config/load-config.d.ts
210
+ declare const ConfigLoadError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
211
+ readonly _tag: "ConfigLoadError";
212
+ } & Readonly<A>;
213
+ declare class ConfigLoadError extends ConfigLoadError_base<{
214
+ readonly path: string;
215
+ readonly cause: unknown;
216
+ }> {}
217
+ declare function loadConfig(configPath: string): Effect.Effect<DialektConfig, ConfigLoadError>;
218
+ //#endregion
219
+ //#region src/cli/format.d.ts
220
+ /**
221
+ * Terminal formatting core utilities for the dialekt CLI output.
222
+ *
223
+ * Two output modes:
224
+ * - `pretty` — lush human-readable output with colours and grouping (TTY only)
225
+ * - `json` — single compact JSON document for AI agents / machines
226
+ *
227
+ * stdout is the data contract in every mode; status / banners go to stderr.
228
+ * All decoration is gated behind `isTTY` so the output is never mojibake-prone
229
+ * when piped or consumed by another process.
230
+ */
231
+ type OutputFormat = 'pretty' | 'json';
232
+ /**
233
+ * Resolves the output format from explicit flag and environment.
234
+ * Precedence: explicit `--format` > auto-detection.
235
+ *
236
+ * Auto-detection picks `json` when stdout is not a TTY or an agent env var
237
+ * is present; otherwise `pretty`.
238
+ */
239
+ declare function detectFormat(explicit?: OutputFormat | undefined): OutputFormat;
240
+ /** Wraps text in ANSI codes only when stdout is a TTY; otherwise returns it bare. */
241
+ declare function color(text: string, ...codes: string[]): string;
242
+ interface Glyphs {
243
+ hLine: string;
244
+ vLine: string;
245
+ cornerTL: string;
246
+ cornerTR: string;
247
+ cornerBL: string;
248
+ cornerBR: string;
249
+ teeRight: string;
250
+ teeLeft: string;
251
+ teeDown: string;
252
+ teeUp: string;
253
+ cross: string;
254
+ bullet: string;
255
+ arrow: string;
256
+ check: string;
257
+ crossMark: string;
258
+ warn: string;
259
+ }
260
+ declare function glyphs(): Glyphs;
261
+ declare function drawTable(headers: readonly string[], rows: readonly (readonly string[])[]): string;
262
+ declare function banner(title: string): string;
263
+ declare function sectionHeader(label: string): string;
264
+ declare function success(text: string): string;
265
+ declare function failure(text: string): string;
266
+ declare function warning(text: string): string;
267
+ declare function info(text: string): string;
268
+ declare function keyValue(key: string, value: string): string;
269
+ //#endregion
270
+ //#region src/cli/formatters.d.ts
271
+ interface MissingKeyEntry {
272
+ readonly adapter: string;
273
+ readonly locale: string;
274
+ readonly resource: string;
275
+ readonly key: string;
276
+ }
277
+ declare function formatMissingKeys(entries: readonly MissingKeyEntry[], format: OutputFormat): string;
278
+ interface UnusedKeyEntry {
279
+ readonly adapter: string;
280
+ readonly locale: string;
281
+ readonly resource: string;
282
+ readonly key: string;
283
+ }
284
+ declare function formatUnusedKeys(entries: readonly UnusedKeyEntry[], format: OutputFormat): string;
285
+ interface ValidateEntry {
286
+ readonly adapter: string;
287
+ readonly locale: string;
288
+ readonly resource: string;
289
+ readonly count: number;
290
+ }
291
+ interface ValidateResult {
292
+ readonly passing: boolean;
293
+ readonly entries: readonly ValidateEntry[];
294
+ }
295
+ declare function formatValidate(result: ValidateResult, format: OutputFormat): string;
296
+ interface LanguageEntry {
297
+ readonly adapter: string;
298
+ readonly locales: readonly string[];
299
+ }
300
+ declare function formatLanguages(entries: readonly LanguageEntry[], format: OutputFormat): string;
301
+ interface TranslateResult {
302
+ readonly success: boolean;
303
+ readonly message: string;
304
+ readonly stats?: {
305
+ readonly adaptersProcessed: number;
306
+ readonly localesTranslated: number;
307
+ readonly keysTranslated: number;
308
+ };
309
+ }
310
+ declare function formatTranslate(result: TranslateResult, format: OutputFormat): string;
311
+ interface AddResult {
312
+ readonly success: boolean;
313
+ readonly message: string;
314
+ readonly addedResources?: readonly string[];
315
+ }
316
+ declare function formatAdd(result: AddResult, format: OutputFormat): string;
317
+ interface BenchmarkEntry {
318
+ readonly strategyName: string;
319
+ readonly totalChunks: number;
320
+ readonly succeededChunks: number;
321
+ readonly failedChunks: number;
322
+ readonly totalDurationMs: number;
323
+ readonly averageDurationMsPerChunk: number;
324
+ readonly totalAttempts: number;
325
+ }
326
+ declare function formatBenchmark(entries: readonly BenchmarkEntry[], format: OutputFormat): string;
327
+ declare function formatError(message: string, format: OutputFormat): string;
328
+ //#endregion
329
+ export { type AdapterCapabilities, AdapterReadError, AdapterWriteError, type AddResult, type BenchmarkEntry, type ChunkingConfig, ConfigLoadError, type DialektConfig, type LanguageEntry, type MissingKeyEntry, type ModelConfig, NodePlatformLayer, type OutputFormat, PhpExecutionError, type ResourceRef, type RetryConfig, type TranslateResult, type TranslationAdapter, type TranslationContext, TranslationFailedError, type TranslationStrategy, UnknownProviderError, type UnusedKeyEntry, type ValidateEntry, type ValidateResult, banner, buildSystemPrompt, buildUserPrompt, chunkKeys, color, computeMissingKeys, createOneShotStrategy, createToolLoopStrategy, defineConfig, detectFormat, diffKeys, drawTable, failure, flattenObject, formatAdd, formatBenchmark, formatError, formatLanguages, formatMissingKeys, formatTranslate, formatUnusedKeys, formatValidate, glyphs, info, keyValue, loadConfig, readFileIfExists, readPhpArrayAsJson, resolveModel, runTranslation, sectionHeader, success, unflattenObject, warning, writeFileEnsuringDir };
package/dist/index.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import { A as resolveModel, C as runTranslation, D as buildUserPrompt, E as buildSystemPrompt, M as diffKeys, N as flattenObject, O as TranslationFailedError, P as unflattenObject, S as computeMissingKeys, T as createOneShotStrategy, _ as sectionHeader, a as formatMissingKeys, b as ConfigLoadError, c as formatValidate, d as detectFormat, f as drawTable, g as keyValue, h as info, i as formatLanguages, j as chunkKeys, k as UnknownProviderError, l as banner, m as glyphs, n as formatBenchmark, o as formatTranslate, p as failure, r as formatError, s as formatUnusedKeys, t as formatAdd, u as color, v as success, w as createToolLoopStrategy, x as loadConfig, y as warning } from "./formatters-De4Q-X1d.mjs";
2
+ import { Data, Effect } from "effect";
3
+ import { NodeContext } from "@effect/platform-node";
4
+ import { Command, FileSystem, Path } from "@effect/platform";
5
+ //#region src/config/define-config.ts
6
+ function defineConfig(config) {
7
+ return config;
8
+ }
9
+ //#endregion
10
+ //#region src/adapter/types.ts
11
+ var AdapterReadError = class extends Data.TaggedError("AdapterReadError") {};
12
+ var AdapterWriteError = class extends Data.TaggedError("AdapterWriteError") {};
13
+ //#endregion
14
+ //#region src/sdk/node-layer.ts
15
+ /**
16
+ * The only file in this package (besides cli/main.ts) permitted to know
17
+ * this is running on Node.js. Provides FileSystem, Path, and
18
+ * CommandExecutor. Swapping to Bun/Deno later means swapping this one
19
+ * import for @effect/platform-bun's equivalent — nothing else changes.
20
+ */
21
+ const NodePlatformLayer = NodeContext.layer;
22
+ //#endregion
23
+ //#region src/sdk/file-io.ts
24
+ function readFileIfExists(path) {
25
+ return Effect.gen(function* () {
26
+ const fs = yield* FileSystem.FileSystem;
27
+ if (!(yield* fs.exists(path))) return null;
28
+ return yield* fs.readFileString(path);
29
+ });
30
+ }
31
+ function writeFileEnsuringDir(path, content) {
32
+ return Effect.gen(function* () {
33
+ const fs = yield* FileSystem.FileSystem;
34
+ const dir = (yield* Path.Path).dirname(path);
35
+ yield* fs.makeDirectory(dir, { recursive: true });
36
+ yield* fs.writeFileString(path, content);
37
+ });
38
+ }
39
+ //#endregion
40
+ //#region src/sdk/php-array-reader.ts
41
+ var PhpExecutionError = class extends Data.TaggedError("PhpExecutionError") {};
42
+ const DUMP_SCRIPT = "echo json_encode(is_array($v = require $argv[1]) ? $v : [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);";
43
+ function readPhpArrayAsJson(absolutePath) {
44
+ return Effect.gen(function* () {
45
+ const cmd = Command.make("php", "-r", DUMP_SCRIPT, "--", absolutePath);
46
+ const output = yield* Command.string(cmd).pipe(Effect.mapError((cause) => new PhpExecutionError({
47
+ path: absolutePath,
48
+ cause
49
+ })));
50
+ return yield* Effect.try({
51
+ try: () => JSON.parse(output),
52
+ catch: (cause) => new PhpExecutionError({
53
+ path: absolutePath,
54
+ cause
55
+ })
56
+ });
57
+ });
58
+ }
59
+ //#endregion
60
+ export { AdapterReadError, AdapterWriteError, ConfigLoadError, NodePlatformLayer, PhpExecutionError, TranslationFailedError, UnknownProviderError, banner, buildSystemPrompt, buildUserPrompt, chunkKeys, color, computeMissingKeys, createOneShotStrategy, createToolLoopStrategy, defineConfig, detectFormat, diffKeys, drawTable, failure, flattenObject, formatAdd, formatBenchmark, formatError, formatLanguages, formatMissingKeys, formatTranslate, formatUnusedKeys, formatValidate, glyphs, info, keyValue, loadConfig, readFileIfExists, readPhpArrayAsJson, resolveModel, runTranslation, sectionHeader, success, unflattenObject, warning, writeFileEnsuringDir };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "dialekt",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "dialekt": "./dist/cli/main.mjs"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@ai-sdk/anthropic": "^4.0.4",
16
+ "@ai-sdk/google": "^4.0.3",
17
+ "@ai-sdk/openai": "^4.0.4",
18
+ "dialekt": "link:",
19
+ "@effect/cli": "^0.75.0",
20
+ "@effect/platform": "^0.96.0",
21
+ "@effect/platform-node": "^0.107.0",
22
+ "ai": "^7.0.0",
23
+ "effect": "^3.21.0",
24
+ "jiti": "^2.7.0",
25
+ "zod": "^3.24.0"
26
+ },
27
+ "devDependencies": {
28
+ "@effect/vitest": "^0.29.0",
29
+ "@types/node": "^26.0.1",
30
+ "tsdown": "^0.22.3",
31
+ "typescript": "^5.8.0",
32
+ "vitest": "^4.0.0"
33
+ },
34
+ "scripts": {
35
+ "build": "tsdown",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest run"
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ allowBuilds:
2
+ '@parcel/watcher': true
3
+ msgpackr-extract: true
4
+ minimumReleaseAgeExclude:
5
+ - '@ai-sdk/anthropic@4.0.4'
6
+ overrides:
7
+ 'dialekt': 'link:'
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { AdapterReadError, AdapterWriteError } from './types.js';
3
+
4
+ describe('AdapterReadError', () => {
5
+ it('carries adapter/locale/resource/cause', () => {
6
+ const err = new AdapterReadError({
7
+ adapter: 'laravel',
8
+ locale: 'en',
9
+ resource: 'validation',
10
+ cause: new Error('boom'),
11
+ });
12
+ expect(err._tag).toBe('AdapterReadError');
13
+ expect(err.adapter).toBe('laravel');
14
+ expect(err.locale).toBe('en');
15
+ expect(err.resource).toBe('validation');
16
+ });
17
+
18
+ it('carries string cause', () => {
19
+ const err = new AdapterReadError({
20
+ adapter: 'paraglide',
21
+ locale: 'de',
22
+ resource: 'messages',
23
+ cause: 'file not found',
24
+ });
25
+ expect(err._tag).toBe('AdapterReadError');
26
+ expect(err.cause).toBe('file not found');
27
+ });
28
+
29
+ it('preserves Error cause identity', () => {
30
+ const cause = new Error('disk full');
31
+ const err = new AdapterReadError({
32
+ adapter: 'laravel',
33
+ locale: 'fr',
34
+ resource: 'auth',
35
+ cause,
36
+ });
37
+ expect(err.cause).toBe(cause);
38
+ });
39
+
40
+ it('accepts empty locale and resource', () => {
41
+ const err = new AdapterReadError({
42
+ adapter: 'test',
43
+ locale: '',
44
+ resource: '',
45
+ cause: 'unknown',
46
+ });
47
+ expect(err.locale).toBe('');
48
+ expect(err.resource).toBe('');
49
+ });
50
+
51
+ it('accepts various adapter names', () => {
52
+ for (const name of ['laravel', 'paraglide', 'symfony', 'custom']) {
53
+ const err = new AdapterReadError({
54
+ adapter: name,
55
+ locale: 'en',
56
+ resource: 'x',
57
+ cause: 'test',
58
+ });
59
+ expect(err.adapter).toBe(name);
60
+ }
61
+ });
62
+ });
63
+
64
+ describe('AdapterWriteError', () => {
65
+ it('carries adapter/locale/resource/cause', () => {
66
+ const err = new AdapterWriteError({
67
+ adapter: 'paraglide',
68
+ locale: 'de',
69
+ resource: 'messages',
70
+ cause: 'disk full',
71
+ });
72
+ expect(err._tag).toBe('AdapterWriteError');
73
+ expect(err.adapter).toBe('paraglide');
74
+ expect(err.locale).toBe('de');
75
+ expect(err.resource).toBe('messages');
76
+ });
77
+
78
+ it('carries Error cause', () => {
79
+ const cause = new Error('permission denied');
80
+ const err = new AdapterWriteError({
81
+ adapter: 'laravel',
82
+ locale: 'en',
83
+ resource: 'validation',
84
+ cause,
85
+ });
86
+ expect(err.cause).toBe(cause);
87
+ });
88
+
89
+ it('accepts empty strings', () => {
90
+ const err = new AdapterWriteError({
91
+ adapter: '',
92
+ locale: '',
93
+ resource: '',
94
+ cause: '',
95
+ });
96
+ expect(err.adapter).toBe('');
97
+ });
98
+ });
@@ -0,0 +1,73 @@
1
+ import { Data, Effect } from 'effect';
2
+
3
+ /** Opaque adapter-specific identifier for one resource within a locale. */
4
+ export interface ResourceRef {
5
+ readonly key: string; // e.g. Laravel domain "validation", or "messages" for Paraglide
6
+ readonly label: string; // human-readable, for CLI output
7
+ }
8
+
9
+ export class AdapterReadError extends Data.TaggedError('AdapterReadError')<{
10
+ readonly adapter: string;
11
+ readonly locale: string;
12
+ readonly resource: string;
13
+ readonly cause: unknown;
14
+ }> {}
15
+
16
+ export class AdapterWriteError extends Data.TaggedError('AdapterWriteError')<{
17
+ readonly adapter: string;
18
+ readonly locale: string;
19
+ readonly resource: string;
20
+ readonly cause: unknown;
21
+ }> {}
22
+
23
+ export interface TranslationAdapter {
24
+ /** Stable adapter name, e.g. "laravel", "paraglide". Used in CLI --adapter flag and error messages. */
25
+ readonly name: string;
26
+
27
+ /** Which optional features this adapter instance supports (see Feature flags below). */
28
+ readonly capabilities: AdapterCapabilities;
29
+
30
+ /** Auto-detect configured locales (e.g. subdirectories of a lang dir), or return the user-configured list. */
31
+ listLocales(): Effect.Effect<readonly string[], AdapterReadError>;
32
+
33
+ /** List the resources available for a given locale (e.g. domain files present for "en"). */
34
+ listResources(locale: string): Effect.Effect<readonly ResourceRef[], AdapterReadError>;
35
+
36
+ /** Read one resource, flattened to dot-notation key → string value. Returns {} if the resource does not exist. */
37
+ readResource(locale: string, resource: ResourceRef): Effect.Effect<Record<string, string>, AdapterReadError>;
38
+
39
+ /** Write a full flattened key→value map back to a resource, unflattening as needed. Creates the resource if absent and `create` capability allows it. */
40
+ writeResource(
41
+ locale: string,
42
+ resource: ResourceRef,
43
+ entries: Record<string, string>,
44
+ ): Effect.Effect<void, AdapterWriteError>;
45
+
46
+ /**
47
+ * Returns translation keys present in the resource but never referenced
48
+ * anywhere in the project's source code. Only called by the CLI's `unused`
49
+ * command when `capabilities.unusedKeyDetection` is `true` — present as an
50
+ * optional method (not required on every adapter) because some future
51
+ * adapter format may have no reliable "is this key referenced" heuristic
52
+ * (e.g. a flat gettext catalog with no consistent call-site convention).
53
+ *
54
+ * Deliberately minimal contract: the adapter receives no scan-path
55
+ * guidance, no shared "grep helper", and no hint about what a "reference"
56
+ * looks like in its ecosystem. It owns the entire strategy internally —
57
+ * Laravel scans for `__('domain.key')`-shaped calls in PHP/Blade files;
58
+ * Paraglide scans for `m.messageName(...)` calls in JS/TS files. Each
59
+ * adapter's own constructor options (`LaravelAdapterOptions`,
60
+ * `ParaglideAdapterOptions`) carry whatever scan-path configuration that
61
+ * adapter's own heuristic needs — core never sees or validates those
62
+ * options.
63
+ */
64
+ findUnusedKeys?(
65
+ locale: string,
66
+ resource: ResourceRef,
67
+ ): Effect.Effect<readonly string[], AdapterReadError>;
68
+ }
69
+
70
+ export interface AdapterCapabilities {
71
+ readonly canCreateResource: boolean; // can writeResource() create a brand-new file?
72
+ readonly unusedKeyDetection: boolean; // is findUnusedKeys implemented?
73
+ }