---
title: "Config and glossary"
description: "SDK entry points that load and validate the verbatra config and edit the glossary, plus the one that detects whether a directory holds a project."
type: "reference"
---
# Config and glossary

SDK entry points that load and validate the verbatra config and edit the glossary, plus the one that detects whether a directory holds a project.

## Load the config [#config]

### loadConfig [#loadconfig]

Finds, loads, and validates the project config, returning a `VerbatraConfig`. `configOverride`
(validate an in-memory object) beats `configPath` (load one explicit file), which beats a search
from `cwd` in the [discovery order](https://verbatra.kreitz-webdev.de/docs/config-file.md#discovery-order). A `glossary` given as a
file path is read and validated here, so downstream code always sees an in-memory glossary: a
version 1 map from source term to translation, or a version 2 definition.

```ts title="Signature"
function loadConfig(
  options?: LoadConfigOptions,
): Promise<VerbatraConfig>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `cwd` | `string` | no | Directory the search starts from, and the base a relative `configPath` resolves against. Defaults to the process working directory. A flow given the loaded config without a `cwd` resolves its paths against the project root resolveProjectRoot describes, except for a `configOverride` config, whose flows fall back to the process working directory. |
| `configOverride` | `unknown` | no | A config object to validate directly instead of reading any file. Takes precedence over `configPath` and over searching. Useful for embedding verbatra in a tool that already holds the configuration in memory. |
| `configPath` | `string` | no | An explicit config file to load, bypassing the search. A missing file is an error rather than a fallback to search. |
| `fs` | `SdkFs` | no | File-system port used to read the glossary file. Defaults to the real file system. |
| `fresh` | `boolean` | no | Re-evaluate a JavaScript or TypeScript config file instead of reusing what an earlier load in the same process evaluated. A `.ts` config is re-evaluated together with the modules it imports; a `.js` or `.cjs` config is re-evaluated alone, so a module it imports or requires keeps its first evaluation until the process restarts. Without it, a process that loads the config twice keeps the first result of a `verbatra.config.ts`, `.js`, or `.cjs` file even after the file changed. A long-running process that reloads the config after an edit sets it. JSON, YAML, and `package.json` configs are always read afresh. Defaults to `false`. |

**Returns:** `VerbatraConfig`

**Throws:** [`CONFIG_NOT_FOUND`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_not_found), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid)

**Example:** [A config without a file](https://verbatra.kreitz-webdev.de/docs/programmatic-api.md#a-config-without-a-file)

See [The config file](https://verbatra.kreitz-webdev.de/docs/config-file.md) for the schema. `fs` is the file-system port a
glossary file is read through, as described under [The dependency seam](https://verbatra.kreitz-webdev.de/docs/sdk.md#the-dependency-seam).

### loadConfigWithMeta [#loadconfigwithmeta]

<AvailableFrom version="0.5.0" />

The same load, plus provenance: returns `{ config, source, glossary }`, where `source` says whether
the config came from a search hit, an explicit path, or an in-memory override (with the absolute
`filepath` when there is one), and `glossary` records whether the glossary was absent, inline, or
resolved from a file. Use it when you need to display where the config came from.

```ts title="Signature"
function loadConfigWithMeta(
  options?: LoadConfigOptions,
): Promise<LoadedConfig>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `cwd` | `string` | no | Directory the search starts from, and the base a relative `configPath` resolves against. Defaults to the process working directory. A flow given the loaded config without a `cwd` resolves its paths against the project root resolveProjectRoot describes, except for a `configOverride` config, whose flows fall back to the process working directory. |
| `configOverride` | `unknown` | no | A config object to validate directly instead of reading any file. Takes precedence over `configPath` and over searching. Useful for embedding verbatra in a tool that already holds the configuration in memory. |
| `configPath` | `string` | no | An explicit config file to load, bypassing the search. A missing file is an error rather than a fallback to search. |
| `fs` | `SdkFs` | no | File-system port used to read the glossary file. Defaults to the real file system. |
| `fresh` | `boolean` | no | Re-evaluate a JavaScript or TypeScript config file instead of reusing what an earlier load in the same process evaluated. A `.ts` config is re-evaluated together with the modules it imports; a `.js` or `.cjs` config is re-evaluated alone, so a module it imports or requires keeps its first evaluation until the process restarts. Without it, a process that loads the config twice keeps the first result of a `verbatra.config.ts`, `.js`, or `.cjs` file even after the file changed. A long-running process that reloads the config after an edit sets it. JSON, YAML, and `package.json` configs are always read afresh. Defaults to `false`. |

**Returns:** `LoadedConfig`

**Throws:** [`CONFIG_NOT_FOUND`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_not_found), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid)

