Docs / Anleitungen
Maschinell übersetzte Seite
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 behandelt den ersten Lauf. Diese Seite setzt dort an
mit vollständigen Rezepten zum Kopieren in ein Skript; die SDK-Referenz 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
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-Block sie bepreist hat.
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 erklärt die
Methode, und RunSummary listet, was jeder EstimateCaveatCode in caveats
auslässt.
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.
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.
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
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.
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.
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.
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
Ü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.
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_, eine, die er mitten im Lauf gestoppt hat, die Notice RUN_. 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
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.
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_. extract listet jeden weiteren Fehler, den
es auslöst.
Einen typisierten Katalog erzeugen
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.
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
listet alles, was ein schreibender Lauf ablehnt.
Eine Pseudo-Locale bauen
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.
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
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.
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.
Werte von Menschen
retranslateEntry lehnt einen Wert, den ein Mensch geschrieben hat, mit KEY_ 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.
Die Review-Warteschlange abarbeiten
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_, 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.
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 zeigt die Batch-Varianten und jeden Code, den sie werfen.
Alle Werte auf einmal lesen
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.
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_.
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.
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.
Der TMX-Roundtrip
importTmx und exportTmx bewegen den Übersetzungsspeicher 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.
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_ 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
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.
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 });