Zum Inhalt springen
Zum Inhalt springen

Referenz / SDK

Maschinell übersetzte Seite

Diese Seite wurde automatisch übersetzt und kann daher Fehler oder seltsame Formulierungen enthalten. Die englische Version ist die maßgebliche Quelle. Das englische Original lesen.

SDK-Referenz

Installiere @verbatra/sdk und finde die Referenzseite zu jedem Einstiegspunkt. Dateien, die nicht auf der Platte liegen, kommen über dein Dateisystem ins SDK.

@verbatra/sdk ist der Motor, auf dem der Befehl verbatra läuft. Die CLI ist ein dünner Wrapper; was die Kommandozeile kann, kannst du also auch im Code.

Hier anfangen

Neu im SDK? Fang mit einer dieser Seiten an:

Referenzseiten

Diese Seiten katalogisieren die gesamte öffentliche Oberfläche, gruppiert nach der Aufgabe jedes Einstiegspunkts.

Übersetzungen ausführen

translate, Fortschrittsereignisse, resolveDryRun, watch, releaseHeldLocks, pseudolocalize, generateTypes

Ein Projekt inspizieren

doctor, dataFlow, dataFlowManifestSchema, extract, check, checkFile, diff, keyIntegrity, localeIntegrity, lockState, loadLockFile, loadProvenance, runStatus, reviewQueue, provenanceReport, provenanceReportPage, previewIcuMessage

Bearbeiten

editEntry, previewValueReplace, applyValueReplace

Prüfen und neu übersetzen

keyValue, keyContext, localeHistory, localeGitStatus, localeGitDiff, retranslateEntry, approveEntry, rejectEntry, createValueMarker, localeValues, localeValuesPage, approveEntries, rejectEntries, approveLocale, retranslateEntries, BatchInterruptedError

Key-Verlauf

keyHistory, runHistory

Übergabe und Austausch

readLocaleFileSnapshot, diffLocaleSnapshots, createLocalePathResolver, localeWatchPaths, exportWorkbook, importWorkbook, importTmx, tmxErrorLocation, exportTmx

Konfiguration und Glossar

loadConfig, loadConfigWithMeta, configCandidatePaths, resolveProjectRoot, readGlossaryFile, updateGlossaryTerm, glossaryForLocale, glossaryHits, glossaryDraftCheck, normalizeGlossary, sharedGlossaryTranslations, redactGlossary, readCurrentGlossary, editConfiguredGlossaryTerm, setKeyLengthLimit, defineConfig, verbatraConfigSchema, scaffoldingMetadata, detectProject, isMachineTranslationEnabled, assertMachineTranslationEnabled, assertTargetLocale

Adapter, Provider, Schwärzung

createDefaultRegistry, AdapterRegistry, createFlatFileAdapter, createTreeFileAdapter, nodeAdapterFs, isCustomFormatId, redact, declareProviderKeyEnvVar

RunSummary

budgetStanding

JSON Schemas

runSummarySchema, checkSummarySchema, checkFileSummarySchema, diffSummarySchema, doctorResultSchema, provenanceReportResultSchema, pseudolocalizeResultSchema, generateTypesResultSchema, extractResultSchema, exportWorkbookResultSchema, importTmxResultSchema, exportTmxResultSchema, runHistoryResultSchema, localeSummarySchema, keyProvenanceSchema, provenanceSummarySchema, keyOriginSchema, progressEventSchema, lockWaitEventSchema, renderJsonSchemas, renderOutputJsonSchema, jsonSchemaUrl

Fehler

SdkError, ProviderError, AdapterError, projectRelativeMessage, errorHint

Installation

npm install --save-dev @verbatra/sdk

Benötigt Node.js ^22.22.0 || >=24.

Mit pnpm installiert pnpm add --save-dev @verbatra/sdk korrekt, beendet sich aber mit 1 und ERR_PNPM_IGNORED_BUILDS: pnpm sperrt die Installationsskripte des mitgelieferten Gemini-SDK und dessen protobufjs und hinterlässt eine unbeantwortete pnpm-workspace.yaml, an der jeder spätere pnpm-Befehl im Projekt genauso scheitert. Führe einmal pnpm approve-builds aus und bestätige keinen der beiden Einträge, oder sieh dir in der Fehlerbehebung die nicht-interaktive Lösung an. Einen npx-Umweg wie bei der CLI gibt es hier nicht, denn du importierst eine Bibliothek und startest kein Binary.