Both take `fresh: true` to evaluate a JavaScript or TypeScript config file again instead of reusing
what an earlier load in the same process evaluated; a long-running process that reloads the config
after an edit sets it. JSON, YAML, and `package.json` configs are always read afresh. <AvailableFrom version="0.12.0" />

### configCandidatePaths [#configcandidatepaths]

<AvailableFrom version="0.12.0" />

Lists every file `loadConfigWithMeta` could read a config from with the same `{ cwd?, configPath? }`:
the explicit file alone when `configPath` is set, otherwise each search place in each directory of
the search chain, nearest first, whether or not it exists. Watch them, for example by modification time, to notice a config being created, edited, or
removed without loading it on every request. The glossary file is not included; `loadConfigWithMeta` names it in `glossary`.

```ts title="Signature"
function configCandidatePaths(
  options?: ConfigCandidateOptions,
): readonly string[];
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `cwd` | `string` | no | Directory the config search starts from. Defaults to the process working directory. |
| `configPath` | `string` | no | An explicit config file, resolved against `cwd`. When set, it is the only candidate. |

**Returns:** `readonly string[]`

### resolveProjectRoot [#resolveprojectroot]

<AvailableFrom version="0.12.0" />

Returns the project root the flows resolve relative paths against: the found config file's
directory, so a process started in a subdirectory works on the project's own locale files, lock file
and `.verbatra-local/`, or `cwd` for a config from `configPath`. A flow given the config
`loadConfig` or `loadConfigWithMeta` returned already defaults its `cwd` to that root. Pass `resolveProjectRoot(loaded.source, cwd)` as
`cwd` when you copy or rebuild the config, and to `loadLockFile`, `loadProvenance` and
`runStatus`, which take no config.

```ts title="Signature"
function resolveProjectRoot(
  source: ConfigSource,
  cwd: string,
): string;
```

**Returns:** `string`

## Glossary [#glossary]

### readGlossaryFile [#readglossaryfile]

<AvailableFrom version="0.9.0" />

Reads a file-backed glossary fresh from disk and returns it as a normalized `Glossary`:
`{ version, terms, doNotTranslate }`, where `version` is `1` for a flat term map and `2` for a
[per-locale glossary](https://verbatra.kreitz-webdev.de/docs/config-file.md#glossary), and every term carries `source`, an optional
`target`, `targets`, `forbidden`, `caseSensitive`, and an optional `note` and `partOfSpeech`. Its
`glossary` input is the `GlossaryProvenance` from `loadConfigWithMeta`, so the file it reads is always
the one the config names; there is no path argument. Use it in a long-running tool that shows the
glossary as it is now, not as it was at load.

```ts title="Signature"
function readGlossaryFile(
  input: GlossaryFileInput,
  deps?: GlossaryFileDeps,
): Promise<Glossary>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `glossary` | `GlossaryProvenance` | yes | Where the loaded config's glossary came from, as reported by loadConfigWithMeta. Only a `file` provenance is accepted; it also carries the path, which is why neither function takes one. |

**Returns:** `Glossary`

