@verbatra/sdk 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://www.npmjs.com/package/@verbatra/sdk"><img src="https://img.shields.io/npm/v/@verbatra/sdk?label=%40verbatra%2Fsdk" alt="@verbatra/sdk npm version" /></a>
13
+ <a href="https://github.com/mariokreitz/verbatra/actions/workflows/ci.yml"><img src="https://github.com/mariokreitz/verbatra/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI" /></a>
14
+ <a href="https://github.com/mariokreitz/verbatra/actions/workflows/e2e.yml"><img src="https://github.com/mariokreitz/verbatra/actions/workflows/e2e.yml/badge.svg?branch=main" alt="E2E" /></a>
13
15
  <a href="https://codecov.io/gh/mariokreitz/verbatra"><img src="https://codecov.io/gh/mariokreitz/verbatra/graph/badge.svg" alt="Coverage" /></a>
14
16
  <a href="https://github.com/mariokreitz/verbatra/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
15
17
  </p>
package/dist/index.d.cts CHANGED
@@ -1,7 +1,186 @@
1
- export { SupportedFormat } from '@verbatra/core';
2
- import { AnthropicModel, OpenAiModel, GeminiModel, ProviderNotice, TranslationProvider } from '@verbatra/ai-providers';
3
1
  import { z } from 'zod';
