---
title: "Référence du SDK"
description: "Installe @verbatra/sdk et trouve la page de référence de chaque point d'entrée. Les fichiers absents du disque passent par ton propre système de fichiers."
type: "reference"
---
# Référence du SDK

Installe @verbatra/sdk et trouve la page de référence de chaque point d'entrée. Les fichiers absents du disque passent par ton propre système de fichiers.

`@verbatra/sdk` est le moteur sur lequel tourne la commande `verbatra`. La CLI est une surcouche légère, donc tout ce que la ligne de commande fait, tu peux le faire en code.

## Par où commencer [#par-où-commencer]

Nouveau dans le SDK ? Commence par l'une de ces pages :

<Cards>
  <Card title="Démarrage rapide du SDK" href="https://verbatra.kreitz-webdev.de/fr/docs/sdk-quickstart.md">Installe le SDK, prévisualise une exécution sans clé, traduis depuis ton propre script, gère les erreurs et protège la CI.</Card>
  <Card title="Recettes SDK" href="https://verbatra.kreitz-webdev.de/fr/docs/programmatic-api.md">Des scripts complets pour les estimations, le mode watch, un faux fournisseur, la file de relecture et les allers-retours par classeur et TMX.</Card>
</Cards>

## Pages de référence [#pages-de-référence]

Ces pages cataloguent toute la surface publique, groupée par la tâche de chaque point d'entrée.

<SdkEntryPoints />

<SdkAnchorForward />

## Installation [#installation]

```bash
npm install --save-dev @verbatra/sdk
```

Requiert Node.js `^22.22.0 || >=24`.

Avec pnpm, `pnpm add --save-dev @verbatra/sdk` installe correctement mais se termine avec le code `1` et `ERR_PNPM_IGNORED_BUILDS` : pnpm bloque les scripts d'installation du SDK Gemini embarqué et de son `protobufjs`, et laisse un `pnpm-workspace.yaml` sans réponse qui fait échouer de la même façon toutes les commandes pnpm suivantes du projet. Lance `pnpm approve-builds` une fois sans approuver aucune des deux entrées, ou consulte le [Dépannage](https://verbatra.kreitz-webdev.de/fr/docs/troubleshooting.md) pour la solution non interactive. Il n'y a pas de raccourci `npx` pour contourner ça comme il en existe un pour la CLI, puisque tu importes une bibliothèque au lieu de lancer un binaire.

<Callout title="Clés d'API et environnement">
Le SDK ne lit, ne détient ni n'accepte jamais de clé d'API. Le fournisseur lit sa clé depuis l'environnement (par exemple `ANTHROPIC_API_KEY`) au moment de sa construction. Contrairement à la CLI, le SDK ne charge pas les fichiers `.env` : dans ton propre script, définis la variable toi-même, par exemple avec `node --env-file=.env script.js`.
</Callout>