**Throws:** [`GLOSSARY_NOT_FILE_BACKED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#glossary_not_file_backed), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid)

Throws `GLOSSARY_NOT_FILE_BACKED` when the glossary is inline or absent, and `CONFIG_INVALID` when
the file breaks the [glossary file rules](https://verbatra.kreitz-webdev.de/docs/config-file.md#glossary) or declares another version.

<AvailableFrom version="0.12.0" />

Up to 0.11 it returned the flat term map itself.

### updateGlossaryTerm [#updateglossaryterm]

<AvailableFrom version="0.9.0" />

Adds, changes, or removes exactly one term in a file-backed glossary and returns the normalized
glossary as it now stands. `translation` is
the new text or `null` to remove the term. The rest of the file keeps its key order and its
indentation, the write is atomic, and the whole read-modify-write is held under a project-wide
glossary lock, so two concurrent edits are serialized rather than interleaved.

```ts title="Signature"
function updateGlossaryTerm(
  input: UpdateGlossaryTermInput,
  deps?: GlossaryFileDeps,
): Promise<Glossary>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `cwd` | `string` | no | Directory the write lock is taken under. Defaults to the process working directory. |
| `term` | `string` | yes | The source term to add, change, or remove. Must not be blank. |
| `translation` | `string \| null` | no | The translation to store for the term. Without `locale` it is the translation every target locale uses; with `locale` it is that locale's own translation. `null` removes that translation only, and the term goes with it once it has no translation and no forbidden rendering left. Must not be blank. |
| `locale` | `string` | no | The target locale `translation` and `forbidden` apply to. Omit it for every locale. |
| `forbidden` | `readonly string[] \| null` | no | Renderings `locale` must never use for the term, replacing any listed before. `null` or an empty list removes them. Requires `locale`. |
| `note` | `string \| null` | no | Free-text context for the term, or `null` to remove it. |
| `partOfSpeech` | `string \| null` | no | The term's part of speech, free text, or `null` to remove it. |
| `caseSensitive` | `boolean` | no | Whether the term is matched with case. |
| `doNotTranslate` | `boolean` | no | `true` adds the term to the terms kept untranslated in every locale, `false` removes it from them. Cannot be combined with any field but `caseSensitive`. |
| `lockAcquireTimeoutMs` | `number` | no | How long, in milliseconds, to wait for the project's glossary write lock before failing with `LOCK_CONTENDED`. Defaults to ten minutes. |
| `glossary` | `GlossaryProvenance` | yes | Where the loaded config's glossary came from, as reported by loadConfigWithMeta. Only a `file` provenance is accepted; it also carries the path, which is why neither function takes one. |

**Returns:** `Glossary`

**Throws:** [`GLOSSARY_NOT_FILE_BACKED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#glossary_not_file_backed), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid), [`LOCK_TIMEOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_timeout_invalid), [`LOCK_CONTENDED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_contended), [`RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#run_cancelled), [`GLOSSARY_UNWRITABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#glossary_unwritable)

<AvailableFrom version="0.12.0" />

It also takes `locale` (then `translation` is that locale's own and `null` removes only it),
`forbidden` (the renderings `locale` must never use; `null` or `[]` removes them), `note`,
`partOfSpeech` (each `null` to remove), `caseSensitive`, and `doNotTranslate` (`true` adds the term
to the terms kept untranslated, `false` removes it; it cannot be combined with the term fields).
A version 1 file stays version 1 while an edit only sets or removes a translation for every
locale; any other edit rewrites it as version 2, every existing term keeping its translation as
`target`. `translation: null` without `locale` now removes only the shared translation; the term itself is removed once it has no translation and no forbidden rendering left, which for a version 1 term is at once.

An inline glossary is refused with `GLOSSARY_NOT_FILE_BACKED` rather than converted: it lives inside
an executable config module. A blank field, an edit that sets no field or mixes `doNotTranslate`
with the term fields, forbidden renderings without a locale, an invalid locale, an edit that would
leave an invalid glossary, and an edit whose result would exceed the 1 MiB glossary limit are
refused with `CONFIG_INVALID`; a failed write, including a glossary write lock that cannot be created in a read-only project or cannot be released after a saved edit, is `GLOSSARY_UNWRITABLE`, with the file-system error as its `cause`. An edit that leaves the
glossary as it already is succeeds and writes nothing.

<AvailableFrom version="0.12.0" />

It also takes `lockAcquireTimeoutMs`, which bounds the wait for the glossary lock before the call fails with `LOCK_CONTENDED` (ten minutes by default); an invalid bound fails with `LOCK_TIMEOUT_INVALID` before anything is read.

### glossaryForLocale [#glossaryforlocale]

<AvailableFrom version="0.12.0" />

Resolves the part of a glossary that applies to one target locale:
`glossaryForLocale(config.glossary, "de")` returns `{ terms, doNotTranslate }`, each term with the
locale's `target` (its own entry, then its base language's, then the shared one), its `forbidden`
renderings, `caseSensitive`, and its `note` and `partOfSpeech`, or `undefined` when nothing in the
glossary applies there. This is exactly what a translation request into that locale carries as
[`TranslateRequest.glossary`](https://verbatra.kreitz-webdev.de/docs/sdk/extend.md#custom-providers), and what the review checks and the cache fingerprint of that locale
see.

```ts title="Signature"
function glossaryForLocale(
  glossary: GlossaryInput | Glossary | undefined,
  locale: string,
): LocaleGlossary | undefined;
```

**Returns:** `LocaleGlossary | undefined`

### glossaryHits [#glossaryhits]

<AvailableFrom version="0.12.0" />

`glossaryHits({ glossary, locale, sourceLocale, source })` returns the glossary entries that apply
to one source text in one target locale, as `{ terms, doNotTranslate }` in the shape
`glossaryForLocale` returns: every term whose source occurs in `source` as a whole term, with the
translation and forbidden renderings that locale is held to, and every term to keep untranslated
that occurs in it. A term matches with or without case as it says, with case folded for
`sourceLocale`. It uses the same matching a translate run applies when it flags a missed glossary
term, and reads nothing.

```ts title="Signature"
function glossaryHits(
  input: GlossaryHitsInput,
): LocaleGlossary;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `glossary` | `Glossary \| GlossaryInput \| undefined` | yes | The config's glossary, in either supported shape or already normalized, or `undefined`. |
| `locale` | `string` | yes | The target locale whose translations and forbidden renderings apply. |
| `sourceLocale` | `string` | yes | The locale the text is written in, used to fold case for a term matched without case. |
| `source` | `string` | yes | The source text to look for terms in. |

**Returns:** `LocaleGlossary`

### glossaryDraftCheck [#glossarydraftcheck]

<AvailableFrom version="0.12.0" />

`glossaryDraftCheck({ glossary, locale, sourceLocale, source, draft })` checks a draft translation
against the glossary entries `glossaryHits` finds in the source text, before you write it. Each
term comes back with `targetUsed` (whether the draft carries the required translation, absent for a
term that only forbids renderings) and `forbiddenUsed` (the forbidden renderings the draft uses
and the source does not), and each term to keep untranslated with `kept`. These are the rules a
translate run uses to flag `GLOSSARY_TERM_MISSED` and `GLOSSARY_FORBIDDEN_TERM`. It reads nothing.

```ts title="Signature"
function glossaryDraftCheck(
  input: GlossaryDraftCheckInput,
): GlossaryDraftCheck;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `glossary` | `Glossary \| GlossaryInput \| undefined` | yes | The config's glossary, in either supported shape or already normalized, or `undefined`. |
| `locale` | `string` | yes | The target locale the draft is written in. |
| `sourceLocale` | `string` | yes | The locale the source text is written in. |
| `source` | `string` | yes | The key's source text. |
| `draft` | `string` | yes | The draft translation to check. |

**Returns:** `GlossaryDraftCheck`

### normalizeGlossary [#normalizeglossary]

<AvailableFrom version="0.12.0" />

Converts a glossary in either shape, a version 1 flat map or a version 2 definition, to the
normalized `Glossary` that `readGlossaryFile` returns, with every default filled in. Use it to show
an inline glossary the same way as a file-backed one. It does not validate again; `loadConfig` has.

```ts title="Signature"
function normalizeGlossary(
  glossary: GlossaryInput | Glossary,
): Glossary;
```

**Returns:** `Glossary`

### sharedGlossaryTranslations [#sharedglossarytranslations]

<AvailableFrom version="0.12.0" />

Projects a normalized `Glossary` onto a flat map of each term that has a shared `target` to that
translation, in glossary order. Per-locale translations, forbidden renderings, and do-not-translate
terms are left out, so use it only where a consumer can show nothing but a flat term map. A term
named `__proto__` stays an own key.

```ts title="Signature"
function sharedGlossaryTranslations(
  glossary: Glossary,
): Readonly<Record<string, string>>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | `1 \| 2` | yes | The format the glossary was written in: `1` for a flat term map, `2` for a GlossaryDefinition. |
| `terms` | `readonly GlossaryTerm[]` | yes | Every term, in the order it was written. |
| `doNotTranslate` | `readonly DoNotTranslateTerm[]` | yes | Every term to keep untranslated, in the order it was written. |

**Returns:** `Readonly<Record<string, string>>`

### redactGlossary [#redactglossary]

<AvailableFrom version="0.12.0" />

Passes every source term, translation, forbidden rendering, note, part of speech, and
do-not-translate term of a normalized `Glossary` through `redact`, and returns `{ glossary, redactedTerms }`, where `redactedTerms` names each term
that had a value replaced, a redacted source term itself as `[REDACTED]`, so the list never
repeats a secret. The MCP
server and Studio use it before a glossary leaves the process; use it the same way in your own
tool.

```ts title="Signature"
function redactGlossary(glossary: Glossary): RedactedGlossary;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | `1 \| 2` | yes | The format the glossary was written in: `1` for a flat term map, `2` for a GlossaryDefinition. |
| `terms` | `readonly GlossaryTerm[]` | yes | Every term, in the order it was written. |
| `doNotTranslate` | `readonly DoNotTranslateTerm[]` | yes | Every term to keep untranslated, in the order it was written. |

**Returns:** `RedactedGlossary`

### readCurrentGlossary [#readcurrentglossary]

<AvailableFrom version="0.12.0" />

Reads the glossary a loaded config names as it is now: `readCurrentGlossary({ loaded })`,
where `loaded` is what `loadConfigWithMeta` returns. A file-backed glossary is read fresh from disk,
an inline one comes from the config, and either way the result is the normalized `Glossary`, or
`undefined` when the config declares none. To see what one target locale is held to, pass the result
to `glossaryForLocale`.

```ts title="Signature"
function readCurrentGlossary(
  input: ReadCurrentGlossaryInput,
  deps?: GlossaryFileDeps,
): Promise<Glossary | undefined>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `loaded` | `GlossaryConfig` | yes | The loaded config, as loadConfigWithMeta returns it. |

**Returns:** `Glossary | undefined`

**Throws:** [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid)

### editConfiguredGlossaryTerm [#editconfiguredglossaryterm]

<AvailableFrom version="0.12.0" />

`updateGlossaryTerm` for a tool that holds a loaded config: takes `loaded` in place of `glossary`
with the same edit fields, treats a field left `undefined` as absent, and checks `locale` against the
configured target locales first, failing with `UNKNOWN_LOCALE` and writing nothing when it is not
one of them. The MCP server and Studio write the glossary through it.

```ts title="Signature"
function editConfiguredGlossaryTerm(
  input: EditConfiguredGlossaryTermInput,
  deps?: GlossaryFileDeps,
): Promise<Glossary>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `loaded` | `GlossaryConfig` | yes | The loaded config, as loadConfigWithMeta returns it. |
| `term` | `string` | yes | The source term to add, change, or remove. Must not be blank. |
| `cwd` | `string` | no | Directory the write lock is taken under. Defaults to the process working directory. |
| `translation` | `string \| null` | no | The translation to store for the term. Without `locale` it is the translation every target locale uses; with `locale` it is that locale's own translation. `null` removes that translation only, and the term goes with it once it has no translation and no forbidden rendering left. Must not be blank. |
| `locale` | `string` | no | The target locale `translation` and `forbidden` apply to. Omit it for every locale. |
| `forbidden` | `readonly string[] \| null` | no | Renderings `locale` must never use for the term, replacing any listed before. `null` or an empty list removes them. Requires `locale`. |
| `note` | `string \| null` | no | Free-text context for the term, or `null` to remove it. |
| `partOfSpeech` | `string \| null` | no | The term's part of speech, free text, or `null` to remove it. |
| `caseSensitive` | `boolean` | no | Whether the term is matched with case. |
| `doNotTranslate` | `boolean` | no | `true` adds the term to the terms kept untranslated in every locale, `false` removes it from them. Cannot be combined with any field but `caseSensitive`. |
| `lockAcquireTimeoutMs` | `number` | no | How long, in milliseconds, to wait for the project's glossary write lock before failing with `LOCK_CONTENDED`. Defaults to ten minutes. |

**Returns:** `Glossary`

**Throws:** [`UNKNOWN_LOCALE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_locale), [`GLOSSARY_NOT_FILE_BACKED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#glossary_not_file_backed), [`CONFIG_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#config_invalid), [`LOCK_TIMEOUT_INVALID`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_timeout_invalid), [`LOCK_CONTENDED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#lock_contended), [`RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#run_cancelled), [`GLOSSARY_UNWRITABLE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#glossary_unwritable)

## Authoring, detection, and human-only mode [#authoring-detection-and-human-only-mode]

### defineConfig [#defineconfig]

An identity helper for a typed `verbatra.config.ts`: it returns its argument unchanged, for type
inference and editor autocomplete, including the selected provider's known model IDs (authoring-only;
at runtime any non-empty string passes). Every provider except `opencode`, `openai-compatible`,
`libretranslate`, and `none` also gets an overload taking `AuthoringConfigFor<"<id>">`, such as
`AuthoringConfigFor<"anthropic">`, narrowing the `provider` block to that provider's options; those
four use the `AuthoringConfig` fallback below.

```ts title="Signature"
function defineConfig(config: AuthoringConfigFor<"anthropic">): VerbatraConfigInput;
function defineConfig(config: AuthoringConfigFor<"openai">): VerbatraConfigInput;
function defineConfig(config: AuthoringConfigFor<"gemini">): VerbatraConfigInput;
function defineConfig(config: AuthoringConfigFor<"deepl">): VerbatraConfigInput;
function defineConfig(config: AuthoringConfigFor<"google-translate">): VerbatraConfigInput;
function defineConfig(config: AuthoringConfig): VerbatraConfigInput;
```

**Returns:** `VerbatraConfigInput`

**Example:** [Add a source file and a config](https://verbatra.kreitz-webdev.de/docs/sdk-quickstart.md#add-a-source-file-and-a-config)

### verbatraConfigSchema [#verbatraconfigschema]

The zod schema `loadConfig` validates with, exported so your own tooling can validate a config
object the same way the SDK does. Unknown top-level keys are rejected, so a stray secret cannot
hide in the config, except `$schema`: an optional string, ignored at runtime, that lets a JSON or
YAML config point an editor at the package's `@verbatra/sdk/config-schema.json`.

```ts title="Signature"
const verbatraConfigSchema: z.ZodObject<{ ... }, z.core.$strict>;
```

Its `rates` block is a `RateCard`, the price list that turns an [estimate](https://verbatra.kreitz-webdev.de/docs/estimating-cost.md) into money:

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `asOf` | `string` | yes | The date the rates were read, written as `YYYY-MM-DD`; printed beside every priced estimate. |
| `currency` | `string` | yes | The three-letter uppercase currency code the rates are written in, such as `USD` or `EUR`. |
| `table` | `Record<string, union>` | yes | The rate entries by rate key: a token rate or a character rate, never both. |

### scaffoldingMetadata [#scaffoldingmetadata]

<AvailableFrom version="0.4.0" />

Read-only metadata the CLI `init` command derives its prompts from: `providerEnv` (provider id to
the environment variable its key is read from), `scaffoldModels` (a default scaffold model per LLM
provider), `providerTokenLimitKeys` (the option key each LLM provider takes its output token limit
under), and `supportedFormats` (the closed set of format ids). Read the token-limit key from
`providerTokenLimitKeys` rather than assuming one, since the keys differ ([providers](https://verbatra.kreitz-webdev.de/docs/providers.md));
DeepL and Google Cloud Translation have no entry because they take no token limit. The `ScaffoldableProviderId` type is
`Exclude<ProviderId, "openai-compatible" | "libretranslate" | "none">`: every provider that needs
nothing but its id. `openai-compatible` and `libretranslate` each need a `baseUrl` only you can
supply and have no required environment variable, and `none` calls no provider at all.

```ts title="Signature"
const scaffoldingMetadata: {
  readonly providerEnv: { ... };
  readonly scaffoldModels: { ... };
  readonly providerTokenLimitKeys: { ... };
  readonly supportedFormats: readonly [...];
  readonly humanOnlyProviderId: "none";
  readonly openAiCompatibleKeyEnv: "OPENAI_COMPATIBLE_API_KEY";
  readonly libreTranslateKeyEnv: "LIBRETRANSLATE_API_KEY";
  readonly configSearchPlaces: readonly string[];
};
```

`scaffoldingMetadata.humanOnlyProviderId` is `"none"`, the id that disables machine translation. It has no `providerEnv` entry, since it reads no key.

<AvailableFrom version="0.12.0" />

`scaffoldingMetadata.libreTranslateKeyEnv` is `"LIBRETRANSLATE_API_KEY"`, the variable a `libretranslate` provider reads its key from. It is optional: a server started without `--api-keys` needs none, so a generator should leave it unset.

`scaffoldingMetadata.openAiCompatibleKeyEnv` is `"OPENAI_COMPATIBLE_API_KEY"`, the variable an `openai-compatible` provider reads when its options name no `apiKeyEnvVar`. `scaffoldingMetadata.configSearchPlaces` lists every file name `loadConfig` searches for a config, in the order it checks them; a generator writing `verbatra.config.ts` should refuse when another of them already exists, since the first one found wins.

### detectProject [#detectproject]

<AvailableFrom version="0.12.0" />

Detects a project's locale file format and layout from its files, the way `verbatra init` does before writing a config. It writes nothing and calls no provider. It scans the directory (skipping hidden directories, `node_modules`, and build output, within a bounded depth and entry count) for files a registered adapter claims, finds the path segment that spells a locale in each, and keeps the file pattern covering the most locales. The format is the one whose adapter `canHandle` claims every matched file given its content; for JSON, where every JSON adapter does, a single matching i18n library in `package.json` decides.

```ts title="Signature"
function detectProject(
  input?: DetectProjectInput,
  deps?: DetectProjectDeps,
): Promise<ProjectDetection>;
```

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `cwd` | `string` | no | The directory to scan. Defaults to the process working directory. |
| `format` | `FormatId` | no | A format already chosen. Only files this format's adapter claims are considered. |
| `pattern` | `string` | no | A file pattern already chosen, containing the `{locale}` token. Only files matching it are considered, and no other layout is searched for. |
| `sourceLocale` | `string` | no | A source locale already chosen. It is reported as the layout's source locale unchanged. |

**Returns:** `ProjectDetection`

```ts title="Example"
import { detectProject } from "@verbatra/sdk";

const detection = await detectProject({ cwd: "/path/to/project" });
if (detection.ambiguities.length > 0) {
  // several formats or file patterns fit: choose one of detection.ambiguities[0].candidates
}
const format = detection.format?.id;          // for example "yaml"
const pattern = detection.layout?.pattern;    // for example "config/locales/{locale}.yml"
```

The result carries `format` (with `from`: `input`, `files`, or `dependencies`), `layout` (`pattern`, `localeStyle`, `locales`, `sourceLocale`, `files`, `unqualifiedSourceFile`), `ambiguities` (each with a `subject` of `format` or `layout` and its `candidates`), a `confidence` of `high`, `medium`, `low`, or `none`, and `reasons`, one sentence per decision. Ties are reported as ambiguities, never guessed. The optional `format`, `pattern`, and `sourceLocale` inputs narrow the search, and `deps.fs` and `deps.adapterRegistry` swap the file system and the adapters, so a third-party adapter's files are detected too. A missing, unreadable, or malformed file never throws; it makes the report thinner. `layout.unqualifiedSourceFile` names a file beside the layout holding strings under no locale name, such as `messages.properties` or a gettext `.pot` template; the source locale is then left open, since the source is read from the file the pattern names for it.

### isMachineTranslationEnabled [#ismachinetranslationenabled]

<AvailableFrom version="0.12.0" />

Returns `false` exactly when the config sets `provider: { id: "none" }`, the [human-only](https://verbatra.kreitz-webdev.de/docs/human-only-workflow.md) mode in which no provider is ever constructed and no API key is read. A surface that offers provider-spending actions reads it to leave them out altogether, which is how Studio and the MCP server withhold spend even with `--allow-spend`.

```ts title="Signature"
function isMachineTranslationEnabled(
  config: Pick<VerbatraConfig, "provider">,
): boolean;
```

**Returns:** `boolean`

### assertMachineTranslationEnabled [#assertmachinetranslationenabled]

<AvailableFrom version="0.12.0" />

Takes the config and a short phrase naming the action, and throws `SdkError` `MACHINE_TRANSLATION_DISABLED` under `provider: { id: "none" }`. Call it before wrapping `translate` in an explicit "translate everything pending" action, so the action fails the same way `retranslateEntry` does instead of quietly filling from the translation memory alone.

```ts title="Signature"
function assertMachineTranslationEnabled(
  config: Pick<VerbatraConfig, "provider">,
  action: string,
): void;
```

**Returns:** `void`

**Throws:** [`MACHINE_TRANSLATION_DISABLED`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#machine_translation_disabled)

### assertTargetLocale [#asserttargetlocale]

<AvailableFrom version="0.12.0" />

Takes the config and one locale, and throws `SdkError` `UNKNOWN_LOCALE` when the locale is not one of the configured `targetLocales`, with the same message every SDK function taking a `locales` filter uses. Call it in a surface that takes a single locale of its own, as the MCP server's `glossary.get` does, so a wrong locale fails exactly as it does in the SDK.

```ts title="Signature"
function assertTargetLocale(
  config: Pick<VerbatraConfig, "targetLocales">,
  locale: string,
): void;
```

**Returns:** `void`

**Throws:** [`UNKNOWN_LOCALE`](https://verbatra.kreitz-webdev.de/docs/error-codes.md#unknown_locale)
