Aller au contenu
Aller au contenu

Docs / Guides

Page traduite automatiquement

Cette page a été traduite automatiquement, elle peut donc contenir des erreurs ou se lire un peu bizarrement. La version anglaise fait foi. Lire l'original en anglais.

Recettes SDK

Des exemples complets et exécutables pour piloter verbatra depuis tes scripts et jobs CI, chacun bâti sur les points d'entrée publics de @verbatra/sdk.

Le démarrage rapide du SDK couvre la première exécution. Cette page prend le relais avec des recettes complètes à copier dans un script ; la référence SDK catalogue chaque point d'entrée. Le SDK ne charge pas les fichiers .env, donc lance une recette qui appelle un fournisseur avec node --env-file=.env. Pour avancer avec un fournisseur limité en débit une langue à la fois, passe locales: ["de"] à translate.

Estimer une exécution avant de dépenser

estimate: true implique dryRun. Le chiffre arrive sur RunSummary.estimate : base ton branchement sur unit pour la dimension facturée et sur pricing pour savoir si un bloc rates l'a chiffré.

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(", ")}`);
}

Cela borne le plan, pas la facture ; Estimer le coût explique la méthode, et RunSummary liste ce que chaque EstimateCaveatCode de caveats laisse de côté.

Vérifier en CI sans écrire

check lit et compare sans appeler de fournisseur ni toucher aucun fichier, et son champ inSync est vrai exactement quand rien n'est manquant ni obsolète.

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;
}

Besoin des noms de clés plutôt que des comptes ? Remplace par diff, qui renvoie les listes de clés missing, changed et orphaned par locale, avec le même verdict hasPendingChanges. Tu lances plutôt la CLI en CI ? Voir CI et codes de sortie.

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;
}

Tester ton intégration sans clé

Un faux fournisseur passé en createProvider exécute tout le flux, fichiers et contrôle d'intégrité compris, sans clé, sans appel réseau et avec une sortie déterministe : ce dont un test unitaire de ton propre outillage a besoin.

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;
}

L'exécution écrit de vrais fichiers, donc teste sur une copie temporaire du projet. Un vrai fournisseur se branche de la même façon ; voir Fournisseurs personnalisés.

Surveiller dans un processus de longue durée

watch déclenche une exécution immédiatement au démarrage, puis une par changement temporisé de la source, en rapportant chacune via onRun. Pour un arrêt propre, attends controller.stop() : il ferme la surveillance et attend la fin de l'exécution en cours avant que ton processus se termine.

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);
  });
});

Un échec d'exécution après le démarrage ne lève jamais ; il arrive comme { status: "failed" } et la surveillance continue.

Annuler une exécution

Passe un AbortSignal comme signal. translate se résout alors au lieu de rejeter : il écrit ce qui est déjà arrivé, laisse le reste en attente et fixe 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;
}

Une locale que l'annulation a empêchée de démarrer porte le code d'erreur RUN_CANCELLED, et une qu'elle a arrêtée en cours d'exécution, l'avis RUN_CANCELLED. Une annulation qui arrive après la fin de chaque locale laisse cancelled absent, et une erreur de toute l'exécution au démarrage rejette toujours.

Extraire les clés de ton code source

extract est le seul point d'entrée qui lit du code applicatif plutôt qu'un fichier de locale. Il n'écrit que le catalogue source, et seulement les clés vraiment nouvelles : une clé déjà présente garde sa valeur, et une clé qu'aucun site d'appel ne mentionne est laissée telle quelle. Tout ce que l'analyse ne peut pas résoudre revient sous forme de données, donc un dry run fait un bon garde-fou avant l'exécution qui écrit.

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 doit nommer le framework et les racines de sources, sinon l'appel lève EXTRACT_NOT_CONFIGURED. extract liste toutes les autres erreurs qu'il lève.

Générer un catalogue typé

generateTypes déclare le catalogue source en TypeScript : une union de toutes les clés, plus les arguments que chaque message interpole. Il lit un fichier et écrit un fichier, et deux exécutions sur un catalogue inchangé écrivent des octets identiques, ce qui fait du mode check un garde-fou de CI exploitable.

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;
}

Passe configPath pour que le fichier de configuration chargé par l'exécution soit refusé comme chemin de sortie. Une exécution avec check n'écrit rien et ne signale que stale. verbatra types liste tout ce qu'une exécution qui génère refuse.

Construire une pseudo-locale

pseudolocalize construit une locale factice à partir des seules chaînes source, pour que les troncatures de mise en page et les chaînes jamais extraites apparaissent avant même qu'une clé d'API existe. Il n'appelle aucun fournisseur, et sa sortie atterrit hors de files.pattern : aucune exécution ne dépense dessus ni ne la signale comme ayant dérivé.

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 vaut en-XA par défaut, ou ar-XB avec mode: "bidi", et out vaut .verbatra-local/pseudo. La transformation est déterministe : une seconde exécution sur une source inchangée ne réécrit rien et signale written à false.

Modifier et retraduire une seule clé

Ce sont les points d'accroche que Verbatra Studio pilote ; utilise-les pour construire ton propre flux de relecture. Lis les valeurs actuelles avec keyValue, enregistre une correction humaine avec editEntry, ou relance le fournisseur pour une clé avec retranslateEntry. Les deux fonctions d'écriture passent la valeur candidate par les mêmes contrôles d'intégrité qu'une exécution complète et renvoient un résultat à deux branches au lieu de lever sur une valeur rejetée.

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(", ")}`);
  }
}