API-Keys und die Umgebung

Das SDK liest, hält oder akzeptiert nie einen API-Key. Der Provider liest seinen Key aus der Umgebung (zum Beispiel ANTHROPIC_API_KEY), wenn er konstruiert wird. Anders als die CLI lädt das SDK keine .env-Dateien: setze die Variable in deinem eigenen Skript selbst, zum Beispiel mit node --env-file=.env script.js.

Jeder Einstiegspunkt nimmt ein Eingabeobjekt, dessen erstes Feld die validierte config (aus loadConfig) ist, außer wo anders vermerkt. Die meisten akzeptieren zusätzlich ein optionales zweites deps-Argument, das eine Registry, einen Provider-Builder oder ein Dateisystem injiziert; im normalen Gebrauch kannst du es ignorieren. Siehe Die Abhängigkeits-Nahtstelle dazu, was deps.fs erreicht.

Die Abhängigkeits-Nahtstelle

deps.fs ersetzt den Dateisystem-Port, über den die eigene I/O des SDK läuft, typisiert als SdkFs. Die Nahtstelle ist vollständig: die Run-Status-Datei, die Lock-Datei, die Werkzeuge für die Glossardatei, Arbeitsmappen- und Austausch-I/O sowie die Locale-Dateien selbst gehen alle darüber, denn die Format-Adapter lesen und schreiben über einen Port, der aus demselben Objekt gebaut wird. Ein kompletter Lauf lässt sich damit im Speicher halten. Genau so vermeiden die Tests des SDK die Platte, und so kann eine einbettende Anwendung einen Teil eines Projekts auf etwas anderem als einer lokalen Platte ablegen.

Eigenschaft

Typ

Zwei Vertragspunkte muss eine Implementierung einhalten. Lesevorgänge sind größenbeschränkt: readFileBounded und readBytesBounded nehmen ein Byte-Limit entgegen und melden missing oder too-large als Zustand, statt zu werfen. Das hält eine bösartige oder versehentlich riesige Datei davon ab, den Speicher zu erschöpfen. Schreibvorgänge sollten atomar sein, damit ein Absturz mitten im Schreiben nie eine halb geschriebene Datei hinterlässt, und createExclusive muss atomar gegenüber anderen Prozessen sein, weil es das Primitiv hinter der Schreibsperre pro Locale ist. Das SDK ruft vor dem Schreiben einer Locale-Datei, der Laufstatus-Datei oder einer Ausgabedatei wie einer Arbeitsmappe das optionale mkdir mit dem übergeordneten Verzeichnis auf, writeFile und writeBytes müssen es also nicht selbst anlegen; lass mkdir weg, wenn der Speicher keine Verzeichnisse kennt. createExclusive bekommt keinen solchen Aufruf und legt ein fehlendes übergeordnetes Verzeichnis selbst an. Die übrigen Pflichtmitglieder sind fileExists und deleteFile, das auch gelingt, wenn die Datei schon fehlt.

readDirectory ist optional, damit eine Implementierung, die vor der Quellcode-Extraktion geschrieben wurde, weiterhin kompiliert. extract ist der einzige Einstiegspunkt, der es braucht, und meldet sein Fehlen als EXTRACT_FS_UNSUPPORTED. Es listet die direkten Einträge eines Verzeichnisses als { name, kind } auf, wobei kind "file", "directory" oder "other" ist. Ein symbolischer Link gehört zu "other": Der Scan folgt ihm nie, und genau das hält ihn innerhalb der übergebenen Roots.

realpath ist ebenfalls optional. Es löst jeden symbolischen Link in einem bestehenden Pfad auf, und die Ausgabe-Guards von generateTypes, exportTmx, exportWorkbook und pseudolocalize sehen damit, wo ein Ausgabepfad wirklich landet, sodass ein Link eine geschriebene Datei weder aus cwd hinaus noch auf eine Datei tragen kann, von der das Projekt abhängt. Ohne es vergleichen diese Guards die Pfade so, wie sie geschrieben sind. Ein Link am Ausgabepfad, dessen Ziel nicht existiert, lässt sich nicht auflösen, daher gehen die Guards davon aus, dass writeFile und writeBytes einen Link dort ersetzen, statt durch ihn hindurch zu schreiben, so wie es der atomare Standard-Schreibvorgang tut.

rename ist ebenfalls optional.