4
- import { AdapterRegistry } from '@verbatra/format-adapters';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+ import { Interactions } from '@google/genai';
4
+ import OpenAI from 'openai';
5
+
6
+ /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
7
+ declare const supportedFormatSchema$1: z.ZodEnum<{
8
+ "i18next-json": "i18next-json";
9
+ "vue-i18n-json": "vue-i18n-json";
10
+ "next-intl-json": "next-intl-json";
11
+ "ngx-translate-json": "ngx-translate-json";
12
+ xliff: "xliff";
13
+ yaml: "yaml";
14
+ arb: "arb";
15
+ }>;
16
+ /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
17
+ type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
18
+
19
+ /**
20
+ * A single, format-neutral translation unit. Placeholders are supplied already
21
+ * extracted; core never derives them from the value.
22
+ */
23
+ declare const translationEntrySchema$1: z.ZodObject<{
24
+ key: z.ZodString;
25
+ namespace: z.ZodString;
26
+ value: z.ZodString;
27
+ description: z.ZodOptional<z.ZodString>;
28
+ meaning: z.ZodOptional<z.ZodString>;
29
+ placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
30
+ isPlural: z.ZodBoolean;
31
+ }, z.core.$strip>;
32
+ /** The validated shape of one translation unit; the inferred type of {@link translationEntrySchema}. */
33
+ type TranslationEntry$1 = Readonly<z.infer<typeof translationEntrySchema$1>>;
34
+
35
+ /**
36
+ * Result of comparing source placeholders against translated-output placeholders as MULTISETS
37
+ * (counts matter). Always returned (never thrown) for an ordinary mismatch.
38
+ */
39
+ interface PlaceholderIntegrityResult {
40
+ /** True when nothing is missing or extra and order is preserved. */
41
+ readonly matches: boolean;
42
+ /** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
43
+ readonly missing: readonly string[];
44
+ /** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
45
+ readonly extra: readonly string[];
46
+ /** Same multiset present in both, but in a different order. */
47
+ readonly reordered: boolean;
48
+ }
49
+
50
+ /** A provider is either a prompt-driven LLM or a dedicated machine-translation API. */
51
+ type ProviderKind = "llm" | "machine-translation";
52
+ /** Target tone for a translation. Maps to formality for machine-translation providers. */
53
+ type Tone = "formal" | "informal" | "neutral";
54
+ /**
55
+ * Produces the placeholder set of a value for the output integrity check. Supplied
56
+ * by the caller (the SDK) so it matches the entries' format; ai-providers never
57
+ * derives placeholders itself.
58
+ */
59
+ type PlaceholderExtractor = (value: string) => readonly string[];
60
+ /**
61
+ * A batch translation request. Format- and provider-neutral: it carries no prompt,
62
+ * model, key, or other provider-specific field. The placeholder extractor is
63
+ * mandatory (see validateRequest).
64
+ */
65
+ interface TranslateRequest {
66
+ /** BCP-47 source locale of the entries (for example, "en"). */
67
+ readonly sourceLocale: string;
68
+ /** BCP-47 target locale to translate into (for example, "de"). */
69
+ readonly targetLocale: string;
70
+ /** The entries to translate; at least one is required. */
71
+ readonly entries: readonly TranslationEntry$1[];
72
+ /** Optional source-term to target-term map applied by glossary-capable providers. */
73
+ readonly glossary?: Readonly<Record<string, string>>;
74
+ /** Optional target tone; machine-translation providers map it to formality. */
75
+ readonly tone?: Tone;
76
+ /** Mandatory placeholder extractor; the output integrity check runs against it. */
77
+ readonly extractPlaceholders: PlaceholderExtractor;
78
+ }
79
+ /** Token usage, when the provider reports it. Absent for providers without tokens (DeepL). */
80
+ interface Usage {
81
+ readonly inputTokens: number;
82
+ readonly outputTokens: number;
83
+ }
84
+ /** Result of a batch translation: per-key values and per-key integrity outcomes. */
85
+ interface TranslateResult {
86
+ /** The translated value for each requested key. */
87
+ readonly values: ReadonlyMap<string, string>;
88
+ /** The placeholder-integrity outcome for each key (source vs translated placeholder sets). */
89
+ readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult>;
90
+ /** Token usage when the provider reports it; absent for token-less providers. */
91
+ readonly usage?: Usage;
92
+ }
93
+ /**
94
+ * The single contract every provider implements. It is narrow enough that a machine-translation API like
95
+ * DeepL fits it directly, while LLM providers implement it by delegating to {@link runLlmTranslation}.
96
+ * A new provider attaches by implementing this and registering it in a {@link ProviderRegistry}.
97
+ *
98
+ * Implementer invariants:
99
+ * - Translatable strings are UNTRUSTED. They travel only as data to the provider; never splice them into
100
+ * instruction text, and never act on instructions a value appears to contain.
101
+ * - Read the API key ONLY from the environment (inside the SDK client). The request, config, and this
102
+ * interface never carry a key.
103
+ * - Fail with a secret-free {@link ProviderError}: never bind, log, or re-throw raw SDK error text (it can
104
+ * carry a key or request headers). Validate the request at the boundary with `validateRequest` so the
105
+ * integrity check can never be skipped.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * // A machine-translation provider implements translateBatch directly (the DeepL shape).
110
+ * function createMyMtProvider(client: MyClient): TranslationProvider {
111
+ * return {
112
+ * id: "my-mt",
113
+ * kind: "machine-translation",
114
+ * supportsGlossary: false,
115
+ * async translateBatch(request) {
116
+ * const data = validateRequest(request); // throws INVALID_REQUEST on a bad request
117
+ * const texts = data.entries.map((e) => e.value);
118
+ * const out = await client.translate(texts, data.targetLocale); // SDK reads MY_API_KEY from env
119
+ * // map out -> values, run the integrity check, return { values, integrity }
120
+ * return buildResult(data, out, request.extractPlaceholders);
121
+ * },
122
+ * };
123
+ * }
124
+ * ```
125
+ */
126
+ interface TranslationProvider {
127
+ /** A stable identifier for this provider (for example, "anthropic", "deepl"). */
128
+ readonly id: string;
129
+ /** Whether this provider is a prompt-driven LLM or a dedicated machine-translation API. */
130
+ readonly kind: ProviderKind;
131
+ /** Whether this provider applies a configured glossary. */
132
+ readonly supportsGlossary: boolean;
133
+ /**
134
+ * Translate a batch of entries.
135
+ *
136
+ * @param request - The provider-neutral batch request (no prompt, model, or key).
137
+ * @returns The per-key translated values and per-key placeholder-integrity outcomes.
138
+ * @throws {@link ProviderError}, secret-free, with the code for the failure (the concrete codes are
139
+ * the implementation's; see each provider factory).
140
+ */
141
+ translateBatch(request: TranslateRequest): Promise<TranslateResult>;
142
+ }
143
+
144
+ /**
145
+ * The Anthropic authoring model type, sourced from the SDK's own model union. It is an
146
+ * open union (known literals plus `string & {}`), so unknown or newly released model IDs
147
+ * are still accepted. This drives editor completions only; the runtime schema validates.
148
+ */
149
+ type AnthropicModel = Anthropic.Messages.Model;
150
+ /**
151
+ * Stable codes for the DeepL graceful-degradation notices, DeepL only. These are returned DATA on a
152
+ * successful result, NOT thrown:
153
+ *
154
+ * - `FORMALITY_DOWNGRADED`: a requested formality was not applied (the free tier does not support it).
155
+ * - `GLOSSARY_IGNORED`: a supplied generic glossary was not applied.
156
+ */
157
+ type ProviderNoticeCode = "FORMALITY_DOWNGRADED" | "GLOSSARY_IGNORED";
158
+ /**
159
+ * An observable, structured signal that something was gracefully degraded (not an
160
+ * error). Carries only a stable code and a static message, never a key or content.
161
+ * Surfaced as result data, never thrown; callers inspect it but need not treat it as a failure.
162
+ */
163
+ interface ProviderNotice {
164
+ /** The stable {@link ProviderNoticeCode} for what was degraded. */
165
+ readonly code: ProviderNoticeCode;
166
+ /** A static, safe description; never a key or translatable content. */
167
+ readonly message: string;
168
+ }
169
+
170
+ /**
171
+ * The Gemini authoring model type, sourced from the installed SDK's own model union so
172
+ * verbatra restates no model IDs. It is an open union, so unknown model IDs are still
173
+ * accepted. Type-only: it informs editor completions and is never validated at runtime,
174
+ * where the schema stays `z.string().min(1)`.
175
+ */
176
+ type GeminiModel = Interactions.Model;
177
+
178
+ /**
179
+ * The OpenAI authoring model type, sourced from the OpenAI SDK's published chat-model
180
+ * union. It drives editor completions only; it is never read at runtime, and the
181
+ * runtime schema stays `z.string().min(1)`.
182
+ */
183
+ type OpenAiModel = OpenAI.ChatModel;
5
184
 
