CI und Exit-Codes
Sichere eine Pipeline mit check oder diff gegen Übersetzungsdrift ab, lies den Exit-Code-Vertrag und verarbeite die JSON-Ausgabe.
Maschinell übersetzte Seite
Diese Anleitung zeigt, wie du CI fehlschlagen lässt, wenn Übersetzungen auseinanderlaufen, und wie du liest, was verbatra dann meldet. Die Werkzeuge dafür sind der Exit-Code-Vertrag, dem jeder Befehl folgt, und die --json-Ausgabe, die deine Skripte parsen können.
verbatra ist eine Dev-Dependency, die Befehle laufen in CI also genauso wie lokal. Die übliche Aufteilung:
- Sichere einen Pull Request ab mit
verbatra checkoderverbatra diff. Beide sind schreibgeschützt: kein Provider-Aufruf, kein API-Key nötig, Exit1bei Drift. - Übersetze bei einem Push mit
verbatra translate, entweder direkt (diese Seite) oder über die GitHub Action.
Die Exit-Codes
Der Exit-Code ist der Vertrag, auf den dein CI-Schritt verzweigt:
| Code | Bedeutung |
|---|---|
0 | Erfolg: translate oder import schloss jedes Locale vollständig ab, check fand jedes Locale synchron, diff fand keine ausstehenden Änderungen, export schrieb seine Arbeitsmappe, init legte das Projekt an, watch oder studio wurde sauber beendet, oder --help beziehungsweise --version wurde ausgegeben |
1 | der Lauf kam durch, aber das Ergebnis ist nicht sauber: translate oder import endete mit mindestens einem fehlgeschlagenen oder partial gebliebenen Locale, check fand ein Locale nicht synchron, diff fand einen fehlenden oder geänderten Key, oder studio scheiterte beim Herunterfahren seines Servers |
2 | konnte nicht laufen: ein Fehler des gesamten Laufs (kaputte Konfiguration, unlesbare Quelle, kaputte Lock-Datei), ein Verwendungsfehler (ein leerer oder unbekannter --locales-Wert, ein ungültiges --debounce oder --port), init ohne auflösbaren Provider oder wenn es keine gültige Konfiguration anlegen kann, ein gescheiterter Start oder Stopp von watch, oder studio, das die Konfiguration nicht laden, @verbatra/studio nicht importieren oder seinen Server nicht starten kann |
130 | watch oder studio wurde durch einen zweiten Interrupt zwangsbeendet |
Einige Randfälle, die du kennen solltest:
- Ein Locale mit dem Status
partialzählt als Fehlschlag. Das heißt, die Datei wurde geschrieben, aber einige Keys fehlen weiterhin, meist weil ein Provider-Sub-Batch fehlschlug oder das Integritäts-Gate eine Übersetzung abgelehnt hat. Es endet mit1genau wie ein fehlgeschlagenes Locale, denn ein halb übersetztes Locale auf der Platte ist kein Zustand, den eine Pipeline durchwinken sollte. Um beide auseinanderzuhalten, liespartialin der Zusammenfassung. - Eine kaputte Lock-Datei ist bei
translatewie beiimportein Fehler des gesamten Laufs, nie ein einzelnes fehlgeschlagenes Locale. Sie ist eine einzige gemeinsame Datei, deshalb bricht ein Lauf, dessen Lock-Datei mittendrin kaputtgeht, mit2ab, statt die restlichen Locales weiter abzuarbeiten. Was vor dem Abbruch geschrieben wurde, bleibt auf der Platte: Repariere die Lock-Datei und starte den Befehl erneut. - Ein einzelner Interrupt ist ein sauberer Stopp, und sowohl
watchals auchstudioenden dabei mit0. Geh darüber hinaus nicht davon aus, dass sich beide gleich verhalten: scheitert der Stopp selbst, endetwatchmit2undstudiomit1. - Ein fehlgeschlagener Lauf während
watcherscheint als Datensatz im Ausgabestrom, nie als Exit-Code ungleich null. exporthat keinen Fehlermodus pro Locale: der Befehl endet mit0oder2, nie mit1.- Ein Fehlermodus liegt außerhalb des Vertrags: ein Parse-Fehler, der kein Verwendungsfehler ist, wird weitergeworfen, und die Binary fängt ihn nicht ab. Damit greift Nodes Standardverhalten für eine unbehandelte Rejection statt eines der vier Codes.
check oder diff: das Gate wählen
check und diff führen dieselbe schreibgeschützte Berechnung über deine Quelle, die Zieldateien und die Lock-Datei aus. Der Unterschied ist, was sie melden:
# counts per locale: exit 1 if any locale is missing or stale
verbatra check
# key lists per locale: exit 1 if any locale has keys to add or re-translate
verbatra diffNimm check, wenn der Exit-Code alles ist, was du brauchst. Nimm diff, wenn du die genauen Keys hinter der Drift willst, etwa um sie als Kommentar in einen Pull Request zu posten. Verwaiste Keys (in einer Zieldatei vorhanden, aber aus der Quelle verschwunden) erscheinen in der diff-Ausgabe, setzen aber nie allein Exit-Code 1.
Beide nehmen --locales de,fr, um nur eine Teilmenge abzusichern. --locales ohne gültiges Locale ist ein Verwendungsfehler und endet mit 2, ein Tippfehler kann das Gate also nie grün schalten. translate, watch und export nehmen dasselbe Flag; so übersetzt du gegen einen ratenbegrenzten Provider ein Locale nach dem anderen.
JSON-Ausgabe
Sechs Befehle akzeptieren --json für maschinenlesbare Ausgabe auf stdout: translate, watch, check, diff, export und import. Jeder Datensatz ist eine Zeile, und jeder Datensatz steckt im selben Envelope. Du verzweigst also auf ein einziges Feld und musst nie raten, von welchem Befehl die Daten stammen, die du gerade in der Hand hast:
type Envelope<TResult> =
| { ok: true; version: 1; command: string; result: TResult }
| { ok: false; version: 1; command: string | null; code: string; message: string };version ist die Version dieser Envelope-Form, nicht die Version des Pakets. Es ist eine ganze Zahl, du vergleichst sie also mit ===, statt einen Bereich zu parsen, und sie ändert sich nur, wenn ein bestehendes Feld seine Bedeutung ändert oder verschwindet. Neue Felder können ohne Erhöhung dazukommen, ignoriere also die, die du nicht kennst.
Ein Lauf, der als Ganzes fehlschlägt, schreibt genau einen ok: false-Datensatz auf stdout und endet mit 2. Sein code ist derselbe stabile Fehlercode, den auch die stderr-Zeile trägt, und damit das, worauf du verzweigst:
{ "ok": false, "version": 1, "command": "translate", "code": "CONFIG_INVALID", "message": "..." }command ist nur dann null, wenn der Fehlschlag eintrat, bevor ein Unterbefehl aufgelöst war.
Fehler gehen zusätzlich in beiden Modi unverändert als eine strukturierte Zeile (verbatra: error [CODE] message) auf stderr, ein Skript, das Exit-Code und stderr liest, braucht also keine Änderung. Ohne --json schreibt ein fehlgeschlagener Lauf weiterhin überhaupt nichts auf stdout. Datensätze zu Fortschritt und Lock-Wartezeit gehen immer auf stderr, auf stdout stehen also ausschließlich Envelopes.
Der Rest dieses Abschnitts beschreibt das result, das jeder Befehl in einen erfolgreichen Envelope legt.
verbatra translate --json und verbatra import --json tragen ein RunSummary:
interface RunSummary {
dryRun: boolean; // whether this was a dry run (no provider calls, no writes)
locales: LocaleSummary[]; // one entry per target locale, in config order
succeeded: string[]; // locales whose run succeeded
partial: string[]; // locales written with keys still missing; these exit 1 too
failed: string[]; // locales whose run failed
usage?: UsageSummary; // summed input/output tokens; absent when no call reported usage
budget?: RunBudget; // the token-budget outcome; present only when maxTokens is configured
}Jedes LocaleSummary trägt die Key-Listen pro Locale (übersetzt, unverändert, verwaist, zurückgehalten, für Review markiert und mehr); die vollständige Anatomie steht in der SDK-Referenz.
verbatra watch --json gibt pro Lauf einen Envelope als NDJSON aus (ein JSON-Objekt pro Zeile), mit command: "watch". Ein erfolgreicher Lauf ist ein ok: true-Datensatz mit dem RunSummary dieses Laufs; ein fehlgeschlagener Lauf ist ein ok: false-Datensatz mit seinem Code und seiner Meldung. Ein fehlgeschlagener Lauf ist nur ein Datensatz im Strom: er stoppt weder den Watcher noch ändert er den Exit-Code.
verbatra check --json trägt ein Statusdokument. Das oberste inSync ist genau dann true, wenn der Befehl mit 0 endet:
interface CheckSummary {
inSync: boolean; // true exactly when the command exits 0
locales: LocaleCheckSummary[];
}
interface LocaleCheckSummary {
locale: string;
missing: number; // in source, absent from target
stale: number; // source changed since last translated
upToDate: number; // target matches the recorded baseline
inSync: boolean; // missing === 0 && stale === 0
}verbatra diff --json trägt Key-Listen statt Zähler. Das oberste hasPendingChanges ist genau dann true, wenn der Befehl mit 1 endet:
interface DiffSummary {
hasPendingChanges: boolean; // true exactly when the command exits 1
locales: LocaleDiff[];
}
interface LocaleDiff {
locale: string;
missing: string[]; // in source, absent from target: would be added
changed: string[]; // source changed since last translated: would be re-translated
orphaned: string[]; // in target, absent from source: reported only
hasPendingChanges: boolean; // missing.length > 0 || changed.length > 0
}verbatra export --json trägt den Pfad, unter dem die Arbeitsmappe geschrieben wurde, plus die Zeilenzahlen pro Locale:
{
path: string; // absolute path of the written workbook
locales: { locale: string; rows: number }[];
}Ein GitHub-Actions-Job mit der CLI
Ein Drift-Gate für Pull Requests, das die CLI direkt ausführt. check ruft nie einen Provider auf, dieser Job braucht also gar keinen API-Key:
name: i18n
on: pull_request
permissions:
contents: read
jobs:
check-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<commit-sha>
- uses: pnpm/action-setup@<commit-sha>
- uses: actions/setup-node@<commit-sha>
with:
node-version: 22
- run: pnpm install --frozen-lockfile
- run: pnpm exec verbatra checkUm stattdessen in CI zu übersetzen, tausche den letzten Schritt gegen translate und übergib den Provider-Key aus deinem Secret-Store als die Umgebungsvariable, die dein Provider erwartet (siehe Provider):
- run: pnpm exec verbatra translate --json
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Wenn du diesen Job lieber nicht selbst schreiben willst: die GitHub Action verpackt die translate-Variante mit Annotationen und einer Job-Zusammenfassung.
Eingefrorene Installationen und Keys
- Installiere aus dem Lockfile.
pnpm install --frozen-lockfile(odernpm ci) pinnt genau das@verbatra/cli-Release, das dein Lockfile festhält; ein CI-Lauf ist damit reproduzierbar und kann nie stillschweigend ein neueres Release ziehen. Die CLI braucht Node>=22.14.0. - Keys sind Umgebungsvariablen, nie Flags. Die CLI nimmt kein Key-Argument entgegen und liest keinen Key aus der Konfiguration; Provider lesen nur ihre Umgebungsvariable (
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY,DEEPL_API_KEY). Lege den Key im Secret-Store deiner CI ab und reiche ihn überenvdurch. Fehlermeldungen nennen die Variable, enthalten aber nie einen Key-Wert. - Schreibgeschützte Gates brauchen keinen Key.
check,diffundexportrufen nie einen Provider auf, halte Secrets also komplett aus diesen Jobs heraus.
translate, watch und studio laden außerdem .env.local und dann .env aus dem Arbeitsverzeichnis, bevor sie laufen, wobei echte Umgebungsvariablen immer gewinnen; check, diff, export und import laden keine .env-Dateien, sodass du dich in CI normalerweise allein auf env: verlässt.
Kosten abschätzen
Schätze den Umfang eines Übersetzungslaufs ab, bevor du Geld ausgibst: zähle die Keys mit einem Dry Run, rechne sie in Requests und Tokens um und bepreise sie mit den Raten deines Providers.
GitHub Action
Führe verbatra translate in GitHub Actions mit der Composite Action aus: Eingaben, Secret-Verdrahtung, Annotationen und die Job-Zusammenfassung.