Es verschiebt eine Datei in einem atomaren Schritt unter einen neuen Namen im selben Verzeichnis, ersetzt dabei eine dort schon liegende Datei und scheitert mit einem Fehler, dessen code ENOENT ist, wenn die Quelle fehlt. Die Schreibsperre nutzt es, um eine Sperre oder einen Übernahme-Guard eines beendeten Prozesses zu entfernen und um ihre eigene Sperre freizugeben: Die Datei wird unter einem eindeutigen Namen <name>.<pid>.<tag>.<uuid>.stale beiseitegelegt und nur gelöscht, wenn sie noch der Eintrag ist, den die Sperre erwartet. Alles andere, etwa eine Sperre, die ein anderer Prozess inzwischen genommen hat, wird mit createExclusive zurückgelegt. Scheitert das Umbenennen mit EPERM, EBUSY oder EACCES, wie Windows es für eine Datei meldet, die ein anderer Prozess offen hat, gilt das als "in dieser Abfrage nicht übernommen", und die Sperre fragt bis zu ihrem Timeout weiter ab. Scheitert das Umbenennen beim Freigeben einer Sperre aus irgendeinem Grund, liest die Freigabe stattdessen die Sperrdatei und löscht sie, solange sie noch das Token des Halters enthält, und versucht einen Fehler mit EPERM, EBUSY oder EACCES ein paar Mal im Abstand einer Abfrage erneut. Ein Eintrag, der zurückgelegt werden müsste, aber nicht zurückgelegt werden kann, weil ein dritter Prozess den Pfad inzwischen genommen hat, wird gelöscht, da die Token-Prüfung seines Halters ab dann ohnehin scheitert. Ohne rename liest die Sperre die Datei und löscht sie, wenn sie unverändert ist. Das lässt ein breiteres Zeitfenster offen, in dem eine gerade genommene Sperre eines anderen Prozesses gelöscht wird.

Beiseitelegen und Zurücklegen sind zwei Schritte, daher können in seltenen Abläufen zwei Prozesse kurz beide glauben, dieselbe Sperre zu halten. Jede Sperre hält deshalb ein zufälliges Besitz-Token fest, und vor jedem Schreibvorgang, den die Sperre schützt (die Locale-Datei, die Lock-Datei, die Provenance-Datei, das Glossar), prüft der Halter, ob die Sperrdatei noch sein Token enthält. Ist das nicht so, bricht die Operation mit LOCK_CONTENDED ab und schreibt nichts. Eine Sperrdatei, die beim Freigeben fehlt oder ein anderes Token enthält, wird genauso gemeldet, wenn die Operation erfolgreich war, denn der andere Prozess kann dieselben Dateien seitdem geändert haben; eine gescheiterte Operation wirft ihren eigenen Fehler. Prüfung und Schreiben bleiben zwei Schritte, und nichts begrenzt die Zeit dazwischen: Ein Halter, der direkt nach der Prüfung angehalten wird (eine schlafende Maschine, ein gestoppter Prozess, ein Debugger, ein Sprung der Uhr oder eine lange blockierte Event-Loop), setzt seinen Heartbeat aus. Sobald seine Sperre drei Intervalle alt ist (standardmäßig 30 Sekunden), übernimmt ein anderer Prozess auf derselben Maschine sie, und der angehaltene Halter kann beim Weiterlaufen noch einmal schreiben. Die Token-Prüfung setzt voraus, dass createExclusive eine Datei anlegt, die readFileBounded zurückliest; eine eigene Implementierung muss das einhalten.

touch und mtimeMs sind ebenfalls optional.

touch(path) setzt die Änderungszeit einer vorhandenen Datei auf jetzt, und mtimeMs(path) gibt sie in Millisekunden zurück, oder undefined, wenn die Datei fehlt. Mit touch hält ein Sperr-Halter ein Heartbeat-Intervall von 10 Sekunden in seiner Sperre fest und frischt die Änderungszeit der Datei in diesem Takt auf, solange er die Sperre hält. Direkt nach dem Anlegen setzt er sie ebenfalls, damit das Alter der Datei nach der Uhr dieses Prozesses gemessen wird und nicht nach der des Dateiservers. Mit mtimeMs behandelt ein wartender Prozess eine Sperre auf seiner eigenen Maschine als verwaist, deren Prozess noch läuft, deren Datei aber seit drei der festgehaltenen Intervalle (standardmäßig 30 Sekunden) nicht aufgefrischt wurde. Das deckt einen Sperreintrag ab, der seinen Halter überlebt hat, etwa einen, der für einen lange laufenden Studio-, watch- oder MCP-Server-Prozess zurückgelegt wurde, der ihn schon freigegeben hatte. Ein Übernahme-Guard hält keinen Heartbeat fest: Er wird abgeräumt, sobald sein Prozess nachweislich nicht mehr existiert und er über zwei Abfragen unverändert gesehen wurde. Ohne diese Member gilt eine Sperre nur dann als verwaist, wenn ihr Prozess nachweislich nicht mehr existiert.