6
185
  /**
7
186
  * The provider section of the config: a discriminated union over the provider id, reusing each
@@ -221,6 +400,171 @@ declare class SdkError extends Error {
221
400
  constructor(code: SdkErrorCode, message: string);
222
401
  }
223
402
 
403
+ /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
404
+ declare const supportedFormatSchema: z.ZodEnum<{
405
+ "i18next-json": "i18next-json";
406
+ "vue-i18n-json": "vue-i18n-json";
407
+ "next-intl-json": "next-intl-json";
408
+ "ngx-translate-json": "ngx-translate-json";
409
+ xliff: "xliff";
410
+ yaml: "yaml";
411
+ arb: "arb";
412
+ }>;
413
+ /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
414
+ type SupportedFormat = z.infer<typeof supportedFormatSchema>;
415
+
416
+ /**
417
+ * A single, format-neutral translation unit. Placeholders are supplied already
418
+ * extracted; core never derives them from the value.
419
+ */
420
+ declare const translationEntrySchema: z.ZodObject<{
421
+ key: z.ZodString;
422
+ namespace: z.ZodString;
423
+ value: z.ZodString;
424
+ description: z.ZodOptional<z.ZodString>;
425
+ meaning: z.ZodOptional<z.ZodString>;
426
+ placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
427
+ isPlural: z.ZodBoolean;
428
+ }, z.core.$strip>;
429
+ /** The validated shape of one translation unit; the inferred type of {@link translationEntrySchema}. */
430
+ type TranslationEntry = Readonly<z.infer<typeof translationEntrySchema>>;
431
+ /** All translation entries for one locale and namespace, keyed by entry key. */
432
+ interface LocaleResource {
433
+ /** The locale these entries belong to (for example, "en" or "de"). */
434
+ readonly locale: string;
435
+ /** The namespace these entries belong to. */
436
+ readonly namespace: string;
437
+ /** The source format the resource came from, for round-trip fidelity. */
438
+ readonly format: SupportedFormat;
439
+ /** Entries addressable by key. */
440
+ readonly entries: ReadonlyMap<string, TranslationEntry>;
441
+ }
442
+
443
+ /**
444
+ * The result of reading a file into core's intermediate representation.
445
+ */
446
+ interface ReadResult {
447
+ /** The parsed locale resource. */
448
+ readonly resource: LocaleResource;
449
+ /** Keys whose values are invalid for the format's message syntax. Empty for non-ICU formats. */
450
+ readonly invalidIcuKeys: readonly string[];
451
+ }
452
+ /**
453
+ * The contract every format adapter implements. A new format attaches by implementing this interface
454
+ * and registering it in an {@link AdapterRegistry}.
455
+ *
456
+ * For a JSON-family format, implement via {@link createJsonFileAdapter} rather than from scratch. A
457
+ * non-JSON format implements this interface directly and first needs its format added to core's
458
+ * `SupportedFormat`.
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * const registry = createDefaultRegistry();
463
+ * const resolution = registry.resolve("locales/en.json", { format: "i18next-json" });
464
+ * if (resolution.status === "resolved") {
465
+ * const { resource } = await resolution.adapter.read("locales/en.json", "en");
466
+ * await resolution.adapter.write(resource, "locales/en.json");
467
+ * }
468
+ * ```
469
+ */
470
+ interface FormatAdapter {
471
+ /** The single format this adapter handles (a `SupportedFormat` from core). */
472
+ readonly format: SupportedFormat;
473
+ /**
474
+ * Detect whether this adapter can handle a file, by path extension and an optional content sample.
475
+ * Best-effort and side-effect-free; it reads nothing from disk.
476
+ *
477
+ * @param filePath - The path of the file under consideration.
478
+ * @param sample - An optional leading content sample to aid detection.
479
+ * @returns True if this adapter could handle the file. Several adapters may return true for the same
480
+ * file (all JSON adapters claim `.json`); the caller disambiguates by explicit format.
481
+ */
482
+ canHandle(filePath: string, sample?: string): boolean;
483
+ /**
484
+ * Read a file into a {@link LocaleResource} plus its message-validity result.
485
+ *
486
+ * @param filePath - The file to read.
487
+ * @param locale - The locale to tag the resource with.
488
+ * @returns The parsed resource and the keys whose values are invalid for the format.
489
+ * @throws {@link AdapterError} when the content is malformed, oversized, or structurally invalid
490
+ * (the implementation names the specific codes). A missing or unopenable path instead rejects with
491
+ * the underlying filesystem error.
492
+ */
493
+ read(filePath: string, locale: string): Promise<ReadResult>;
494
+ /**
495
+ * Write a {@link LocaleResource} back to a file, preserving key order and structure, atomically.
496
+ *
497
+ * @param resource - The resource to serialize.
498
+ * @param filePath - The destination file.
499
+ * @throws {@link AdapterError} if the resource cannot be represented in the format; rejects with the
500
+ * underlying filesystem error on a write failure.
501
+ */
502
+ write(resource: LocaleResource, filePath: string): Promise<void>;
503
+ /**
504
+ * Extract the format's placeholder tokens from a single value, resolving nothing.
505
+ *
506
+ * @param value - The translatable string to scan.
507
+ * @returns The placeholder tokens found, in document order. Does not throw.
508
+ */
509
+ extractPlaceholders(value: string): readonly string[];
510
+ /**
511
+ * Validate a single value against the format's message syntax before it is written. Non-ICU formats
512
+ * (i18next, vue-i18n, ngx-translate) treat every value as valid and return true.
513
+ *
514
+ * @param value - The candidate translated value to validate.
515
+ * @returns True when the value is valid for the format's message syntax. Does not throw; an
516
+ * unparseable value returns false.
517
+ */
518
+ validateMessage(value: string): boolean;
519
+ }
520
+
521
+ /** Outcome of resolving an adapter for a file. Structured; never throws. */
522
+ type AdapterResolution = {
523
+ readonly status: "resolved";
524
+ readonly adapter: FormatAdapter;
525
+ } | {
526
+ readonly status: "no-match";
527
+ readonly filePath: string;
528
+ readonly triedFormats: readonly SupportedFormat[];
529
+ } | {
530
+ readonly status: "ambiguous";
531
+ readonly filePath: string;
532
+ readonly candidates: readonly SupportedFormat[];
533
+ };
534
+ /** Options for {@link AdapterRegistry.resolve}: select a format explicitly, or aid detection. */
535
+ interface ResolveOptions {
536
+ /** A content sample to aid detection. */
537
+ readonly sample?: string;
538
+ /** Bypass detection and select this format explicitly. */
539
+ readonly format?: SupportedFormat;
540
+ }
541
+ /**
542
+ * Holds the registered adapters and resolves one for a file. Open for extension:
543
+ * adapters attach through register without changing resolution logic.
544
+ */
545
+ declare class AdapterRegistry {
546
+ private readonly adapters;
547
+ /**
548
+ * Register an adapter.
549
+ *
550
+ * @param adapter - The adapter to add.
551
+ * @returns This registry, for chaining.
552
+ */
553
+ register(adapter: FormatAdapter): this;
554
+ private formats;
555
+ private resolveByFormat;
556
+ private resolveByDetection;
557
+ /**
558
+ * Resolve the adapter for a file, by explicit format when given, otherwise by detection.
559
+ *
560
+ * @param filePath - The file to resolve an adapter for.
561
+ * @param options - `format` selects explicitly and skips detection; `sample` aids detection.
562
+ * @returns A structured {@link AdapterResolution}: `resolved`, `no-match`, or `ambiguous`. Never
563
+ * throws; an unresolvable file is a status, not an exception.
564
+ */
565
+ resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
566
+ }
567
+
224
568
  /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