retranslateEntry liste ce que chacune lève.

Valeurs écrites par une personne

retranslateEntry refuse avec KEY_PROTECTED une valeur écrite par une personne, y compris celle que editEntry vient d'enregistrer, sauf si tu passes includeHuman: true ou si la config définit humanEdits: "overwrite". C'est pourquoi la recette le passe. Une clé épinglée est toujours refusée ; voir Protéger les traductions humaines.

Traiter la file de relecture

reviewQueue liste chaque valeur écrite par une machine que personne n'a encore approuvée, avec les raisons pour lesquelles la dernière exécution l'a signalée. Ce script approuve les valeurs non signalées et rejette les autres, ce qui les retire pour que le prochain translate les remplisse à nouveau. expectedValue fait lever REVIEW_VALUE_CHANGED à un appel si la valeur a changé entre-temps. Sans trace de la dernière exécution sur cette machine, comme dans un clone tout neuf ou sur un runner de CI, reasons est vide aussi : le script s'arrête donc si lastRunAt manque.

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}`);
    }
  }
}

Relire et retraduire présente les variantes par lot et chaque code qu'elles lèvent.

Lire toutes les valeurs en bloc

localeValues est le pendant en masse de keyValue : une passe sur les fichiers déjà présents sur le disque, qui renvoie par locale le texte source et le texte cible actuels de chaque clé.

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}`);
    }
  }
}

En lecture seule : il n'appelle aucun fournisseur, n'écrit rien et ne touche ni au fichier de verrouillage ni au cache. Il couvre toutes les locales cibles configurées ; passe locales pour le limiter à certaines d'entre elles, et une locale hors de targetLocales lève UNKNOWN_LOCALE.

L'aller-retour du classeur

Exporte les chaînes à traduire dans un classeur Excel, confie-le à un traducteur, puis réimporte le fichier rempli. importWorkbook exécute les mêmes vérifications de dérive et d'intégrité que translate et renvoie la même forme RunSummary.

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`);

Passe dryRun: true à importWorkbook pour valider un fichier renvoyé sans rien écrire. Voir Traduction humaine pour ce que les traducteurs peuvent modifier et comment les lignes sont validées.

L'aller-retour TMX

importTmx et exportTmx font entrer et sortir la mémoire de traduction au format TMX, le format d'échange que produisent et lisent les plateformes de traduction courantes. Contrairement à la paire classeur, elle déplace la mémoire entière comme un seul fichier multilingue entre outils.

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 signale les données abîmées au lieu de lever dessus : un fichier refusé lève SOURCE_INVALID, et tmxErrorLocation lit cet emplacement dans l'erreur sans avoir à la caster ni à caster son cause. Seules les entrées sous l'empreinte de configuration actuelle sont exportées, soit exactement l'ensemble qu'une exécution réutiliserait.

Une config sans fichier

loadConfig accepte un configOverride en mémoire, validé exactement comme un fichier chargé, donc tu peux piloter verbatra entièrement depuis le code, sans fichier de config sur disque.

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 });
Modifier sur GitHub

Sur cette page