@verbatra/sdk 0.2.2 → 0.3.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.
package/dist/index.d.cts CHANGED
@@ -38,12 +38,6 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
38
38
  type ProviderConfig = z.infer<typeof providerConfigSchema>;
39
39
  type ProviderId = ProviderConfig["id"];
40
40
 
41
- /**
42
- * The verbatra project configuration. Non-secret only: it carries no API key (the
43
- * provider reads its key from the environment), and unknown top-level keys are rejected
44
- * so a stray secret cannot hide in the config. Validated with zod at the boundary
45
- * regardless of where it was loaded from.
46
- */
47
41
  declare const verbatraConfigSchema: z.ZodObject<{
48
42
  sourceLocale: z.ZodString;
49
43
  targetLocales: z.ZodArray<z.ZodString>;
@@ -86,27 +80,24 @@ declare const verbatraConfigSchema: z.ZodObject<{
86
80
  informal: "informal";
87
81
  neutral: "neutral";
88
82
  }>>;
83
+ prune: z.ZodOptional<z.ZodBoolean>;
84
+ generatePlurals: z.ZodOptional<z.ZodBoolean>;
85
+ maxBatchSize: z.ZodOptional<z.ZodNumber>;
89
86
  }, z.core.$strict>;
90
87
  type VerbatraConfig = z.infer<typeof verbatraConfigSchema>;
91
88
 
92
89
  /**
93
- * An open union over a provider's known model IDs: the suggested IDs plus any other
94
- * non-empty string. The `string & {}` arm keeps the union from collapsing to plain
95
- * `string` (so editors still surface the literals as completions) while accepting a
96
- * brand-new model ID the tool has never heard of. This is a static authoring hint
97
- * only; the runtime schema stays `z.string().min(1)` and validates nothing against
98
- * these literals.
99
- */
100
- type OpenModel<M extends string> = M | (string & {});
101
- /**
102
- * The authoring view of one provider variant: its runtime config with `options.model`
103
- * narrowed to that provider's open model union. LLM providers (anthropic, openai,
104
- * gemini) get suggestions; DeepL has no model field and is carried through unchanged.
90
+ * The closed set of a provider's known model IDs: exactly the string literals its SDK
91
+ * model type ships, with the SDK's own open `string & {}` arm stripped out. Distributing
92
+ * over the union and dropping any arm the wide `string` extends leaves only the literals,
93
+ * so the authoring field offers and ACCEPTS only the selected provider's known models. A
94
+ * foreign or unknown model (for example a Claude model under `id: "gemini"`) is a type
95
+ * error at authoring time. This is a static authoring constraint only; the runtime schema
96
+ * stays `z.string().min(1)` and still accepts any non-empty string, so a brand-new model
97
+ * the installed SDK does not yet list is flagged in the editor but still runs.
105
98
  */
106
- type AuthoringProviderConfig = AuthoringVariant<"anthropic", AnthropicModel> | AuthoringVariant<"openai", OpenAiModel> | AuthoringVariant<"gemini", GeminiModel> | Extract<ProviderConfig, {
107
- id: "deepl";
108
- }>;
109
- type AuthoringVariant<Id extends ProviderConfig["id"], M extends string> = Extract<ProviderConfig, {
99
+ type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
100
+ type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
110
101
  id: Id;
111
102
  }> extends infer Variant ? Variant extends {
112
103
  options: {
@@ -114,30 +105,80 @@ type AuthoringVariant<Id extends ProviderConfig["id"], M extends string> = Extra
114
105
  };
115
106
  } ? Omit<Variant, "options"> & {
116
107
  options: Omit<Variant["options"], "model"> & {
117
- model: OpenModel<M>;
108
+ model: KnownModels<M>;
118
109
  };
119
110
  } : never : never;
120
111
  /**
121
- * The authoring view of the whole config: identical to {@link VerbatraConfig} except
122
- * that `provider` offers per-provider model completions. Every value assignable to
123
- * this type is assignable to {@link VerbatraConfig}, because the open model union is a
124
- * subtype of `string`; `defineConfig` returns the runtime {@link VerbatraConfig}.
112
+ * The authoring view of one provider variant, keyed by id. LLM providers (anthropic,
113
+ * openai, gemini) restrict `options.model` to that provider's known model literals; DeepL
114
+ * has no model field and is carried through unchanged. Keying by id (rather than a flat
115
+ * union) lets {@link AuthoringConfigFor} select exactly one variant for a literal id, so
116
+ * the `options.model` site is one provider's literal set and never a multi-provider union.
125
117
  */