225
569
  type BoundedFileRead = {
226
570
  readonly kind: "ok";
@@ -706,4 +1050,4 @@ interface WatchController {
706
1050
  */
707
1051
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
708
1052
 
709
- export { type CheckDeps, type CheckInput, type CheckSummary, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleCheckSummary, type LocaleDiff, type LocaleNotice, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
1053
+ export { type CheckDeps, type CheckInput, type CheckSummary, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleCheckSummary, type LocaleDiff, type LocaleNotice, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,186 @@
1
- export { SupportedFormat } from '@verbatra/core';
2
- import { AnthropicModel, OpenAiModel, GeminiModel, ProviderNotice, TranslationProvider } from '@verbatra/ai-providers';
3
1
  import { z } from 'zod';
4
- import { AdapterRegistry } from '@verbatra/format-adapters';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+ import { Interactions } from '@google/genai';
4
+ import OpenAI from 'openai';
5
+
6
+ /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
7
+ declare const supportedFormatSchema$1: z.ZodEnum<{
8
+ "i18next-json": "i18next-json";
9
+ "vue-i18n-json": "vue-i18n-json";
10
+ "next-intl-json": "next-intl-json";
11
+ "ngx-translate-json": "ngx-translate-json";
12
+ xliff: "xliff";
13
+ yaml: "yaml";
14
+ arb: "arb";
15
+ }>;
16
+ /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
17
+ type SupportedFormat$1 = z.infer<typeof supportedFormatSchema$1>;
18
+
19
+ /**
20
+ * A single, format-neutral translation unit. Placeholders are supplied already
21
+ * extracted; core never derives them from the value.
22
+ */
23
+ declare const translationEntrySchema$1: z.ZodObject<{
24
+ key: z.ZodString;
25
+ namespace: z.ZodString;
26
+ value: z.ZodString;
27
+ description: z.ZodOptional<z.ZodString>;
28
+ meaning: z.ZodOptional<z.ZodString>;
29
+ placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
30
+ isPlural: z.ZodBoolean;
31
+ }, z.core.$strip>;
32
+ /** The validated shape of one translation unit; the inferred type of {@link translationEntrySchema}. */
33
+ type TranslationEntry$1 = Readonly<z.infer<typeof translationEntrySchema$1>>;
34
+
35
+ /**
36
+ * Result of comparing source placeholders against translated-output placeholders as MULTISETS
37
+ * (counts matter). Always returned (never thrown) for an ordinary mismatch.
38
+ */
39
+ interface PlaceholderIntegrityResult {
40
+ /** True when nothing is missing or extra and order is preserved. */
41
+ readonly matches: boolean;
42
+ /** Occurrences present in source but absent from the translation, one per dropped occurrence. Sorted. */
43
+ readonly missing: readonly string[];
44
+ /** Occurrences present in the translation but absent from source, one per surplus occurrence. Sorted. */
45
+ readonly extra: readonly string[];
46
+ /** Same multiset present in both, but in a different order. */
47
+ readonly reordered: boolean;
48
+ }
49
+
50
+ /** A provider is either a prompt-driven LLM or a dedicated machine-translation API. */
51
+ type ProviderKind = "llm" | "machine-translation";
52
+ /** Target tone for a translation. Maps to formality for machine-translation providers. */
53
+ type Tone = "formal" | "informal" | "neutral";
54
+ /**
55
+ * Produces the placeholder set of a value for the output integrity check. Supplied
56
+ * by the caller (the SDK) so it matches the entries' format; ai-providers never
57
+ * derives placeholders itself.
58
+ */
59
+ type PlaceholderExtractor = (value: string) => readonly string[];
60
+ /**
61
+ * A batch translation request. Format- and provider-neutral: it carries no prompt,
62
+ * model, key, or other provider-specific field. The placeholder extractor is
63
+ * mandatory (see validateRequest).
64
+ */
65
+ interface TranslateRequest {
66
+ /** BCP-47 source locale of the entries (for example, "en"). */
67
+ readonly sourceLocale: string;
68
+ /** BCP-47 target locale to translate into (for example, "de"). */
69
+ readonly targetLocale: string;
70
+ /** The entries to translate; at least one is required. */
71
+ readonly entries: readonly TranslationEntry$1[];
72
+ /** Optional source-term to target-term map applied by glossary-capable providers. */
73
+ readonly glossary?: Readonly<Record<string, string>>;
74
+ /** Optional target tone; machine-translation providers map it to formality. */
75
+ readonly tone?: Tone;
76
+ /** Mandatory placeholder extractor; the output integrity check runs against it. */
77
+ readonly extractPlaceholders: PlaceholderExtractor;
78
+ }
79
+ /** Token usage, when the provider reports it. Absent for providers without tokens (DeepL). */
80
+ interface Usage {
81
+ readonly inputTokens: number;
82
+ readonly outputTokens: number;
83
+ }
84
+ /** Result of a batch translation: per-key values and per-key integrity outcomes. */
85
+ interface TranslateResult {
86
+ /** The translated value for each requested key. */
87
+ readonly values: ReadonlyMap<string, string>;
88
+ /** The placeholder-integrity outcome for each key (source vs translated placeholder sets). */
89
+ readonly integrity: ReadonlyMap<string, PlaceholderIntegrityResult>;
90
+ /** Token usage when the provider reports it; absent for token-less providers. */
91
+ readonly usage?: Usage;
92
+ }
93
+ /**
94
+ * The single contract every provider implements. It is narrow enough that a machine-translation API like
95
+ * DeepL fits it directly, while LLM providers implement it by delegating to {@link runLlmTranslation}.
96
+ * A new provider attaches by implementing this and registering it in a {@link ProviderRegistry}.
97
+ *
98
+ * Implementer invariants:
99
+ * - Translatable strings are UNTRUSTED. They travel only as data to the provider; never splice them into
100
+ * instruction text, and never act on instructions a value appears to contain.
101
+ * - Read the API key ONLY from the environment (inside the SDK client). The request, config, and this
102
+ * interface never carry a key.
103
+ * - Fail with a secret-free {@link ProviderError}: never bind, log, or re-throw raw SDK error text (it can
104
+ * carry a key or request headers). Validate the request at the boundary with `validateRequest` so the
105
+ * integrity check can never be skipped.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * // A machine-translation provider implements translateBatch directly (the DeepL shape).
110
+ * function createMyMtProvider(client: MyClient): TranslationProvider {
111
+ * return {
112
+ * id: "my-mt",
113
+ * kind: "machine-translation",
114
+ * supportsGlossary: false,
115
+ * async translateBatch(request) {
116
+ * const data = validateRequest(request); // throws INVALID_REQUEST on a bad request
117
+ * const texts = data.entries.map((e) => e.value);
118
+ * const out = await client.translate(texts, data.targetLocale); // SDK reads MY_API_KEY from env
119
+ * // map out -> values, run the integrity check, return { values, integrity }
120
+ * return buildResult(data, out, request.extractPlaceholders);
121
+ * },
122
+ * };
123
+ * }
124
+ * ```
125
+ */
126
+ interface TranslationProvider {
127
+ /** A stable identifier for this provider (for example, "anthropic", "deepl"). */
128
+ readonly id: string;
129
+ /** Whether this provider is a prompt-driven LLM or a dedicated machine-translation API. */
130
+ readonly kind: ProviderKind;
131
+ /** Whether this provider applies a configured glossary. */
132
+ readonly supportsGlossary: boolean;
133
+ /**
134
+ * Translate a batch of entries.
135
+ *
136
+ * @param request - The provider-neutral batch request (no prompt, model, or key).
137
+ * @returns The per-key translated values and per-key placeholder-integrity outcomes.
138
+ * @throws {@link ProviderError}, secret-free, with the code for the failure (the concrete codes are
139
+ * the implementation's; see each provider factory).
140
+ */
141
+ translateBatch(request: TranslateRequest): Promise<TranslateResult>;
142
+ }
143
+
144
+ /**
145
+ * The Anthropic authoring model type, sourced from the SDK's own model union. It is an
146
+ * open union (known literals plus `string & {}`), so unknown or newly released model IDs
147
+ * are still accepted. This drives editor completions only; the runtime schema validates.
148
+ */
149
+ type AnthropicModel = Anthropic.Messages.Model;
150
+ /**
151
+ * Stable codes for the DeepL graceful-degradation notices, DeepL only. These are returned DATA on a
152
+ * successful result, NOT thrown:
153
+ *
154
+ * - `FORMALITY_DOWNGRADED`: a requested formality was not applied (the free tier does not support it).
155
+ * - `GLOSSARY_IGNORED`: a supplied generic glossary was not applied.
156
+ */
157
+ type ProviderNoticeCode = "FORMALITY_DOWNGRADED" | "GLOSSARY_IGNORED";
158
+ /**
159
+ * An observable, structured signal that something was gracefully degraded (not an
160
+ * error). Carries only a stable code and a static message, never a key or content.
161
+ * Surfaced as result data, never thrown; callers inspect it but need not treat it as a failure.
162
+ */
163
+ interface ProviderNotice {
164
+ /** The stable {@link ProviderNoticeCode} for what was degraded. */
165
+ readonly code: ProviderNoticeCode;
166
+ /** A static, safe description; never a key or translatable content. */
167
+ readonly message: string;
168
+ }
169
+
170
+ /**
171
+ * The Gemini authoring model type, sourced from the installed SDK's own model union so
172
+ * verbatra restates no model IDs. It is an open union, so unknown model IDs are still
173
+ * accepted. Type-only: it informs editor completions and is never validated at runtime,
174
+ * where the schema stays `z.string().min(1)`.
175
+ */
176
+ type GeminiModel = Interactions.Model;
177
+
178
+ /**
179
+ * The OpenAI authoring model type, sourced from the OpenAI SDK's published chat-model
180
+ * union. It drives editor completions only; it is never read at runtime, and the
181
+ * runtime schema stays `z.string().min(1)`.
182
+ */
183
+ type OpenAiModel = OpenAI.ChatModel;
5
184
 
6
185
  /**
7
186
  * The provider section of the config: a discriminated union over the provider id, reusing each
@@ -221,6 +400,171 @@ declare class SdkError extends Error {
221
400
  constructor(code: SdkErrorCode, message: string);
222
401
  }
223
402
 
403
+ /** Zod schema accepting exactly one of {@link SUPPORTED_FORMATS}. */
404
+ declare const supportedFormatSchema: z.ZodEnum<{
405
+ "i18next-json": "i18next-json";
406
+ "vue-i18n-json": "vue-i18n-json";
407
+ "next-intl-json": "next-intl-json";
408
+ "ngx-translate-json": "ngx-translate-json";
409
+ xliff: "xliff";
410
+ yaml: "yaml";
411
+ arb: "arb";
412
+ }>;
413
+ /** One of the supported source formats; a member of {@link SUPPORTED_FORMATS}. */
414
+ type SupportedFormat = z.infer<typeof supportedFormatSchema>;
415
+
416
+ /**
417
+ * A single, format-neutral translation unit. Placeholders are supplied already
418
+ * extracted; core never derives them from the value.
419
+ */
420
+ declare const translationEntrySchema: z.ZodObject<{
421
+ key: z.ZodString;
422
+ namespace: z.ZodString;
423
+ value: z.ZodString;
424
+ description: z.ZodOptional<z.ZodString>;
425
+ meaning: z.ZodOptional<z.ZodString>;
426
+ placeholders: z.ZodReadonly<z.ZodArray<z.ZodString>>;
427
+ isPlural: z.ZodBoolean;
428
+ }, z.core.$strip>;
429
+ /** The validated shape of one translation unit; the inferred type of {@link translationEntrySchema}. */
430
+ type TranslationEntry = Readonly<z.infer<typeof translationEntrySchema>>;
431
+ /** All translation entries for one locale and namespace, keyed by entry key. */
432
+ interface LocaleResource {
433
+ /** The locale these entries belong to (for example, "en" or "de"). */
434
+ readonly locale: string;
435
+ /** The namespace these entries belong to. */
436
+ readonly namespace: string;
437
+ /** The source format the resource came from, for round-trip fidelity. */
438
+ readonly format: SupportedFormat;
439
+ /** Entries addressable by key. */
440
+ readonly entries: ReadonlyMap<string, TranslationEntry>;
441
+ }
442
+
443
+ /**
444
+ * The result of reading a file into core's intermediate representation.
445
+ */
446
+ interface ReadResult {
447
+ /** The parsed locale resource. */
448
+ readonly resource: LocaleResource;
449
+ /** Keys whose values are invalid for the format's message syntax. Empty for non-ICU formats. */
450
+ readonly invalidIcuKeys: readonly string[];
451
+ }
452
+ /**
453
+ * The contract every format adapter implements. A new format attaches by implementing this interface
454
+ * and registering it in an {@link AdapterRegistry}.
455
+ *
456
+ * For a JSON-family format, implement via {@link createJsonFileAdapter} rather than from scratch. A
457
+ * non-JSON format implements this interface directly and first needs its format added to core's
458
+ * `SupportedFormat`.
459
+ *
460
+ * @example
461
+ * ```ts
462
+ * const registry = createDefaultRegistry();
463
+ * const resolution = registry.resolve("locales/en.json", { format: "i18next-json" });
464
+ * if (resolution.status === "resolved") {
465
+ * const { resource } = await resolution.adapter.read("locales/en.json", "en");
466
+ * await resolution.adapter.write(resource, "locales/en.json");
467
+ * }
468
+ * ```
469
+ */
470
+ interface FormatAdapter {
471
+ /** The single format this adapter handles (a `SupportedFormat` from core). */
472
+ readonly format: SupportedFormat;
473
+ /**
474
+ * Detect whether this adapter can handle a file, by path extension and an optional content sample.
475
+ * Best-effort and side-effect-free; it reads nothing from disk.
476
+ *
477
+ * @param filePath - The path of the file under consideration.
478
+ * @param sample - An optional leading content sample to aid detection.
479
+ * @returns True if this adapter could handle the file. Several adapters may return true for the same
480
+ * file (all JSON adapters claim `.json`); the caller disambiguates by explicit format.
481
+ */
482
+ canHandle(filePath: string, sample?: string): boolean;
483
+ /**
484
+ * Read a file into a {@link LocaleResource} plus its message-validity result.
485
+ *
486
+ * @param filePath - The file to read.
487
+ * @param locale - The locale to tag the resource with.
488
+ * @returns The parsed resource and the keys whose values are invalid for the format.
489
+ * @throws {@link AdapterError} when the content is malformed, oversized, or structurally invalid
490
+ * (the implementation names the specific codes). A missing or unopenable path instead rejects with
491
+ * the underlying filesystem error.
492
+ */
493
+ read(filePath: string, locale: string): Promise<ReadResult>;
494
+ /**
495
+ * Write a {@link LocaleResource} back to a file, preserving key order and structure, atomically.
496
+ *
497
+ * @param resource - The resource to serialize.
498
+ * @param filePath - The destination file.
499
+ * @throws {@link AdapterError} if the resource cannot be represented in the format; rejects with the
500
+ * underlying filesystem error on a write failure.
501
+ */
502
+ write(resource: LocaleResource, filePath: string): Promise<void>;
503
+ /**
504
+ * Extract the format's placeholder tokens from a single value, resolving nothing.
505
+ *
506
+ * @param value - The translatable string to scan.
507
+ * @returns The placeholder tokens found, in document order. Does not throw.
508
+ */
509
+ extractPlaceholders(value: string): readonly string[];
510
+ /**
511
+ * Validate a single value against the format's message syntax before it is written. Non-ICU formats
512
+ * (i18next, vue-i18n, ngx-translate) treat every value as valid and return true.
513
+ *
514
+ * @param value - The candidate translated value to validate.
515
+ * @returns True when the value is valid for the format's message syntax. Does not throw; an
516
+ * unparseable value returns false.
517
+ */
518
+ validateMessage(value: string): boolean;
519
+ }
520
+
521
+ /** Outcome of resolving an adapter for a file. Structured; never throws. */
522
+ type AdapterResolution = {
523
+ readonly status: "resolved";
524
+ readonly adapter: FormatAdapter;
525
+ } | {
526
+ readonly status: "no-match";
527
+ readonly filePath: string;
528
+ readonly triedFormats: readonly SupportedFormat[];
529
+ } | {
530
+ readonly status: "ambiguous";
531
+ readonly filePath: string;
532
+ readonly candidates: readonly SupportedFormat[];
533
+ };
534
+ /** Options for {@link AdapterRegistry.resolve}: select a format explicitly, or aid detection. */
535
+ interface ResolveOptions {
536
+ /** A content sample to aid detection. */
537
+ readonly sample?: string;
538
+ /** Bypass detection and select this format explicitly. */
539
+ readonly format?: SupportedFormat;
540
+ }
541
+ /**
542
+ * Holds the registered adapters and resolves one for a file. Open for extension:
543
+ * adapters attach through register without changing resolution logic.
544
+ */
545
+ declare class AdapterRegistry {
546
+ private readonly adapters;
547
+ /**
548
+ * Register an adapter.
549
+ *
550
+ * @param adapter - The adapter to add.
551
+ * @returns This registry, for chaining.
552
+ */
553
+ register(adapter: FormatAdapter): this;
554
+ private formats;
555
+ private resolveByFormat;
556
+ private resolveByDetection;
557
+ /**
558
+ * Resolve the adapter for a file, by explicit format when given, otherwise by detection.
559
+ *
560
+ * @param filePath - The file to resolve an adapter for.
561
+ * @param options - `format` selects explicitly and skips detection; `sample` aids detection.
562
+ * @returns A structured {@link AdapterResolution}: `resolved`, `no-match`, or `ambiguous`. Never
563
+ * throws; an unresolvable file is a status, not an exception.
564
+ */
565
+ resolve(filePath: string, options?: ResolveOptions): AdapterResolution;
566
+ }
567
+
224
568
  /** Outcome of a bounded read: the content, or why it could not be read in bounds. */
225
569
  type BoundedFileRead = {
226
570
  readonly kind: "ok";
@@ -706,4 +1050,4 @@ interface WatchController {
706
1050
  */
707
1051
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
708
1052
 
709
- export { type CheckDeps, type CheckInput, type CheckSummary, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleCheckSummary, type LocaleDiff, type LocaleNotice, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
1053
+ export { type CheckDeps, type CheckInput, type CheckSummary, type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type DiffDeps, type DiffInput, type DiffSummary, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleCheckSummary, type LocaleDiff, type LocaleNotice, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type SdkNotice, type SdkNoticeCode, type SupportedFormat$1 as SupportedFormat, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, check, defineConfig, diff, exportWorkbook, importWorkbook, loadConfig, scaffoldingMetadata, translate, verbatraConfigSchema, watch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verbatra/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Programmatic API to automate i18n translation and keep your locale files in sync across languages with AI and machine-translation providers.",
6
6
  "keywords": [
@@ -79,10 +79,10 @@
79
79
  "typescript": "6.0.3",
80
80
  "vitest": "4.1.9",
81
81
  "@verbatra/ai-providers": "0.1.3",
82
+ "@verbatra/format-adapters": "0.3.0",
82
83
  "@verbatra/config": "0.0.0",
83
84
  "@verbatra/core": "0.1.1",
84
- "@verbatra/exchange": "0.2.1",
85
- "@verbatra/format-adapters": "0.3.0"
85
+ "@verbatra/exchange": "0.2.1"
86
86
  },
87
87
  "scripts": {
88
88
  "build": "tsup",