---
title: "SDK-Rezepte"
description: "Vollständige, lauffähige Muster, um verbatra aus deinen eigenen Skripten und CI-Jobs anzusteuern, jedes auf den öffentlichen Einstiegspunkten von @verbatra/sdk."
type: "how-to"
---
# SDK-Rezepte

Vollständige, lauffähige Muster, um verbatra aus deinen eigenen Skripten und CI-Jobs anzusteuern, jedes auf den öffentlichen Einstiegspunkten von @verbatra/sdk.

Der [SDK-Schnellstart](https://verbatra.kreitz-webdev.de/de/docs/sdk-quickstart.md) behandelt den ersten Lauf. Diese Seite setzt dort an
mit vollständigen Rezepten zum Kopieren in ein Skript; die [SDK-Referenz](https://verbatra.kreitz-webdev.de/de/docs/sdk.md) katalogisiert
jeden Einstiegspunkt. Das SDK lädt keine `.env`-Dateien, also führst du ein Rezept, das einen
Provider aufruft, mit `node --env-file=.env` aus. Um einen ratenbegrenzten Provider Sprache für
Sprache abzuarbeiten, übergib `locales: ["de"]` an `translate`.

## Einen Lauf schätzen, bevor du ausgibst [#einen-lauf-schätzen-bevor-du-ausgibst]

<AvailableFrom version="0.11.0" />

`estimate: true` impliziert `dryRun`. Die Zahl landet auf `RunSummary.estimate`: Verzweige über
`unit` für die abgerechnete Dimension und über `pricing` dafür, ob ein
[`rates`](https://verbatra.kreitz-webdev.de/de/docs/config-file.md#rates)-Block sie bepreist hat.

```ts
import { loadConfig, translate } from "@verbatra/sdk";

const summary = await translate({ config: await loadConfig(), estimate: true });
const estimate = summary.estimate;

if (estimate !== undefined) {
  console.log(`${estimate.keys} keys in ${estimate.requests} requests to ${estimate.rateKey}`);

  if (estimate.unit === "tokens") {
    console.log(`about ${estimate.inputTokens} in, ${estimate.outputTokens} out`);
  } else {
    console.log(`about ${estimate.sourceCharacters} source characters`);
  }

  if (estimate.pricing === "priced") {
    console.log(`about ${estimate.cost} ${estimate.currency}, rates read ${estimate.asOf}`);
    for (const locale of estimate.locales) {
      console.log(`  ${locale.locale}: ${locale.cost}`);
    }
  } else {
    console.log(`no currency figure: ${estimate.pricing}`);
  }

  console.log(`leaves out: ${estimate.caveats.join(", ")}`);
}
```

Es begrenzt den Plan, nicht die Rechnung; [Kosten abschätzen](https://verbatra.kreitz-webdev.de/de/docs/estimating-cost.md) erklärt die
Methode, und [RunSummary](https://verbatra.kreitz-webdev.de/de/docs/sdk/run-summary.md) listet, was jeder `EstimateCaveatCode` in `caveats`
auslässt.

## In CI prüfen, ohne zu schreiben [#in-ci-prüfen-ohne-zu-schreiben]

`check` liest und vergleicht, ohne einen Provider aufzurufen oder irgendeine Datei anzufassen, und
sein `inSync`-Flag ist genau dann true, wenn nichts fehlt und nichts veraltet ist.

```ts
import { check, loadConfig } from "@verbatra/sdk";

const summary = await check({ config: await loadConfig() });

if (!summary.inSync) {
  for (const locale of summary.locales) {
    if (!locale.inSync) {
      console.error(`${locale.locale}: ${locale.missing} missing, ${locale.stale} stale`);
    }
  }
  process.exitCode = 1;
}
```

Du brauchst die Key-Namen statt der Anzahlen? Tausche `diff` ein, das die Key-Listen `missing`,
`changed` und `orphaned` pro Locale zurückgibt, mit demselben `hasPendingChanges`-Gate. Du führst
stattdessen die CLI in CI aus? Siehe [CI und Exit-Codes](https://verbatra.kreitz-webdev.de/de/docs/ci-and-exit-codes.md).

```ts
import { diff, loadConfig } from "@verbatra/sdk";

const summary = await diff({ config: await loadConfig() });

if (summary.hasPendingChanges) {
  for (const locale of summary.locales) {
    console.error(`${locale.locale}: missing ${locale.missing.join(", ")}; changed ${locale.changed.join(", ")}`);
  }
  process.exitCode = 1;
}
```

## Deine Integration ohne Schlüssel testen [#deine-integration-ohne-schlüssel-testen]

Ein als `createProvider` übergebener Fake-Provider durchläuft den ganzen Flow, Dateien und
Integritätsprüfung eingeschlossen, ohne Schlüssel, ohne Netzwerkaufruf und mit deterministischer
Ausgabe: genau das, was ein Unit-Test deiner eigenen Tools braucht.

```ts title="fake-provider.ts"
import { type CreateProvider, check, loadConfig, translate } from "@verbatra/sdk";

const createProvider: CreateProvider = () => ({
  id: "fake",
  kind: "machine-translation",
  supportsGlossary: false,
  translateBatch: async (request) => ({
    values: new Map(
      request.entries.map((entry) => [entry.key, `[${request.targetLocale}] ${entry.value}`]),
    ),
    integrity: new Map(),
    notices: [],
  }),
});

const config = await loadConfig();
const summary = await translate({ config }, { createProvider });
const after = await check({ config });

console.log(`${summary.succeeded.length} locales translated, in sync: ${after.inSync}`);
if (summary.failed.length > 0 || !after.inSync) {
  process.exitCode = 1;
}
```

Der Lauf schreibt echte Dateien, also teste gegen eine temporäre Kopie des Projekts. Ein echter
Provider wird genauso eingebunden; siehe [Eigene Provider](https://verbatra.kreitz-webdev.de/de/docs/sdk/extend.md#eigene-provider).

## Beobachten in einem langlaufenden Prozess [#beobachten-in-einem-langlaufenden-prozess]

`watch` feuert beim Start sofort einen Lauf, danach einen pro entprellter Quelländerung, und
meldet jeden über `onRun`. Für ein sauberes Herunterfahren warte auf `controller.stop()`: es
schließt den Watcher und wartet auf den laufenden Lauf, bevor dein Prozess endet.

```ts
import { loadConfig, watch } from "@verbatra/sdk";

const config = await loadConfig();
const controller = await watch({
  config,
  onRun: (result) => {
    if (result.status === "succeeded") {
      console.log(`ran: ${result.summary.succeeded.length} ok, ${result.summary.failed.length} failed`);
    } else {
      console.error(`run failed: ${result.error.code} ${result.error.message}`);
    }
  },
});

process.on("SIGINT", () => {
  void controller.stop().then(() => {
    process.exit(0);
  });
});
```

Ein Lauf-Fehlschlag nach dem Start wirft nie; er kommt als `{ status: "failed" }` an, und das
Beobachten geht weiter.

## Einen Lauf abbrechen [#einen-lauf-abbrechen]

<AvailableFrom version="0.12.0" />

Übergib ein `AbortSignal` als `signal`. `translate` löst dann auf, statt fehlzuschlagen: Es
schreibt, was schon angekommen ist, lässt den Rest offen und setzt `cancelled: true`.

```ts
import { loadConfig, translate } from "@verbatra/sdk";

const controller = new AbortController();
process.once("SIGINT", () => controller.abort());

const summary = await translate({ config: await loadConfig(), signal: controller.signal });

if (summary.cancelled === true) {
  const { succeeded, partial, failed } = summary;
  console.error(
    `cancelled: ${succeeded.length} done, ${partial.length} partial,`,
    `${failed.length} failed or not started`,
  );
  process.exitCode = 130;
}
```

Eine Locale, deren Start der Abbruch verhindert hat, trägt den [Fehlercode `RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/de/docs/error-codes.md#run_cancelled), eine, die er mitten im Lauf gestoppt hat, die [Notice `RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/de/docs/error-codes.md#notice-run_cancelled). Ein Abbruch, der erst ankommt, wenn jede Locale fertig ist, lässt `cancelled` weg, und ein Fehler des ganzen Laufs beim Start schlägt weiterhin fehl.

## Keys aus deinem Quellcode extrahieren [#keys-aus-deinem-quellcode-extrahieren]

<AvailableFrom version="0.11.0" />

`extract` ist der eine Einstiegspunkt, der Anwendungscode statt einer Locale-Datei liest. Er
schreibt nur den Quellkatalog, und nur wirklich neue Keys: Ein vorhandener Key behält seinen Wert,
und ein Key, den keine Aufrufstelle nennt, bleibt unangetastet. Alles, was der Scan nicht auflösen
kann, kommt als Daten zurück, ein Dry Run ist also ein gutes Gate vor dem schreibenden Lauf.

```ts
import { extract, loadConfig } from "@verbatra/sdk";

const config = await loadConfig();
const preview = await extract({ config, dryRun: true });

console.log(`${preview.scannedFiles} files scanned, ${preview.existingKeys} keys already present`);
for (const added of preview.added) {
  console.log(`+ ${added.key} = ${JSON.stringify(added.value)} (${added.file}:${added.line})`);
}
for (const conflict of preview.conflicts) {
  console.warn(`${conflict.key} has two defaults: ${conflict.values.join(" | ")}`);
}
for (const site of preview.dynamic) {
  console.warn(`non-static key argument at ${site.file}:${site.line}`);
}
for (const diagnostic of preview.diagnostics) {
  console.warn(`${diagnostic.file}: ${diagnostic.reason}`);
}

if (preview.added.length > 0 && preview.conflicts.length === 0) {
  const run = await extract({ config });
  console.log(`wrote ${run.added.length} keys to ${run.sourcePath}: ${run.written}`);
}
```

`config.extract` muss das Framework und die Quellverzeichnisse nennen, sonst wirft der Aufruf
`EXTRACT_NOT_CONFIGURED`. [`extract`](https://verbatra.kreitz-webdev.de/de/docs/sdk/inspect.md#extract) listet jeden weiteren Fehler, den
es auslöst.

## Einen typisierten Katalog erzeugen [#einen-typisierten-katalog-erzeugen]

<AvailableFrom version="0.11.0" />

`generateTypes` deklariert den Quellkatalog als TypeScript: eine Union aus jedem Key plus den
Argumenten, die jede Nachricht interpoliert. Es liest eine Datei und schreibt eine Datei, und zwei
Läufe über einen unveränderten Katalog schreiben Byte für Byte dasselbe: genau das macht den
`check`-Modus zu einem brauchbaren CI-Gate.

```ts
import { generateTypes, loadConfigWithMeta } from "@verbatra/sdk";

const loaded = await loadConfigWithMeta();
const out = "src/generated/messages.d.ts";
const configPath = loaded.source.kind === "override" ? undefined : loaded.source.filepath;

const result = await generateTypes({
  config: loaded.config,
  out,
  ...(configPath !== undefined ? { configPath } : {}),
});

console.log(`${result.keys} keys in ${result.path}, ${result.withArguments} take arguments`);
for (const message of result.unresolved) {
  console.warn(`arguments not claimed for ${message.key}: ${message.reason}`);
}

// In CI, compare instead of writing.
const gate = await generateTypes({ config: loaded.config, out, check: true });
if (gate.stale) {
  console.error(`${gate.path} is out of date, re-generate it`);
  process.exitCode = 1;
}
```

Übergib `configPath`, damit die vom Lauf geladene Konfigurationsdatei als Ausgabepfad abgelehnt
wird. Ein `check`-Lauf schreibt nichts und meldet nur `stale`. [`verbatra types`](https://verbatra.kreitz-webdev.de/de/docs/cli/types.md)
listet alles, was ein schreibender Lauf ablehnt.

## Eine Pseudo-Locale bauen [#eine-pseudo-locale-bauen]

<AvailableFrom version="0.11.0" />

`pseudolocalize` baut allein aus den Quellstrings eine künstliche Locale, sodass abgeschnittene
Layouts und nie extrahierte Strings auftauchen, bevor es überhaupt einen API-Key gibt. Es
ruft keinen Provider auf, und seine Ausgabe landet außerhalb von `files.pattern`, sodass kein Lauf
Geld dafür ausgibt oder sie als abgedriftet meldet.

```ts
import { loadConfig, pseudolocalize } from "@verbatra/sdk";

const result = await pseudolocalize({ config: await loadConfig() });

console.log(`${result.locale} written to ${result.path}: ${result.written}`);
console.log(`${result.transformed} of ${result.entries} entries transformed`);
for (const key of result.copied) {
  console.warn(`copied from source verbatim, a pseudolocalized value would not pass the gate: ${key}`);
}
```

`locale` ist standardmäßig `en-XA`, mit `mode: "bidi"` `ar-XB`, und `out` `.verbatra-local/pseudo`. Die Transformation ist
deterministisch, ein zweiter Lauf über unveränderte Quelle schreibt also nichts neu und meldet
`written` als false.

## Einen einzelnen Key bearbeiten und neu übersetzen [#einen-einzelnen-key-bearbeiten-und-neu-übersetzen]

Das sind die Nahtstellen, an denen Verbatra Studio ansetzt; nutze sie, um deinen eigenen
Review-Flow zu bauen. Lies die aktuellen Werte mit `keyValue`, speichere eine menschliche Korrektur
mit `editEntry` oder lass den Provider für einen Key mit `retranslateEntry` neu laufen. Beide
schreibenden Einstiegspunkte schicken den Kandidatenwert durch dieselben Integritätsprüfungen wie
ein voller Lauf und geben ein zweiarmiges Ergebnis zurück, statt bei einem abgelehnten Wert zu
werfen.

```ts
import { editEntry, keyValue, loadConfig, retranslateEntry } from "@verbatra/sdk";

const config = await loadConfig();

// Read the live values feeding your edit UI.
const current = await keyValue({ config, locale: "de", key: "checkout.title" });
console.log(`source: ${current.source}, target: ${current.target ?? "(not yet translated)"}`);

// Save a human-typed correction. No provider call.
const edit = await editEntry({
  config,
  locale: "de",
  key: "checkout.title",
  value: "Zur Kasse",
});
if (!edit.accepted) {
  console.error(`rejected (${edit.reason} check failed), nothing written`);
}

// Or ask the provider for a fresh translation of just this key. The edit above
// recorded a value a person wrote, which retranslateEntry protects unless told otherwise.
const retry = await retranslateEntry({
  config,
  locale: "de",
  key: "checkout.title",
  includeHuman: true,
});
if (retry.accepted) {
  console.log(`wrote: ${retry.value}`);
  if (retry.reviewReasons.length > 0) {
    console.warn(`flagged for review: ${retry.reviewReasons.join(", ")}`);
  }
}
```

Was jede davon wirft, listet [`retranslateEntry`](https://verbatra.kreitz-webdev.de/de/docs/sdk/review.md#retranslateentry).

### Werte von Menschen [#werte-von-menschen]

<AvailableFrom version="0.12.0" />

`retranslateEntry` lehnt einen Wert, den ein Mensch geschrieben hat, mit `KEY_PROTECTED` ab, auch
den, den `editEntry` gerade gespeichert hat, außer du übergibst `includeHuman: true` oder die Config
setzt `humanEdits: "overwrite"`. Deshalb übergibt das Rezept es. Einen gepinnten Key lehnt es immer
ab; siehe
[Menschliche Übersetzungen schützen](https://verbatra.kreitz-webdev.de/de/docs/protecting-human-translations.md).

## Die Review-Warteschlange abarbeiten [#die-review-warteschlange-abarbeiten]

<AvailableFrom version="0.12.0" />

`reviewQueue` listet jeden maschinell geschriebenen Wert, den noch niemand freigegeben hat, mit den
Gründen, aus denen der letzte Lauf ihn markiert hat. Dieses Skript gibt die unmarkierten Werte frei
und lehnt den Rest ab; abgelehnte Werte werden entfernt, sodass das nächste `translate` sie neu
füllt. Mit `expectedValue` wirft ein Aufruf `REVIEW_VALUE_CHANGED`, wenn sich der Wert
zwischendurch geändert hat. Ohne Aufzeichnung des letzten Laufs auf dieser Maschine, etwa in einem
frischen Klon oder auf einem CI-Runner, ist `reasons` ebenfalls leer, deshalb bricht das Skript ab,
wenn `lastRunAt` fehlt.

```ts title="review.ts"
import { approveEntry, keyValue, loadConfig, rejectEntry, reviewQueue } from "@verbatra/sdk";

const config = await loadConfig();
const queue = await reviewQueue({ config });

if (!queue.available) {
  console.error(`no review queue: ${queue.reason}`);
  process.exit(1);
}
if (queue.lastRunAt === undefined) {
  console.error("no run recorded on this machine, so flags are unknown: run translate first");
  process.exit(1);
}

for (const { locale, needsReview } of queue.locales) {
  for (const entry of needsReview) {
    const { target } = await keyValue({ config, locale, key: entry.key });
    if (target === undefined) {
      continue;
    }
    const decision = { config, locale, key: entry.key, expectedValue: target };
    if (entry.reasons.length > 0) {
      await rejectEntry(decision);
      console.log(`rejected ${locale}/${entry.key}: ${entry.reasons.join(", ")}`);
    } else {
      await approveEntry(decision);
      console.log(`approved ${locale}/${entry.key}`);
    }
  }
}
```

[Prüfen und neu übersetzen](https://verbatra.kreitz-webdev.de/de/docs/sdk/review.md#approveentry) zeigt die Batch-Varianten und jeden Code, den sie werfen.

## Alle Werte auf einmal lesen [#alle-werte-auf-einmal-lesen]

<AvailableFrom version="0.10.0" />

`localeValues` ist das Gegenstück zu `keyValue` für große Mengen: ein Durchgang über die Dateien,
die schon auf der Platte liegen, der pro Locale den aktuellen Quell- und Zieltext jedes Keys
zurückgibt.

```ts title="scan-values.ts"
import { loadConfig, localeValues } from "@verbatra/sdk";

const report = await localeValues({ config: await loadConfig() });

for (const { locale, values } of report) {
  for (const [key, pair] of Object.entries(values)) {
    if (pair.source === undefined) {
      console.warn(`${locale}/${key}: orphaned, no source key any more`);
    } else if (pair.target === undefined) {
      console.warn(`${locale}/${key}: not translated yet`);
    } else if (pair.target.includes("TODO")) {
      console.log(`${locale}/${key}: ${pair.target}`);
    }
  }
}
```

Nur lesend: Es ruft keinen Provider auf, schreibt nichts und berührt weder die Lock-Datei noch den
Cache. Es deckt jede konfigurierte Ziel-Locale ab; mit `locales` grenzt du es auf einige davon ein, und eine Locale außerhalb von `targetLocales` wirft `UNKNOWN_LOCALE`.

## Der Arbeitsmappen-Roundtrip [#der-arbeitsmappen-roundtrip]

Exportiere die zu übersetzenden Strings in eine Excel-Arbeitsmappe, gib sie an eine Übersetzerin,
dann importiere die ausgefüllte Datei zurück. `importWorkbook` führt dieselben Drift- und
Integritätsprüfungen aus wie `translate` und gibt dieselbe `RunSummary`-Form zurück.

```ts
import { exportWorkbook, importWorkbook, loadConfig } from "@verbatra/sdk";

const config = await loadConfig();

// Write a workbook of the missing and changed strings.
const exported = await exportWorkbook({ config });
for (const sheet of exported.locales) {
  console.log(`${sheet.locale}: ${sheet.rows} rows`);
}
console.log(`wrote ${exported.path}`);

// ...later, after the translator returns the file, import it back.
const summary = await importWorkbook({ config, workbook: exported.path });
console.log(`${summary.succeeded.length} locales ok, ${summary.failed.length} failed`);
```

Übergib `dryRun: true` an `importWorkbook`, um eine zurückgekommene Datei zu validieren, ohne
etwas zu schreiben. Was Übersetzerinnen bearbeiten dürfen und wie Zeilen validiert werden, steht
in [Menschliche Übersetzung](https://verbatra.kreitz-webdev.de/de/docs/manual-translation.md).

## Der TMX-Roundtrip [#der-tmx-roundtrip]

<AvailableFrom version="0.11.0" />

`importTmx` und `exportTmx` bewegen den [Übersetzungsspeicher](https://verbatra.kreitz-webdev.de/de/docs/the-cache.md) in TMX hinein und
wieder heraus, in das Austauschformat, das die gängigen Übersetzungsplattformen erzeugen und lesen.
Anders als das Arbeitsmappen-Paar bewegt dieses Paar den ganzen Speicher als eine mehrsprachige
Datei zwischen Werkzeugen.

```ts
import { exportTmx, importTmx, loadConfig, tmxErrorLocation } from "@verbatra/sdk";

const config = await loadConfig();

// Validate another tool's file before it touches the memory.
try {
  const preview = await importTmx({ config, file: "legacy.tmx", dryRun: true });
  console.log(`${preview.units} units, source language ${preview.sourceLanguage ?? "undeclared"}`);
  for (const locale of preview.locales) {
    console.log(
      `${locale.locale}: +${locale.added}, kept ${locale.kept}, conflicting ${locale.conflicting}`,
    );
  }

  // Then land it. Pass overwrite: true to let the file win where the two disagree.
  const landed = await importTmx({ config, file: "legacy.tmx" });
  if (!landed.memoryWritable) {
    console.error("the memory file was written by a newer verbatra and was left untouched");
  }
} catch (error) {
  const at = tmxErrorLocation(error);
  if (at === undefined) {
    throw error;
  }
  console.error(`legacy.tmx is malformed at line ${at.line}, column ${at.column}`);
}

// ...and hand this project's memory to another tool.
const exported = await exportTmx({ config, out: "out/memory.tmx" });
console.log(`${exported.units} units to ${exported.path}`);
```

`importTmx` meldet beschädigte Daten, statt bei ihnen zu werfen: Eine abgelehnte
Datei löst `SOURCE_INVALID` aus, und `tmxErrorLocation` liest die Stelle aus diesem Fehler, ohne
ihn oder sein `cause` zu casten. Exportiert werden nur Einträge unter dem aktuellen
Konfigurations-Fingerprint, also genau die Menge, die ein Lauf wiederverwenden würde.

## Eine Konfiguration ohne Datei [#eine-konfiguration-ohne-datei]

`loadConfig` akzeptiert ein `configOverride` im Speicher, das genau wie eine geladene Datei
validiert wird; du kannst verbatra also komplett aus Code heraus steuern, ganz ohne
Konfigurationsdatei auf der Platte.

```ts
import { loadConfig, translate } from "@verbatra/sdk";

const config = await loadConfig({
  configOverride: {
    sourceLocale: "en",
    targetLocales: ["de", "fr"],
    format: "i18next-json",
    files: { pattern: "locales/{locale}.json" },
    provider: { id: "gemini", options: { model: "gemini-2.5-flash", maxOutputTokens: 4096 } },
  },
});

await translate({ config });
```
