Zum Inhalt springen
Zum Inhalt springen

Docs / Anleitungen

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-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.

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.

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_CANCELLED, eine, die er mitten im Lauf gestoppt hat, die 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

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_NOT_CONFIGURED. 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_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.

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_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.

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 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.

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

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_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

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 });
Auf GitHub bearbeiten

Auf dieser Seite