---
title: "Referencia del SDK"
description: "Instala @verbatra/sdk y encuentra la página de referencia de cada punto de entrada. Los archivos que no están en disco llegan al SDK por tu sistema de archivos."
type: "reference"
---
# Referencia del SDK

Instala @verbatra/sdk y encuentra la página de referencia de cada punto de entrada. Los archivos que no están en disco llegan al SDK por tu sistema de archivos.

`@verbatra/sdk` es el motor sobre el que corre el comando `verbatra`. La CLI es una envoltura fina, así que todo
lo que hace la línea de comandos lo puedes hacer en código.

## Empieza aquí [#empieza-aquí]

¿Eres nuevo en el SDK? Empieza por una de estas páginas:

<Cards>
  <Card title="Inicio rápido del SDK" href="https://verbatra.kreitz-webdev.de/es/docs/sdk-quickstart.md">Instala el SDK, previsualiza una ejecución sin clave, traduce desde tu propio script, gestiona los errores y protege la CI.</Card>
  <Card title="Recetas del SDK" href="https://verbatra.kreitz-webdev.de/es/docs/programmatic-api.md">Scripts completos para estimaciones, el modo watch, un proveedor falso, la cola de revisión y los viajes de ida y vuelta con libro y TMX.</Card>
</Cards>

## Páginas de referencia [#páginas-de-referencia]

Estas páginas catalogan toda la superficie pública, agrupada por la tarea que hace cada punto de
entrada.

<SdkEntryPoints />

<SdkAnchorForward />

## Instalación [#instalación]

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

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