Chaque point d'entrée prend un objet d'entrée dont le premier champ est la `config` validée (issue de `loadConfig`), sauf mention contraire. La plupart acceptent aussi un second argument optionnel `deps` qui injecte un registre, un constructeur de fournisseur ou un système de fichiers ; tu peux l'ignorer en usage normal. Voir [Le point d'injection de dépendances](#le-point-dinjection-de-dépendances) pour ce que `deps.fs` atteint.

## Le point d'injection de dépendances [#le-point-dinjection-de-dépendances]

<AvailableFrom version="0.9.0" />

`deps.fs` remplace le port de système de fichiers par lequel passent les entrées-sorties propres au SDK, typé `SdkFs`. Le point d'injection est complet : le fichier d'état d'exécution, le fichier de verrouillage, les outils du fichier de glossaire, les entrées-sorties de classeur et d'échange, ainsi que les fichiers de locale eux-mêmes y passent tous, car les adaptateurs de format lisent et écrivent via un port construit à partir du même objet. Une exécution entière peut donc être tenue en mémoire : c'est ainsi que les tests du SDK évitent de toucher au disque, et qu'une application hôte peut adosser une partie d'un projet à autre chose qu'un disque local.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `fileExists` | `(path: string) => Promise<boolean>` | yes | Reports whether a file exists at the path. The default implementation checks existence only. |
| `readFileBounded` | `function` | yes | Reads a file as UTF-8 text, refusing to read more than `maxBytes`. |
| `readBytesBounded` | `function` | yes | Reads a file as raw bytes, refusing to read more than `maxBytes`. |
| `writeFile` | `(path: string, data: string) => Promise<void>` | yes | Writes UTF-8 text to the path, replacing any existing file atomically. |
| `writeBytes` | `function` | yes | Writes raw bytes to the path, replacing any existing file atomically. |
| `createExclusive` | `(path: string, data: string) => Promise<boolean>` | yes | Creates a file only if it does not already exist, returning `false` when it does. This is the primitive behind the per-locale write lock, so it must be atomic against other processes. |
| `deleteFile` | `(path: string) => Promise<void>` | yes | Removes the file at the path, succeeding even when it is already absent. |
| `mkdir` | `((path: string) => Promise<void>)` | no | Creates a directory and any missing parents. Optional; omit it if the backing store has no directories. |
| `readDirectory` | `function` | no | Lists one directory's immediate entries, without recursing and without following symbolic links. Optional, so an implementation written before source extraction existed keeps working; extract is the only entry point that needs it and reports its absence as `EXTRACT_FS_UNSUPPORTED`. |
| `realpath` | `((path: string) => Promise<string>)` | no | Resolves every symbolic link in an existing path and returns the canonical absolute path. Optional, so an implementation written before it existed keeps compiling. The output guards of generateTypes, exportTmx, exportWorkbook, and pseudolocalize use it to see where a path really lands, so a symbolic link cannot carry an output file outside the working directory or onto a file the project depends on. Without it, those guards compare paths as written. A symbolic link at the output path whose target does not exist cannot be resolved, so the guards assume SdkFs.writeFile and SdkFs.writeBytes replace a link there rather than writing through it, as the default atomic write does. |
| `rename` | `((from: string, to: string) => Promise<void>)` | no | Moves the file at `from` to `to` in one atomic step, replacing any file already at `to`. The SDK only ever renames within one directory. Optional, so an implementation written before it existed keeps compiling. The write lock uses it to remove a lock or reclaim guard left by a process that is gone, and to release its own lock: it moves the file aside under a unique `<name>.<pid>.<host tag>.<uuid>.stale` name and deletes it only when what it moved is the record it expected. Anything else, such as a lock another process took in the meantime, is put back with SdkFs.createExclusive; a record that cannot be put back because a third process took the path in the meantime is deleted, since its holder's ownership check fails from then on. An abandoned reclaim guard is cleared only after it is seen unchanged across two polls, and only when its recorded process is gone: a guard records no heartbeat. A reclaim rename that fails with `EPERM`, `EBUSY`, or `EACCES`, as Windows reports for a file another process has open, is treated as "not reclaimed this poll" and retried until the lock timeout. A release rename that fails for any reason falls back to reading the lock file and deleting it when it still holds this holder's token, and that fallback retries a failure with one of those three codes a few times, a poll apart, before it gives up. Each lock records a random ownership token, and before every write it protects the holder checks that the lock file still holds its token, failing with `LOCK_CONTENDED` and writing nothing when it does not. That check and the write are two steps, and nothing bounds the time between them: a holder paused after the check (a sleeping machine, a stopped process, a debugger, a clock step, or a long block of its event loop) can have its lock judged abandoned once its heartbeat is three intervals old (30 seconds by default), and still write once when it resumes, after another process took the lock over. Without `rename`, the lock reads the file and deletes it when unchanged, a second such window. A `.stale` file left in `.verbatra-local/locks` by a process that crashed mid-move is safe to delete by hand; the next lock acquisition deletes it once the process that moved it is gone, or, on a file system that implements SdkFs.touch, once it is older than the stale threshold, measured from the move. |
| `touch` | `((path: string) => Promise<void>)` | no | Sets the modification time of an existing file to the current time of this process's clock, not the file server's. Optional. Together with SdkFs.mtimeMs it is the write lock's heartbeat: a holder whose file system implements `touch` records a heartbeat interval (10 seconds) in its lock, touches the lock file right after creating it, and refreshes its modification time at that interval while it holds the lock. A file moved aside is touched too, so its age counts from the move. |
| `mtimeMs` | `((path: string) => Promise<number \| undefined>)` | no | Reads a file's modification time. Optional. A waiting process whose file system implements it treats a lock on its own machine whose holder recorded a heartbeat interval but has not refreshed the file for three intervals (30 seconds by default) as abandoned, even when the recorded process is still running, such as a long-lived Studio, `watch`, or MCP server whose lock record outlived its hold. Without `touch` and `mtimeMs`, a lock is judged abandoned only when its recorded process is provably gone. |

Une implémentation doit respecter deux points du contrat. Les lectures sont bornées en taille : `readFileBounded` et `readBytesBounded` prennent une limite d'octets et rapportent `missing` ou `too-large` comme un état plutôt que de lever, ce qui empêche un fichier hostile ou accidentellement énorme d'épuiser la mémoire. Les écritures devraient être atomiques, pour qu'un crash en cours d'écriture ne laisse jamais un fichier à moitié écrit, et `createExclusive` doit être atomique face aux autres processus, car c'est la primitive derrière le verrou d'écriture par locale. Avant d'écrire un fichier de locale, le fichier d'état d'exécution ou un fichier de sortie comme un classeur, le SDK appelle le `mkdir` optionnel avec le répertoire parent, donc `writeFile` et `writeBytes` n'ont pas à le créer ; laisse `mkdir` de côté quand le stockage n'a pas de répertoires. `createExclusive` ne reçoit pas cet appel et crée lui-même un répertoire parent manquant. Les autres membres obligatoires sont `fileExists` et `deleteFile`, qui réussit même quand le fichier n'existe déjà plus.

`readDirectory` est optionnel, pour qu'une implémentation écrite avant l'extraction depuis le code continue de compiler. [`extract`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/inspect.md#extract) est le seul point d'entrée qui en a besoin, et signale son absence par `EXTRACT_FS_UNSUPPORTED`. Il liste les entrées immédiates d'un répertoire sous la forme `{ name, kind }`, où `kind` vaut `"file"`, `"directory"` ou `"other"`. Un lien symbolique relève de `"other"` : l'analyse ne le suit jamais, et c'est ce qui la maintient à l'intérieur des racines qu'on lui a données.

`realpath` est lui aussi optionnel. Il résout tous les liens symboliques d'un chemin existant, et
les gardes de sortie de [`generateTypes`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/run.md#generatetypes), [`exportTmx`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/exchange.md#exporttmx),
[`exportWorkbook`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/exchange.md#exportworkbook) et [`pseudolocalize`](https://verbatra.kreitz-webdev.de/fr/docs/sdk/run.md#pseudolocalize) s'en servent
pour voir où un chemin de sortie aboutit vraiment, si bien qu'un lien ne peut porter un fichier
écrit ni hors de `cwd` ni sur un fichier dont le projet dépend. Sans lui, ces gardes comparent les
chemins tels qu'ils sont écrits. Un lien au chemin de sortie dont la cible n'existe pas ne peut pas
être résolu, donc les gardes supposent que `writeFile` et `writeBytes` remplacent ce lien au lieu d'écrire à travers
lui, comme le fait l'écriture atomique par défaut.

`rename` est lui aussi optionnel.

<AvailableFrom version="0.12.0" />

Il déplace un fichier vers un nouveau nom dans le même répertoire en une seule étape atomique, en remplaçant tout fichier déjà présent, et rejette avec une erreur dont le `code` est `ENOENT` quand la source manque. Le verrou d'écriture s'en sert pour retirer un verrou ou une garde de reprise laissés par un processus disparu, et pour libérer son propre verrou : le fichier est mis de côté sous un nom unique `<name>.<pid>.<tag>.<uuid>.stale` et supprimé seulement s'il est encore l'enregistrement que le verrou attendait. Tout le reste, comme un verrou qu'un autre processus a pris entre-temps, est remis en place avec `createExclusive`. Un renommage qui échoue avec `EPERM`, `EBUSY` ou `EACCES`, comme Windows le signale pour un fichier qu'un autre processus a ouvert, compte comme « pas repris à ce sondage », et le verrou continue de sonder jusqu'à son délai. Quand le renommage qui libère un verrou échoue pour une raison quelconque, la libération se rabat sur la lecture du fichier de verrou et sa suppression tant qu'il porte encore le jeton du détenteur, et réessaie quelques fois, à un sondage d'intervalle, un échec avec `EPERM`, `EBUSY` ou `EACCES`. Un enregistrement qui aurait dû être remis en place mais ne peut pas l'être, parce qu'un troisième processus a pris le chemin entre-temps, est supprimé, puisque la vérification du jeton de son détenteur échoue dès lors. Sans `rename`, le verrou lit le fichier et le supprime s'il n'a pas changé, ce qui laisse une fenêtre plus large pendant laquelle un verrou qu'un autre processus vient de prendre est supprimé.

Mettre un fichier de côté puis le remettre en place prend deux étapes, donc dans de rares entrelacements deux processus peuvent croire un instant détenir le même verrou. Chaque verrou enregistre donc un jeton de propriété aléatoire, et avant chaque écriture que le verrou protège (le fichier de la locale, le fichier de verrouillage, le fichier de provenance, le glossaire), le détenteur vérifie que le fichier de verrou porte toujours son jeton. Sinon, l'opération s'arrête avec `LOCK_CONTENDED` et n'écrit rien. Un fichier de verrou absent ou portant un autre jeton à la libération est signalé de la même façon quand l'opération a réussi, puisque l'autre processus a pu modifier les mêmes fichiers depuis ; une opération qui a échoué lève sa propre erreur. La vérification et l'écriture restent deux étapes, et rien ne borne le temps qui les sépare : un détenteur mis en pause juste après la vérification (une machine en veille, un processus arrêté, un débogueur, un saut d'horloge ou une boucle d'événements longtemps bloquée) cesse de battre, donc une fois son verrou vieux de trois intervalles (30 secondes par défaut), un autre processus de la même machine le reprend, et le détenteur en pause peut encore écrire une fois quand il reprend. La vérification du jeton suppose que `createExclusive` crée un fichier que `readFileBounded` relit, donc une implémentation personnalisée doit tenir cette promesse.

`touch` et `mtimeMs` sont eux aussi optionnels.

<AvailableFrom version="0.12.0" />

`touch(path)` met la date de modification d'un fichier existant à maintenant, et `mtimeMs(path)` la renvoie en millisecondes, ou `undefined` quand le fichier manque. Avec `touch`, le détenteur d'un verrou y enregistre un intervalle de battement de 10 secondes et rafraîchit la date de modification du fichier à ce rythme tant qu'il détient le verrou. Il la rafraîchit aussi juste après avoir créé le fichier, pour que l'âge du fichier se mesure à l'horloge de ce processus et non à celle du serveur de fichiers. Avec `mtimeMs`, un processus en attente traite comme abandonné un verrou de sa propre machine dont le processus tourne encore mais dont le fichier n'a pas été rafraîchi depuis trois des intervalles enregistrés (30 secondes par défaut). Cela couvre un enregistrement de verrou qui a survécu à sa détention, par exemple un verrou remis en place pour un processus Studio, `watch` ou serveur MCP de longue durée qui l'avait déjà libéré. Une garde de reprise n'enregistre aucun battement : elle est retirée une fois que son processus n'existe plus de façon prouvée et qu'elle a été vue inchangée sur deux sondages. Sans ces membres, un verrou n'est jugé abandonné que lorsque son processus n'existe plus de façon prouvée.

Un processus qui plante alors qu'il a mis un verrou de côté peut laisser un fichier `.stale` dans `.verbatra-local/locks/`. Tu peux supprimer ces fichiers à la main sans risque, et la prochaine acquisition d'un verrou les supprime dès que le processus qui les a déplacés n'existe plus ou, quand `touch` est implémenté, dès qu'ils dépassent le seuil de péremption, compté depuis leur mise de côté. Un système de fichiers sans `touch` les garde jusqu'à ce que leur processus disparaisse.

`deps.fs` n'atteint pas un `deps.adapterRegistry` fourni par l'appelant. Ces adaptateurs ont été construits par l'appelant, donc leur accès aux fichiers est celui que l'appelant y a câblé. Fournir les deux signifie que l'appelant possède ce câblage.

Le chargement de la configuration reste lui aussi hors du point d'injection. `loadConfig` et `loadConfigWithMeta` ne prennent pas de `deps` : ils lisent le fichier de configuration lui-même sur le vrai disque, et un fichier de glossaire que nomme la configuration via leur propre option `fs`, donc le `deps.fs` que reçoit un flux ultérieur n'atteint aucune de ces deux lectures. Pour garder un projet entier en mémoire, passe le même `SdkFs` comme `loadConfig({ fs })` et fournis la configuration comme `configOverride` plutôt que comme fichier. Les flux résolvent les chemins par rapport à leur propre `cwd`, alors passe-le aussi là.

L'exemple garde un projet entier en mémoire avec les seuls membres obligatoires. Avec le fournisseur `none`, l'exécution n'a besoin d'aucune clé et laisse `greeting` à traduire par une personne.

```ts title="Exemple"
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
```
