---
title: "Recettes SDK"
description: "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."
type: "how-to"
---
# 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](https://verbatra.kreitz-webdev.de/fr/docs/sdk-quickstart.md) 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](https://verbatra.kreitz-webdev.de/fr/docs/sdk.md) 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 [#estimer-une-exécution-avant-de-dépenser]

<AvailableFrom version="0.11.0" />

`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`](https://verbatra.kreitz-webdev.de/fr/docs/config-file.md#rates) l'a chiffré.

```ts
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](https://verbatra.kreitz-webdev.de/fr/docs/estimating-cost.md) explique la méthode,
et [RunSummary](https://verbatra.kreitz-webdev.de/fr/docs/sdk/run-summary.md) liste ce que chaque `EstimateCaveatCode` de `caveats` laisse
de côté.

## Vérifier en CI sans écrire [#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.

```ts
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](https://verbatra.kreitz-webdev.de/fr/docs/ci-and-exit-codes.md).

```ts
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é [#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.

```ts title="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](https://verbatra.kreitz-webdev.de/fr/docs/sdk/extend.md#fournisseurs-personnalisés).

## Surveiller dans un processus de longue durée [#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.

```ts
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 [#annuler-une-exécution]

<AvailableFrom version="0.12.0" />

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

```ts
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`](https://verbatra.kreitz-webdev.de/fr/docs/error-codes.md#run_cancelled), et une qu'elle a arrêtée en cours d'exécution, l'[avis `RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/fr/docs/error-codes.md#notice-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 [#extraire-les-clés-de-ton-code-source]

<AvailableFrom version="0.11.0" />

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

```ts
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`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/inspect.md#extract) liste toutes les autres erreurs
qu'il lève.

## Générer un catalogue typé [#générer-un-catalogue-typé]

<AvailableFrom version="0.11.0" />

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

```ts
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`](https://verbatra.kreitz-webdev.de/fr/docs/cli/types.md) liste tout ce qu'une exécution qui génère refuse.

## Construire une pseudo-locale [#construire-une-pseudo-locale]

<AvailableFrom version="0.11.0" />

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

```ts
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é [#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.

```ts
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`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/review.md#retranslateentry) liste ce que chacune lève.

### Valeurs écrites par une personne [#valeurs-écrites-par-une-personne]

<AvailableFrom version="0.12.0" />

`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](https://verbatra.kreitz-webdev.de/fr/docs/protecting-human-translations.md).

## Traiter la file de relecture [#traiter-la-file-de-relecture]

<AvailableFrom version="0.12.0" />

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

```ts title="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](https://verbatra.kreitz-webdev.de/fr/docs/sdk/review.md#approveentry) présente les variantes par lot et chaque code qu'elles lèvent.

## Lire toutes les valeurs en bloc [#lire-toutes-les-valeurs-en-bloc]

<AvailableFrom version="0.10.0" />

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


```ts title="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 [#laller-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`.

```ts
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](https://verbatra.kreitz-webdev.de/fr/docs/manual-translation.md) pour ce que les traducteurs peuvent modifier et comment les lignes sont validées.

## L'aller-retour TMX [#laller-retour-tmx]

<AvailableFrom version="0.11.0" />

`importTmx` et `exportTmx` font entrer et sortir la [mémoire de traduction](https://verbatra.kreitz-webdev.de/fr/docs/the-cache.md) 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.

```ts
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 [#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.

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