126
- type AuthoringConfig = Omit<VerbatraConfig, "provider"> & {
127
- provider: AuthoringProviderConfig;
118
+ type AuthoringProviderVariant = {
119
+ anthropic: AuthoringVariant<"anthropic", AnthropicModel>;
120
+ openai: AuthoringVariant<"openai", OpenAiModel>;
121
+ gemini: AuthoringVariant<"gemini", GeminiModel>;
122
+ deepl: Extract<ProviderConfig, {
123
+ id: "deepl";
124
+ }>;
128
125
  };
126
+ /**
127
+ * The authoring view of the whole config for a given provider id. It is structurally a
128
+ * {@link VerbatraConfig} whose `provider` is the single authoring variant for `TId`.
129
+ *
130
+ * When `TId` is a single provider literal, `provider` is one concrete variant, so
131
+ * `options.model` is that provider's known model literals alone and not a union across
132
+ * providers. `defineConfig` declares one overload per provider parameterized on this type
133
+ * (`AuthoringConfigFor<"openai">` and so on), so overload resolution picks the variant from
134
+ * the `provider.id` literal. That, together with the closed {@link KnownModels} set, makes a
135
+ * foreign model (for example a Claude model under `id: "gemini"`) a type error and aims to
136
+ * keep editors with weaker discriminated-union narrowing (for example the JetBrains/WebStorm
137
+ * completion engine) offering only the selected provider's models, since each overload's
138
+ * parameter is already a single variant with no nested union to narrow. When `TId` defaults
139
+ * to `ProviderId`, `provider` is the full authoring union. Every value assignable here is
140
+ * assignable to {@link VerbatraConfig}, because a model literal is a subtype of `string`.
141
+ */
142
+ type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfig, "provider"> & {
143
+ provider: AuthoringProviderVariant[TId];
144
+ };
145
+ /**
146
+ * The authoring view of the whole config across every provider (the `TId = ProviderId`
147
+ * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfig} except that
148
+ * `provider` offers per-provider model completions.
149
+ */
150
+ type AuthoringConfig = AuthoringConfigFor;
129
151
 
130
152
  /**
131
153
  * Identity helper for authoring a code-defined verbatra.config.ts. It returns its
132
154
  * argument unchanged; its only purpose is to give the author full type inference and
133
155
  * editor autocomplete on the config object.
134
156
  *
135
- * The argument type is {@link AuthoringConfig}, which is structurally a
136
- * {@link VerbatraConfig} whose `provider.options.model` offers the selected provider's
137
- * known model IDs as completions while still accepting any other string. The return
138
- * type is the runtime {@link VerbatraConfig}: the suggestions are a static authoring
139
- * hint, not a runtime constraint, and `loadConfig` validates `model` exactly as before.
157
+ * It is declared as one overload per provider id rather than a single generic. Each
158
+ * overload's parameter is the concrete single-provider authoring config
159
+ * ({@link AuthoringConfigFor}), so `provider.options.model` is already that one provider's
160
+ * known model literals, with no union across providers and no generic for an editor to
161
+ * infer. Overload resolution selects the matching overload from the `provider.id` literal,
162
+ * so the editor offers only the selected provider's models and a foreign or unknown model
163
+ * (for example a Claude model under `id: "gemini"`) is a type error. This is deliberately
164
+ * overload-based: a generic whose type parameter is inferred from a nested `provider.id`
165
+ * collapses correctly in tsserver but not in editors with weaker inference (notably the
166
+ * JetBrains/WebStorm completion engine), which then fall back to the full union and offer
167
+ * every provider's models. Concrete per-provider signatures avoid that inference step.
168
+ *
169
+ * The final overload accepts the full {@link AuthoringConfig} union, so a config whose
170
+ * provider id is not a single literal (for example a value typed as the union) still
171
+ * type-checks.
172
+ *
173
+ * The return type is the runtime {@link VerbatraConfig}. The model restriction is a static
174
+ * authoring constraint, not a runtime one: `loadConfig` still validates `model` as
175
+ * `z.string().min(1)`, so a model the installed provider SDK does not yet list is flagged
176
+ * in the editor but still runs.
140
177
  */
178
+ declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfig;
179
+ declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfig;
180
+ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfig;
181
+ declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfig;
141
182
  declare function defineConfig(config: AuthoringConfig): VerbatraConfig;
142
183
 
143
184
  interface LoadConfigOptions {
@@ -192,16 +233,16 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
192
233
  * any message: provider/adapter/core errors are already secret-free, and the SDK never
193
234
  * reads or holds a key. Each names a distinct boundary:
194
235
  *
195
- * - `CONFIG_NOT_FOUND`:no config was found by search, or an explicit `configPath` does not exist
236
+ * - `CONFIG_NOT_FOUND`: no config was found by search, or an explicit `configPath` does not exist
196
237
  * (thrown by `loadConfig`).
197
- * - `CONFIG_INVALID`:a config was found but is unparseable or fails validation (thrown by `loadConfig`).
198
- * - `UNKNOWN_FORMAT`:no adapter is registered for the configured format (thrown by `translate`).
199
- * - `PROVIDER_CONSTRUCTION_FAILED`:the provider factory threw; wraps the provider's own error, including
238
+ * - `CONFIG_INVALID`: a config was found but is unparseable or fails validation (thrown by `loadConfig`).
239
+ * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format (thrown by `translate`).
240
+ * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw; wraps the provider's own error, including
200
241
  * a missing `*_API_KEY` reported as `MISSING_API_KEY` (thrown by `translate`, non-dry-run only).
201
- * - `SOURCE_UNREADABLE`:the source locale file is absent (thrown by `translate`, and by `watch` at startup).
202
- * - `SOURCE_INVALID`:the source locale file could not be read or parsed; wraps the adapter read error
242
+ * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by `translate`, and by `watch` at startup).
243
+ * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read error
203
244
  * (thrown by `translate`).
204
- * - `LOCK_FILE_INVALID`:the lock-file is present but corrupt or oversized (thrown by `translate`).
245
+ * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized (thrown by `translate`).
205
246
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
206
247
  * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction on
207
248
  * `translate`.
@@ -218,6 +259,23 @@ declare class SdkError extends Error {
218
259
  constructor(code: SdkErrorCode, message: string);
219
260
  }
220
261
 
262
+ /** A stable code for an SDK-originated notice (not a provider notice). */
263
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED";
264
+ /**
265
+ * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
266
+ * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
267
+ * secret-free message; never a key value or translatable content. The SDK keeps its own codes here so
268
+ * the ai-providers `ProviderNoticeCode` union stays free of SDK concerns (the dependency arrow points
269
+ * sdk -> ai-providers, never the reverse).
270
+ */
271
+ interface SdkNotice {
272
+ /** The stable {@link SdkNoticeCode} for what the SDK is reporting. */
273
+ readonly code: SdkNoticeCode;
274
+ /** A static, safe description; never a key or translatable content. */
275
+ readonly message: string;
276
+ }
277
+ /** A notice on a locale summary: either a provider-emitted notice or an SDK-emitted one. */
278
+ type LocaleNotice = ProviderNotice | SdkNotice;
221
279
  /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
222
280
  interface LocaleSummary {
223
281
  /** The target locale this summary is for. */
@@ -231,14 +289,31 @@ interface LocaleSummary {
231
289
  readonly translated: readonly string[];
232
290
  /** Keys already up to date, left unchanged this run. */
233
291
  readonly unchanged: readonly string[];
234
- /** Target keys with no corresponding source key (candidates for removal). */
292
+ /** Target keys with no corresponding source key (candidates for removal). Reported regardless of pruning. */
235
293
  readonly orphaned: readonly string[];
294
+ /**
295
+ * Orphaned keys actually removed this run because pruning was on. In a dry-run with pruning on, the keys
296
+ * that WOULD be removed. Empty when pruning is off (the orphans then survive and are reported in
297
+ * `orphaned` only). A subset of `orphaned`; never includes a source-present key.
298
+ */
299
+ readonly pruned: readonly string[];
236
300
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
237
301
  readonly invalidIcuSource: readonly string[];
238
302
  /** Translated keys that failed the placeholder-integrity check and were withheld. */
239
303
  readonly integrityMismatches: readonly string[];
240
- /** Provider notices for this locale (e.g. DeepL graceful-degradation); empty for LLM providers. */
241
- readonly notices: readonly ProviderNotice[];
304
+ /**
305
+ * Plural-category keys verbatra SYNTHESIZED this run (for example a Polish `items_few` the source
306
+ * never supplied), kept distinct from {@link translated} because they were generated from the meaning
307
+ * of the source plural forms, not translated 1:1 from a source string. Empty unless plural generation
308
+ * was enabled and acted. In a dry-run this is empty (the provider is not called).
309
+ */
310
+ readonly generated: readonly string[];
311
+ /**
312
+ * Notices for this locale: provider notices (e.g. DeepL graceful-degradation) and SDK notices
313
+ * (e.g. a target language needing more CLDR plural categories than the source supplies). Empty when
314
+ * nothing was degraded or flagged.
315
+ */
316
+ readonly notices: readonly LocaleNotice[];
242
317
  /**
243
318
  * Present only when status is "failed": a structured, secret-free error. `code` is a PRESERVED string
244
319
  * (the underlying provider/adapter error's `code`, or `"LOCALE_FAILED"` as a fallback), intentionally
@@ -320,6 +395,19 @@ interface TranslateInput {
320
395
  readonly cwd?: string;
321
396
  /** When true, read + diff + report only: the provider is never constructed or called and nothing is written. */
322
397
  readonly dryRun?: boolean;
398
+ /**
399
+ * When true, remove orphaned keys (target keys absent from source) from the written file and the lock.
400
+ * Off by default. When set, takes precedence over the config's `prune` option for this run; when unset,
401
+ * the config's `prune` applies. Only `diff.orphaned` keys are ever removed; no other key is touched.
402
+ */
403
+ readonly prune?: boolean;
404
+ /**
405
+ * When true, synthesize the CLDR plural forms a richer target language requires but the source lacks
406
+ * (i18next-JSON + LLM providers only; every other case falls back to the existing warning). Off by
407
+ * default. When set, takes precedence over the config's `generatePlurals` option for this run; when
408
+ * unset, the config's `generatePlurals` applies.
409
+ */
410
+ readonly generatePlurals?: boolean;
323
411
  }
324
412
  /** Composition seam: inject a registry, a provider builder, and a file system for tests. */
325
413
  interface TranslateDeps {
@@ -344,7 +432,7 @@ interface TranslateDeps {
344
432
  * {@link SdkErrorCode}. DeepL notices, integrity mismatches, and invalid-ICU source keys likewise
345
433
  * surface on each `LocaleSummary`, never as throws.
346
434
  *
347
- * @param input - The validated config and run options (cwd, dryRun).
435
+ * @param input - The validated config and run options (cwd, dryRun, prune, generatePlurals).
348
436
  * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
349
437
  * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/failed locale lists.
350
438
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
@@ -470,7 +558,7 @@ interface Watcher {
470
558
  }
471
559
  /** Builds a {@link Watcher} for the given paths; the seam production fills with chokidar. */
472
560
  type CreateWatcher = (paths: readonly string[]) => Watcher;
473
- /** The run a watch trigger performs: the slice-1 one-shot translate, unchanged. */
561
+ /** The run a watch trigger performs: the one-shot translate, unchanged. */
474
562
  type RunTranslate = (input: TranslateInput) => Promise<RunSummary>;
475
563
  /** The outcome of one run, surfaced to the caller; never carries a secret. */
476
564
  type WatchRunResult = {
@@ -558,4 +646,4 @@ interface WatchController {
558
646
  */
559
647
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
560
648
 
561
- export { type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate, verbatraConfigSchema, watch };
649
+ export { type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, 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, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate, verbatraConfigSchema, watch };
package/dist/index.d.ts CHANGED
@@ -38,12 +38,6 @@ declare const providerConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
38
38
  type ProviderConfig = z.infer<typeof providerConfigSchema>;
39
39
  type ProviderId = ProviderConfig["id"];
40
40
 
41
- /**
42
- * The verbatra project configuration. Non-secret only: it carries no API key (the
43
- * provider reads its key from the environment), and unknown top-level keys are rejected
44
- * so a stray secret cannot hide in the config. Validated with zod at the boundary
45
- * regardless of where it was loaded from.
46
- */
47
41
  declare const verbatraConfigSchema: z.ZodObject<{
48
42
  sourceLocale: z.ZodString;
49
43
  targetLocales: z.ZodArray<z.ZodString>;
@@ -86,27 +80,24 @@ declare const verbatraConfigSchema: z.ZodObject<{
86
80
  informal: "informal";
87
81
  neutral: "neutral";
88
82
  }>>;
83
+ prune: z.ZodOptional<z.ZodBoolean>;
84
+ generatePlurals: z.ZodOptional<z.ZodBoolean>;
85
+ maxBatchSize: z.ZodOptional<z.ZodNumber>;
89
86
  }, z.core.$strict>;
90
87
  type VerbatraConfig = z.infer<typeof verbatraConfigSchema>;
91
88
 
92
89
  /**
93
- * An open union over a provider's known model IDs: the suggested IDs plus any other
94
- * non-empty string. The `string & {}` arm keeps the union from collapsing to plain
95
- * `string` (so editors still surface the literals as completions) while accepting a
96
- * brand-new model ID the tool has never heard of. This is a static authoring hint
97
- * only; the runtime schema stays `z.string().min(1)` and validates nothing against
98
- * these literals.
99
- */
100
- type OpenModel<M extends string> = M | (string & {});
101
- /**
102
- * The authoring view of one provider variant: its runtime config with `options.model`
103
- * narrowed to that provider's open model union. LLM providers (anthropic, openai,
104
- * gemini) get suggestions; DeepL has no model field and is carried through unchanged.
90
+ * The closed set of a provider's known model IDs: exactly the string literals its SDK
91
+ * model type ships, with the SDK's own open `string & {}` arm stripped out. Distributing
92
+ * over the union and dropping any arm the wide `string` extends leaves only the literals,
93
+ * so the authoring field offers and ACCEPTS only the selected provider's known models. A
94
+ * foreign or unknown model (for example a Claude model under `id: "gemini"`) is a type
95
+ * error at authoring time. This is a static authoring constraint only; the runtime schema
96
+ * stays `z.string().min(1)` and still accepts any non-empty string, so a brand-new model
97
+ * the installed SDK does not yet list is flagged in the editor but still runs.
105
98
  */
106
- type AuthoringProviderConfig = AuthoringVariant<"anthropic", AnthropicModel> | AuthoringVariant<"openai", OpenAiModel> | AuthoringVariant<"gemini", GeminiModel> | Extract<ProviderConfig, {
107
- id: "deepl";
108
- }>;
109
- type AuthoringVariant<Id extends ProviderConfig["id"], M extends string> = Extract<ProviderConfig, {
99
+ type KnownModels<M extends string> = M extends string ? (string extends M ? never : M) : never;
100
+ type AuthoringVariant<Id extends ProviderId, M extends string> = Extract<ProviderConfig, {
110
101
  id: Id;
111
102
  }> extends infer Variant ? Variant extends {
112
103
  options: {
@@ -114,30 +105,80 @@ type AuthoringVariant<Id extends ProviderConfig["id"], M extends string> = Extra
114
105
  };
115
106
  } ? Omit<Variant, "options"> & {
116
107
  options: Omit<Variant["options"], "model"> & {
117
- model: OpenModel<M>;
108
+ model: KnownModels<M>;
118
109
  };
119
110
  } : never : never;
120
111
  /**
121
- * The authoring view of the whole config: identical to {@link VerbatraConfig} except
122
- * that `provider` offers per-provider model completions. Every value assignable to
123
- * this type is assignable to {@link VerbatraConfig}, because the open model union is a
124
- * subtype of `string`; `defineConfig` returns the runtime {@link VerbatraConfig}.
112
+ * The authoring view of one provider variant, keyed by id. LLM providers (anthropic,
113
+ * openai, gemini) restrict `options.model` to that provider's known model literals; DeepL
114
+ * has no model field and is carried through unchanged. Keying by id (rather than a flat
115
+ * union) lets {@link AuthoringConfigFor} select exactly one variant for a literal id, so
116
+ * the `options.model` site is one provider's literal set and never a multi-provider union.
125
117
  */
126
- type AuthoringConfig = Omit<VerbatraConfig, "provider"> & {
127
- provider: AuthoringProviderConfig;
118
+ type AuthoringProviderVariant = {
119
+ anthropic: AuthoringVariant<"anthropic", AnthropicModel>;
120
+ openai: AuthoringVariant<"openai", OpenAiModel>;
121
+ gemini: AuthoringVariant<"gemini", GeminiModel>;
122
+ deepl: Extract<ProviderConfig, {
123
+ id: "deepl";
124
+ }>;
128
125
  };
126
+ /**
127
+ * The authoring view of the whole config for a given provider id. It is structurally a
128
+ * {@link VerbatraConfig} whose `provider` is the single authoring variant for `TId`.
129
+ *
130
+ * When `TId` is a single provider literal, `provider` is one concrete variant, so
131
+ * `options.model` is that provider's known model literals alone and not a union across
132
+ * providers. `defineConfig` declares one overload per provider parameterized on this type
133
+ * (`AuthoringConfigFor<"openai">` and so on), so overload resolution picks the variant from
134
+ * the `provider.id` literal. That, together with the closed {@link KnownModels} set, makes a
135
+ * foreign model (for example a Claude model under `id: "gemini"`) a type error and aims to
136
+ * keep editors with weaker discriminated-union narrowing (for example the JetBrains/WebStorm
137
+ * completion engine) offering only the selected provider's models, since each overload's
138
+ * parameter is already a single variant with no nested union to narrow. When `TId` defaults
139
+ * to `ProviderId`, `provider` is the full authoring union. Every value assignable here is
140
+ * assignable to {@link VerbatraConfig}, because a model literal is a subtype of `string`.
141
+ */
142
+ type AuthoringConfigFor<TId extends ProviderId = ProviderId> = Omit<VerbatraConfig, "provider"> & {
143
+ provider: AuthoringProviderVariant[TId];
144
+ };
145
+ /**
146
+ * The authoring view of the whole config across every provider (the `TId = ProviderId`
147
+ * case of {@link AuthoringConfigFor}): identical to {@link VerbatraConfig} except that
148
+ * `provider` offers per-provider model completions.
149
+ */
150
+ type AuthoringConfig = AuthoringConfigFor;
129
151
 
130
152
  /**
131
153
  * Identity helper for authoring a code-defined verbatra.config.ts. It returns its
132
154
  * argument unchanged; its only purpose is to give the author full type inference and
133
155
  * editor autocomplete on the config object.
134
156
  *
135
- * The argument type is {@link AuthoringConfig}, which is structurally a
136
- * {@link VerbatraConfig} whose `provider.options.model` offers the selected provider's
137
- * known model IDs as completions while still accepting any other string. The return
138
- * type is the runtime {@link VerbatraConfig}: the suggestions are a static authoring
139
- * hint, not a runtime constraint, and `loadConfig` validates `model` exactly as before.
157
+ * It is declared as one overload per provider id rather than a single generic. Each
158
+ * overload's parameter is the concrete single-provider authoring config
159
+ * ({@link AuthoringConfigFor}), so `provider.options.model` is already that one provider's
160
+ * known model literals, with no union across providers and no generic for an editor to
161
+ * infer. Overload resolution selects the matching overload from the `provider.id` literal,
162
+ * so the editor offers only the selected provider's models and a foreign or unknown model
163
+ * (for example a Claude model under `id: "gemini"`) is a type error. This is deliberately
164
+ * overload-based: a generic whose type parameter is inferred from a nested `provider.id`
165
+ * collapses correctly in tsserver but not in editors with weaker inference (notably the
166
+ * JetBrains/WebStorm completion engine), which then fall back to the full union and offer
167
+ * every provider's models. Concrete per-provider signatures avoid that inference step.
168
+ *
169
+ * The final overload accepts the full {@link AuthoringConfig} union, so a config whose
170
+ * provider id is not a single literal (for example a value typed as the union) still
171
+ * type-checks.
172
+ *
173
+ * The return type is the runtime {@link VerbatraConfig}. The model restriction is a static
174
+ * authoring constraint, not a runtime one: `loadConfig` still validates `model` as
175
+ * `z.string().min(1)`, so a model the installed provider SDK does not yet list is flagged
176
+ * in the editor but still runs.
140
177
  */
178
+ declare function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfig;
179
+ declare function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfig;
180
+ declare function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfig;
181
+ declare function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfig;
141
182
  declare function defineConfig(config: AuthoringConfig): VerbatraConfig;
142
183
 
143
184
  interface LoadConfigOptions {
@@ -192,16 +233,16 @@ declare function loadConfig(options?: LoadConfigOptions): Promise<VerbatraConfig
192
233
  * any message: provider/adapter/core errors are already secret-free, and the SDK never
193
234
  * reads or holds a key. Each names a distinct boundary:
194
235
  *
195
- * - `CONFIG_NOT_FOUND`:no config was found by search, or an explicit `configPath` does not exist
236
+ * - `CONFIG_NOT_FOUND`: no config was found by search, or an explicit `configPath` does not exist
196
237
  * (thrown by `loadConfig`).
197
- * - `CONFIG_INVALID`:a config was found but is unparseable or fails validation (thrown by `loadConfig`).
198
- * - `UNKNOWN_FORMAT`:no adapter is registered for the configured format (thrown by `translate`).
199
- * - `PROVIDER_CONSTRUCTION_FAILED`:the provider factory threw; wraps the provider's own error, including
238
+ * - `CONFIG_INVALID`: a config was found but is unparseable or fails validation (thrown by `loadConfig`).
239
+ * - `UNKNOWN_FORMAT`: no adapter is registered for the configured format (thrown by `translate`).
240
+ * - `PROVIDER_CONSTRUCTION_FAILED`: the provider factory threw; wraps the provider's own error, including
200
241
  * a missing `*_API_KEY` reported as `MISSING_API_KEY` (thrown by `translate`, non-dry-run only).
201
- * - `SOURCE_UNREADABLE`:the source locale file is absent (thrown by `translate`, and by `watch` at startup).
202
- * - `SOURCE_INVALID`:the source locale file could not be read or parsed; wraps the adapter read error
242
+ * - `SOURCE_UNREADABLE`: the source locale file is absent (thrown by `translate`, and by `watch` at startup).
243
+ * - `SOURCE_INVALID`: the source locale file could not be read or parsed; wraps the adapter read error
203
244
  * (thrown by `translate`).
204
- * - `LOCK_FILE_INVALID`:the lock-file is present but corrupt or oversized (thrown by `translate`).
245
+ * - `LOCK_FILE_INVALID`: the lock-file is present but corrupt or oversized (thrown by `translate`).
205
246
  * - `LOCALE_FAILED` (NOT thrown): the fallback `code` recorded on a failed `LocaleSummary` when a
206
247
  * per-locale failure carries no string code of its own. See the surfaced-not-thrown distinction on
207
248
  * `translate`.
@@ -218,6 +259,23 @@ declare class SdkError extends Error {
218
259
  constructor(code: SdkErrorCode, message: string);
219
260
  }
220
261
 
262
+ /** A stable code for an SDK-originated notice (not a provider notice). */
263
+ type SdkNoticeCode = "PLURAL_CATEGORIES_INCOMPLETE" | "SUB_BATCH_FAILED";
264
+ /**
265
+ * A notice raised by the SDK itself (not a provider), structurally identical to a {@link ProviderNotice}
266
+ * so both share the {@link LocaleSummary.notices} channel. Carries only a stable code and a static,
267
+ * secret-free message; never a key value or translatable content. The SDK keeps its own codes here so
268
+ * the ai-providers `ProviderNoticeCode` union stays free of SDK concerns (the dependency arrow points
269
+ * sdk -> ai-providers, never the reverse).
270
+ */
271
+ interface SdkNotice {
272
+ /** The stable {@link SdkNoticeCode} for what the SDK is reporting. */
273
+ readonly code: SdkNoticeCode;
274
+ /** A static, safe description; never a key or translatable content. */
275
+ readonly message: string;
276
+ }
277
+ /** A notice on a locale summary: either a provider-emitted notice or an SDK-emitted one. */
278
+ type LocaleNotice = ProviderNotice | SdkNotice;
221
279
  /** Structured outcome for one target locale; surfaced as data on the run, never thrown. */
222
280
  interface LocaleSummary {
223
281
  /** The target locale this summary is for. */
@@ -231,14 +289,31 @@ interface LocaleSummary {
231
289
  readonly translated: readonly string[];
232
290
  /** Keys already up to date, left unchanged this run. */
233
291
  readonly unchanged: readonly string[];
234
- /** Target keys with no corresponding source key (candidates for removal). */
292
+ /** Target keys with no corresponding source key (candidates for removal). Reported regardless of pruning. */
235
293
  readonly orphaned: readonly string[];
294
+ /**
295
+ * Orphaned keys actually removed this run because pruning was on. In a dry-run with pruning on, the keys
296
+ * that WOULD be removed. Empty when pruning is off (the orphans then survive and are reported in
297
+ * `orphaned` only). A subset of `orphaned`; never includes a source-present key.
298
+ */
299
+ readonly pruned: readonly string[];
236
300
  /** Source keys flagged invalid-ICU that were skipped for translation this run. */
237
301
  readonly invalidIcuSource: readonly string[];
238
302
  /** Translated keys that failed the placeholder-integrity check and were withheld. */
239
303
  readonly integrityMismatches: readonly string[];
240
- /** Provider notices for this locale (e.g. DeepL graceful-degradation); empty for LLM providers. */
241
- readonly notices: readonly ProviderNotice[];
304
+ /**
305
+ * Plural-category keys verbatra SYNTHESIZED this run (for example a Polish `items_few` the source
306
+ * never supplied), kept distinct from {@link translated} because they were generated from the meaning
307
+ * of the source plural forms, not translated 1:1 from a source string. Empty unless plural generation
308
+ * was enabled and acted. In a dry-run this is empty (the provider is not called).
309
+ */
310
+ readonly generated: readonly string[];
311
+ /**
312
+ * Notices for this locale: provider notices (e.g. DeepL graceful-degradation) and SDK notices
313
+ * (e.g. a target language needing more CLDR plural categories than the source supplies). Empty when
314
+ * nothing was degraded or flagged.
315
+ */
316
+ readonly notices: readonly LocaleNotice[];
242
317
  /**
243
318
  * Present only when status is "failed": a structured, secret-free error. `code` is a PRESERVED string
244
319
  * (the underlying provider/adapter error's `code`, or `"LOCALE_FAILED"` as a fallback), intentionally
@@ -320,6 +395,19 @@ interface TranslateInput {
320
395
  readonly cwd?: string;
321
396
  /** When true, read + diff + report only: the provider is never constructed or called and nothing is written. */
322
397
  readonly dryRun?: boolean;
398
+ /**
399
+ * When true, remove orphaned keys (target keys absent from source) from the written file and the lock.
400
+ * Off by default. When set, takes precedence over the config's `prune` option for this run; when unset,
401
+ * the config's `prune` applies. Only `diff.orphaned` keys are ever removed; no other key is touched.
402
+ */
403
+ readonly prune?: boolean;
404
+ /**
405
+ * When true, synthesize the CLDR plural forms a richer target language requires but the source lacks
406
+ * (i18next-JSON + LLM providers only; every other case falls back to the existing warning). Off by
407
+ * default. When set, takes precedence over the config's `generatePlurals` option for this run; when
408
+ * unset, the config's `generatePlurals` applies.
409
+ */
410
+ readonly generatePlurals?: boolean;
323
411
  }
324
412
  /** Composition seam: inject a registry, a provider builder, and a file system for tests. */
325
413
  interface TranslateDeps {
@@ -344,7 +432,7 @@ interface TranslateDeps {
344
432
  * {@link SdkErrorCode}. DeepL notices, integrity mismatches, and invalid-ICU source keys likewise
345
433
  * surface on each `LocaleSummary`, never as throws.
346
434
  *
347
- * @param input - The validated config and run options (cwd, dryRun).
435
+ * @param input - The validated config and run options (cwd, dryRun, prune, generatePlurals).
348
436
  * @param deps - Optional composition seams (registry, provider builder, file system) for tests.
349
437
  * @returns A {@link RunSummary}: the per-locale {@link LocaleSummary}s and the succeeded/failed locale lists.
350
438
  * @throws {@link SdkError} `UNKNOWN_FORMAT`: no adapter is registered for the configured format.
@@ -470,7 +558,7 @@ interface Watcher {
470
558
  }
471
559
  /** Builds a {@link Watcher} for the given paths; the seam production fills with chokidar. */
472
560
  type CreateWatcher = (paths: readonly string[]) => Watcher;
473
- /** The run a watch trigger performs: the slice-1 one-shot translate, unchanged. */
561
+ /** The run a watch trigger performs: the one-shot translate, unchanged. */
474
562
  type RunTranslate = (input: TranslateInput) => Promise<RunSummary>;
475
563
  /** The outcome of one run, surfaced to the caller; never carries a secret. */
476
564
  type WatchRunResult = {
@@ -558,4 +646,4 @@ interface WatchController {
558
646
  */
559
647
  declare function watch(input: WatchInput, deps?: WatchDeps): Promise<WatchController>;
560
648
 
561
- export { type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, type LocaleSummary, type ProviderConfig, type ProviderId, type RunSummary, type RunTranslate, SdkError, type SdkErrorCode, type SdkFs, type TranslateDeps, type TranslateInput, type VerbatraConfig, type WatchController, type WatchDeps, type WatchInput, type WatchRunResult, type Watcher, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate, verbatraConfigSchema, watch };
649
+ export { type CreateProvider, type CreateWatcher, DEFAULT_WORKBOOK_PATH, type ExportWorkbookDeps, type ExportWorkbookInput, type ExportWorkbookResult, type ImportWorkbookDeps, type ImportWorkbookInput, type LoadConfigOptions, 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, defineConfig, exportWorkbook, importWorkbook, loadConfig, translate, verbatraConfigSchema, watch };