Ein Prozess, der abstürzt, während er eine Sperre beiseitegelegt hat, kann eine .stale-Datei in .verbatra-local/locks/ hinterlassen. Solche Dateien kannst du gefahrlos von Hand löschen, und der nächste Erwerb einer Sperre löscht sie, sobald der Prozess, der sie verschoben hat, nicht mehr existiert oder, wenn touch implementiert ist, sobald sie älter als die Veraltungsschwelle sind, gezählt ab dem Beiseitelegen. Ein Dateisystem ohne touch behält sie, bis ihr Verschieber nicht mehr existiert.

deps.fs erreicht keine vom Aufrufer übergebene deps.adapterRegistry. Diese Adapter hat der Aufrufer konstruiert, ihr Dateizugriff ist also genau das, was der Aufrufer darin verdrahtet hat. Beides zu übergeben heißt, dass der Aufrufer diese Verdrahtung besitzt.

Auch das Laden der Konfiguration liegt außerhalb der Nahtstelle. loadConfig und loadConfigWithMeta nehmen keine deps: sie lesen die Konfigurationsdatei selbst von der echten Platte und eine Glossardatei, die die Konfiguration nennt, über ihre eigene Option fs, das deps.fs eines späteren Flows erreicht also keinen der beiden Lesevorgänge. Um ein ganzes Projekt im Speicher zu halten, übergib dasselbe SdkFs als loadConfig({ fs }) und reich die Konfiguration als configOverride statt als Datei herein. Die Flows lösen Pfade gegen ihr eigenes cwd auf, gib es also auch dort mit.

Das Beispiel hält ein ganzes Projekt im Speicher und kommt mit den Pflichtmitgliedern aus. Mit dem Provider none braucht der Lauf keinen Schlüssel und lässt greeting für einen Menschen zum Übersetzen übrig.

Beispiel
import { check, loadConfig, type SdkFs, translate } from "@verbatra/sdk";

const files = new Map<string, Uint8Array>();
const encoder = new TextEncoder();
const decoder = new TextDecoder();

function bounded(path: string, maxBytes: number) {
  const bytes = files.get(path);
  if (bytes === undefined) return { kind: "missing" } as const;
  if (bytes.byteLength > maxBytes) return { kind: "too-large" } as const;
  return { kind: "ok", bytes } as const;
}

const memoryFs: SdkFs = {
  fileExists: async (path) => files.has(path),
  readFileBounded: async (path, maxBytes) => {
    const read = bounded(path, maxBytes);
    return read.kind === "ok" ? { kind: "ok", content: decoder.decode(read.bytes) } : read;
  },
  readBytesBounded: async (path, maxBytes) => bounded(path, maxBytes),
  writeFile: async (path, data) => {
    files.set(path, encoder.encode(data));
  },
  writeBytes: async (path, data) => {
    files.set(path, data);
  },
  createExclusive: async (path, data) => {
    if (files.has(path)) return false;
    files.set(path, encoder.encode(data));
    return true;
  },
  deleteFile: async (path) => {
    files.delete(path);
  },
};

files.set("/project/locales/en.json", encoder.encode('{ "greeting": "Hello, {{name}}!" }'));

const config = await loadConfig({
  cwd: "/project",
  fs: memoryFs,
  configOverride: {
    sourceLocale: "en",
    targetLocales: ["de"],
    format: "i18next-json",
    files: { pattern: "locales/{locale}.json" },
    provider: { id: "none" },
  },
});

const summary = await translate({ config, cwd: "/project" }, { fs: memoryFs });
const status = await check({ config, cwd: "/project" }, { fs: memoryFs });

console.log(summary.locales[0]?.unfilled, status.inSync); // [ 'greeting' ] false
console.log([...files.keys()]); // the source, verbatra.lock.json, .verbatra-local/run-status.json
Auf GitHub bearbeiten

Auf dieser Seite