Con pnpm, `pnpm add --save-dev @verbatra/sdk` instala bien, pero sale con `1` y `ERR_PNPM_IGNORED_BUILDS`: pnpm bloquea los
scripts de instalación del SDK de Gemini incluido y de su `protobufjs`, y deja un
`pnpm-workspace.yaml` sin responder con el que cualquier comando pnpm posterior del proyecto falla
igual. Ejecuta `pnpm approve-builds` una vez y no apruebes ninguna de las dos entradas, o consulta
la [Solución de problemas](https://verbatra.kreitz-webdev.de/es/docs/troubleshooting.md) para la solución no interactiva. Aquí no hay
atajo con `npx` como en la CLI, porque estás importando una biblioteca, no ejecutando un binario.

<Callout title="Las claves de API y el entorno">
El SDK nunca lee, guarda ni acepta una clave de API. El proveedor lee su clave del entorno
(por ejemplo `ANTHROPIC_API_KEY`) cuando se construye. A diferencia de la CLI, el SDK no carga
archivos `.env`: en tu propio script, define tú la variable, por ejemplo con
`node --env-file=.env script.js`.
</Callout>

Cada punto de entrada toma un objeto de entrada cuyo primer campo es la `config` validada (de
`loadConfig`), salvo donde se indica. La mayoría también acepta un segundo argumento opcional `deps` que
inyecta un registro, un constructor de proveedor o un sistema de archivos; puedes ignorarlo
en el uso normal. Consulta
[El punto de inyección de dependencias](#el-punto-de-inyección-de-dependencias) para ver hasta dónde
llega `deps.fs`.

## El punto de inyección de dependencias [#el-punto-de-inyección-de-dependencias]

<AvailableFrom version="0.9.0" />

`deps.fs` sustituye el puerto de sistema de archivos por el que viaja la propia I/O del SDK, con el
tipo `SdkFs`. El punto de inyección es completo: el archivo de estado de la ejecución, el archivo
de bloqueo, las herramientas del archivo de glosario, la I/O de libros e intercambio y los propios archivos
de locale pasan todos por él, porque los adaptadores de formato leen y escriben a través de un
puerto construido con ese mismo objeto. Así una ejecución entera se puede mantener en memoria, que
es como las pruebas del propio SDK evitan tocar el disco y como una aplicación anfitriona puede
respaldar parte de un proyecto con algo que no sea un disco 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. |

Una implementación tiene que respetar dos puntos del contrato. Las lecturas están acotadas por
tamaño: `readFileBounded` y `readBytesBounded` reciben un límite de bytes e informan de `missing` o
`too-large` como estado en lugar de lanzar, que es lo que impide que un archivo hostil o
accidentalmente enorme agote la memoria. Se espera que las escrituras sean atómicas, de modo que una caída a
mitad de escritura nunca deje un archivo a medio escribir, y `createExclusive` debe ser atómica
frente a otros procesos porque es la primitiva detrás del bloqueo de escritura por locale. Antes de
escribir un archivo de locale, el archivo de estado de la ejecución o un archivo de salida como un
libro, el SDK llama al `mkdir` opcional con el directorio padre, así que `writeFile` y `writeBytes`
no tienen que crearlo; omite `mkdir` cuando el almacén no tenga directorios. `createExclusive` no
recibe esa llamada y crea él mismo un directorio padre que falte. Los demás miembros obligatorios
son `fileExists` y `deleteFile`, que tiene éxito aunque el archivo ya no exista.

`readDirectory` es opcional, para que una implementación escrita antes de que existiera la
extracción desde código siga compilando. [`extract`](https://verbatra.kreitz-webdev.de/es/docs/sdk/inspect.md#extract) es el único punto de entrada que lo
necesita, e informa de su ausencia como `EXTRACT_FS_UNSUPPORTED`. Lista las entradas inmediatas de
un directorio como `{ name, kind }`, donde `kind` es `"file"`, `"directory"` u `"other"`. Un enlace
simbólico pertenece a `"other"`: el análisis nunca lo sigue, y eso es lo que lo mantiene dentro de
las raíces que recibió.

`realpath` también es opcional. Resuelve todos los enlaces simbólicos de una ruta existente, y las
guardas de salida de [`generateTypes`](https://verbatra.kreitz-webdev.de/es/docs/sdk/run.md#generatetypes), [`exportTmx`](https://verbatra.kreitz-webdev.de/es/docs/sdk/exchange.md#exporttmx),
[`exportWorkbook`](https://verbatra.kreitz-webdev.de/es/docs/sdk/exchange.md#exportworkbook) y [`pseudolocalize`](https://verbatra.kreitz-webdev.de/es/docs/sdk/run.md#pseudolocalize) lo usan para ver
dónde cae de verdad una ruta de salida, así que un enlace no puede llevar un archivo escrito fuera
de `cwd` ni sobre un archivo del que depende el proyecto. Sin él, esas guardas comparan las rutas
tal como están escritas. Un enlace en la ruta de salida cuyo destino no existe no se puede resolver,
así que las guardas dan por hecho que `writeFile` y `writeBytes` reemplazan ese enlace en lugar de escribir a través
de él, como hace la escritura atómica por defecto.

`rename` también es opcional.

<AvailableFrom version="0.12.0" />

Mueve un archivo a un nombre nuevo en el mismo directorio en un solo paso atómico, reemplazando
cualquier archivo que ya esté allí, y rechaza con un error cuyo `code` es `ENOENT` cuando falta el
origen. El bloqueo de escritura lo usa para quitar un bloqueo o una guarda de recuperación que dejó
un proceso que ya no existe, y para liberar su propio bloqueo: el archivo se aparta con un nombre
único `<name>.<pid>.<tag>.<uuid>.stale` y solo se borra si sigue siendo el registro que esperaba el
bloqueo. Cualquier otra cosa, como un bloqueo que otro proceso tomó entretanto, se devuelve a su
sitio con `createExclusive`. Un renombrado que falla con `EPERM`, `EBUSY` o `EACCES`, como informa
Windows de un archivo que otro proceso tiene abierto, cuenta como "no recuperado en este sondeo", y
el bloqueo sigue sondeando hasta su tiempo límite. Cuando el renombrado que libera un bloqueo falla
por cualquier motivo, la liberación recurre a leer el archivo de bloqueo y borrarlo mientras siga
llevando el token de quien lo tiene, y reintenta unas cuantas veces, con un sondeo de separación, un
fallo con `EPERM`, `EBUSY` o `EACCES`. Un registro que debería devolverse a su sitio pero no puede,
porque un tercer proceso tomó la ruta entretanto, se borra, ya que la comprobación del token de
quien lo tenía falla a partir de ese momento. Sin `rename`, el bloqueo lee el archivo y lo
borra si no ha cambiado, lo que deja una ventana más amplia en la que se borra un bloqueo que otro
proceso acaba de tomar.

Apartar un archivo y devolverlo son dos pasos, así que en intercalados poco frecuentes dos procesos
pueden creer durante un momento que tienen el mismo bloqueo. Por eso cada bloqueo registra un token
de propiedad aleatorio, y antes de cada escritura que protege el bloqueo (el archivo del locale, el
archivo de bloqueo, el archivo de procedencia, el glosario) quien lo tiene comprueba que el archivo
de bloqueo sigue llevando su token. Si no es así, la operación se detiene con `LOCK_CONTENDED` y no
escribe nada. Un archivo de bloqueo que falta o lleva otro token al liberar se informa igual cuando
la operación terminó bien, porque el otro proceso puede haber cambiado los mismos archivos desde
entonces; una operación que falló lanza su propio error. La comprobación y la escritura siguen
siendo dos pasos, y nada limita el tiempo entre ellos: quien tiene el bloqueo y se pausa justo
después de la comprobación (una máquina suspendida, un proceso detenido, un depurador, un salto del
reloj o un bucle de eventos bloqueado mucho tiempo) deja de latir, así que cuando su bloqueo tiene
tres intervalos (30 segundos por defecto) otro proceso de la misma máquina lo toma, y quien estaba
pausado todavía puede escribir una vez al continuar. La comprobación del token da por hecho que `createExclusive` crea un archivo que `readFileBounded` lee
de vuelta, así que una implementación propia tiene que cumplirlo.

`touch` y `mtimeMs` también son opcionales.

<AvailableFrom version="0.12.0" />

`touch(path)` pone la fecha de modificación de un archivo existente en el momento actual, y
`mtimeMs(path)` la devuelve en milisegundos, o `undefined` si falta el archivo. Con `touch`, quien
tiene un bloqueo registra en él un intervalo de latido de 10 segundos y refresca la fecha de
modificación del archivo a ese ritmo mientras lo tiene. También la refresca justo después de
crearlo, para que la antigüedad del archivo se mida con el reloj de este proceso y no con el del
servidor de archivos. Con `mtimeMs`, un proceso en espera trata
como abandonado un bloqueo de su propia máquina cuyo proceso sigue en marcha pero cuyo archivo no
se ha refrescado durante tres de los intervalos registrados (30 segundos por defecto). Eso cubre un
registro de bloqueo que sobrevivió a su uso, por ejemplo uno devuelto a su sitio para un proceso de
Studio, `watch` o del servidor MCP de larga duración que ya lo había liberado. Una guarda de
recuperación no registra latido: se quita cuando su proceso ya no existe de forma demostrable y se
ha visto sin cambios en dos sondeos. Sin estos miembros, un bloqueo solo se considera abandonado cuando su
proceso ya no existe de forma demostrable.

Un proceso que se cae mientras tiene un bloqueo apartado puede dejar un archivo `.stale` en
`.verbatra-local/locks/`. Puedes borrar esos archivos a mano sin riesgo, y la siguiente adquisición
de un bloqueo los borra cuando el proceso que los movió ya no existe o, si `touch` está
implementado, cuando superan el umbral de caducidad, contado desde que se apartaron. Un sistema de
archivos sin `touch` los conserva hasta que su proceso ya no existe.

`deps.fs` no alcanza un `deps.adapterRegistry` suministrado por quien llama. Esos
adaptadores los construyó quien llama, así que su acceso a archivos es el que quien llama haya
cableado en ellos. Suministrar ambos significa que quien llama es dueño de ese cableado.

La carga de la configuración también queda fuera del punto de inyección. `loadConfig` y
`loadConfigWithMeta` no reciben `deps`: leen el propio archivo de configuración del disco real, y un
archivo de glosario que nombre la configuración a través de su propia opción `fs`, así que el
`deps.fs` que recibe un flujo posterior no alcanza ninguna de las dos lecturas. Para mantener un
proyecto entero en memoria, pasa el mismo `SdkFs` como `loadConfig({ fs })` y entrega la
configuración como `configOverride` en lugar de como archivo. Los flujos resuelven las rutas contra
su propio `cwd`, así que pásalo también ahí.

El ejemplo mantiene un proyecto entero en memoria solo con los miembros obligatorios. Con el
proveedor `none` la ejecución no necesita clave y deja `greeting` para que lo traduzca una persona.

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