---
title: "Recetas del SDK"
description: "Patrones completos y ejecutables para manejar verbatra desde tus propios scripts y jobs de CI, cada uno sobre los puntos de entrada públicos de @verbatra/sdk."
type: "how-to"
---
# Recetas del SDK

Patrones completos y ejecutables para manejar verbatra desde tus propios scripts y jobs de CI, cada uno sobre los puntos de entrada públicos de @verbatra/sdk.

El [inicio rápido del SDK](https://verbatra.kreitz-webdev.de/es/docs/sdk-quickstart.md) cubre la primera ejecución. Esta página sigue desde
ahí con recetas completas para copiar en un script; la [referencia del SDK](https://verbatra.kreitz-webdev.de/es/docs/sdk.md) cataloga cada
punto de entrada. El SDK no carga archivos `.env`, así que ejecuta una receta que llama a un proveedor
con `node --env-file=.env`. Para recorrer un proveedor con límite de tasa un idioma cada vez, pasa
`locales: ["de"]` a `translate`.

## Estima una ejecución antes de gastar [#estima-una-ejecución-antes-de-gastar]

<AvailableFrom version="0.11.0" />

`estimate: true` implica `dryRun`. La cifra aparece en `RunSummary.estimate`: ramifica según `unit`
para la dimensión facturada y según `pricing` para saber si un bloque
[`rates`](https://verbatra.kreitz-webdev.de/es/docs/config-file.md#rates) le puso precio.

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

Acota el plan, no la factura; [Estimar el coste](https://verbatra.kreitz-webdev.de/es/docs/estimating-cost.md) explica el método, y
[RunSummary](https://verbatra.kreitz-webdev.de/es/docs/sdk/run-summary.md) enumera lo que deja fuera cada `EstimateCaveatCode` de
`caveats`.

## Comprueba en CI sin escribir [#comprueba-en-ci-sin-escribir]

`check` lee y compara sin llamar a un proveedor ni tocar ningún archivo, y su indicador `inSync` es
true exactamente cuando no hay nada faltante ni obsoleto.

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

¿Necesitas los nombres de las claves en lugar de contadores? Cambia a `diff`, que devuelve listas de
claves `missing`, `changed` y `orphaned` por locale con la misma puerta `hasPendingChanges`. ¿Ejecutas
la CLI en CI en su lugar? Consulta [CI y códigos de salida](https://verbatra.kreitz-webdev.de/es/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;
}
```

## Prueba tu integración sin clave [#prueba-tu-integración-sin-clave]

Un proveedor falso pasado como `createProvider` ejecuta todo el flujo, archivos y control de
integridad incluidos, sin clave, sin llamada de red y con una salida determinista: justo lo que
necesita una prueba unitaria de tus propias herramientas.

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

La ejecución escribe archivos reales, así que prueba contra una copia temporal del proyecto. Un
proveedor real se conecta de la misma forma; consulta [Proveedores personalizados](https://verbatra.kreitz-webdev.de/es/docs/sdk/extend.md#proveedores-propios).

## Vigila en un proceso de larga duración [#vigila-en-un-proceso-de-larga-duración]

`watch` dispara una ejecución inmediatamente al arrancar, y luego una por cada cambio del origen,
una vez pasado el anti-rebote, informando de cada una a través de `onRun`. Para un apagado limpio,
haz `await controller.stop()`: cierra el watcher y espera a que termine la ejecución en curso antes
de que salga tu proceso.

```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 fallo de ejecución después del arranque nunca lanza; llega como `{ status: "failed" }` y la
vigilancia continúa.

## Cancela una ejecución [#cancela-una-ejecución]

<AvailableFrom version="0.12.0" />

Pasa un `AbortSignal` como `signal`. `translate` entonces se resuelve en lugar de rechazar: escribe
lo que ya llegó, deja el resto pendiente y fija `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;
}
```

Un locale que la cancelación impidió empezar lleva el [código de error `RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/es/docs/error-codes.md#run_cancelled), y uno que detuvo a media ejecución, el [aviso `RUN_CANCELLED`](https://verbatra.kreitz-webdev.de/es/docs/error-codes.md#notice-run_cancelled). Una cancelación que llega cuando ya terminó cada locale deja `cancelled` ausente, y un error de toda la ejecución al arrancar sigue rechazando.

## Extrae claves de tu código fuente [#extrae-claves-de-tu-código-fuente]

<AvailableFrom version="0.11.0" />

`extract` es el único punto de entrada que lee código de aplicación en lugar de un archivo de
locale. Solo escribe el catálogo de origen, y solo claves realmente nuevas: una clave ya existente
conserva su valor, y una clave que ninguna llamada menciona se deja intacta. Todo lo que el análisis
no puede resolver vuelve como datos, así que un dry run es una buena puerta antes de la ejecución
que escribe.

```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` tiene que nombrar el framework y las raíces de código, o la llamada lanza
`EXTRACT_NOT_CONFIGURED`. [`extract`](https://verbatra.kreitz-webdev.de/es/docs/sdk/inspect.md#extract) enumera cada uno de los demás
errores que lanza.

## Genera un catálogo tipado [#genera-un-catálogo-tipado]

<AvailableFrom version="0.11.0" />

`generateTypes` declara el catálogo de origen como TypeScript: una unión de todas las claves, más
los argumentos que interpola cada mensaje. Lee un archivo y escribe un archivo, y dos ejecuciones
sobre un catálogo sin cambios escriben bytes idénticos, que es lo que hace del modo `check` una
puerta de CI utilizable.

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

Pasa `configPath` para que el archivo de configuración que cargó la ejecución se rechace como ruta
de salida. Una ejecución con `check` no escribe nada y solo informa de `stale`.
[`verbatra types`](https://verbatra.kreitz-webdev.de/es/docs/cli/types.md) enumera todo lo que rechaza una ejecución que escribe.

## Construye un pseudolocale [#construye-un-pseudolocale]

<AvailableFrom version="0.11.0" />

`pseudolocalize` construye un locale falso solo a partir de las cadenas de origen, para que los
recortes de maquetación y las cadenas que nunca se extrajeron salgan a la luz antes de que exista
ninguna clave de API. No llama a ningún proveedor, y su salida
cae fuera de `files.pattern`, así que ninguna ejecución gasta en él ni lo informa como desfasado.

```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` es `en-XA` por defecto, o `ar-XB` con `mode: "bidi"`, y `out` es `.verbatra-local/pseudo`. La transformación es
determinista, así que una segunda ejecución sobre un origen sin cambios no reescribe nada e informa
de `written` como false.

## Edita y retraduce una sola clave [#edita-y-retraduce-una-sola-clave]

Estos son los puntos de integración que usa Verbatra Studio; úsalos para construir tu propio flujo de
revisión. Lee los valores actuales con `keyValue`, guarda una corrección humana con `editEntry` o vuelve
a ejecutar el proveedor para una clave con `retranslateEntry`. Ambos escritores pasan el valor candidato
por las mismas comprobaciones de integridad que una ejecución completa y devuelven un
resultado de dos ramas en lugar de lanzar ante un valor rechazado.

```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/es/docs/sdk/review.md#retranslateentry) enumera lo que lanza cada una.

### Valores escritos por personas [#valores-escritos-por-personas]

<AvailableFrom version="0.12.0" />

`retranslateEntry` rechaza con `KEY_PROTECTED` un valor que escribió una persona, incluido el que
`editEntry` acaba de guardar, salvo que pases `includeHuman: true` o la config fije
`humanEdits: "overwrite"`. Por eso la receta lo pasa. Una clave fijada la rechaza siempre; consulta [Proteger traducciones humanas](https://verbatra.kreitz-webdev.de/es/docs/protecting-human-translations.md).

## Recorre la cola de revisión [#recorre-la-cola-de-revisión]

<AvailableFrom version="0.12.0" />

`reviewQueue` enumera cada valor escrito por máquina que nadie ha aprobado todavía, con los motivos
por los que la última ejecución lo marcó. Este script aprueba los valores sin marcar y rechaza el
resto, lo que los elimina para que el siguiente `translate` los vuelva a rellenar. `expectedValue` hace
que una llamada lance `REVIEW_VALUE_CHANGED` si el valor cambió entretanto. Sin registro de la última ejecución en esta
máquina, como en un clon recién hecho o en un runner de CI, `reasons` también está vacío, así que el
script se detiene si falta `lastRunAt`.

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

[Revisa y retraduce](https://verbatra.kreitz-webdev.de/es/docs/sdk/review.md#approveentry) tiene las variantes por lotes y cada código que lanzan.

## Lee todos los valores de golpe [#lee-todos-los-valores-de-golpe]

<AvailableFrom version="0.10.0" />

`localeValues` es la contraparte en bloque de `keyValue`: una pasada sobre los archivos que ya están
en disco, que devuelve el texto de origen y de destino actual de cada clave por locale.

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

Solo lectura: no llama a ningún proveedor, no escribe nada y no toca ni el archivo de bloqueo ni la
caché. Cubre todos los locales de destino configurados; pasa `locales` para limitarlo a algunos, y un locale fuera de `targetLocales` lanza `UNKNOWN_LOCALE`.

## El ciclo de ida y vuelta del libro [#el-ciclo-de-ida-y-vuelta-del-libro]

Exporta las cadenas que necesitan traducción a un libro de Excel, entrégaselo a un traductor y luego
importa el archivo rellenado de vuelta. `importWorkbook` ejecuta las mismas comprobaciones de
desfase e integridad que `translate` y devuelve la misma forma `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`);
```

Pasa `dryRun: true` a `importWorkbook` para validar un archivo devuelto sin escribir nada. Consulta
[Traducción humana](https://verbatra.kreitz-webdev.de/es/docs/manual-translation.md) para saber qué pueden editar los traductores y cómo se
validan las filas.

## El ciclo de ida y vuelta de TMX [#el-ciclo-de-ida-y-vuelta-de-tmx]

<AvailableFrom version="0.11.0" />

`importTmx` y `exportTmx` mueven la [memoria de traducción](https://verbatra.kreitz-webdev.de/es/docs/the-cache.md) hacia y desde TMX, el
formato de intercambio que producen y leen las plataformas de traducción más extendidas. A diferencia
del par del libro, mueve la memoria entera como un único archivo multilingüe entre herramientas.

```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` informa de los datos dañados en lugar de lanzar sobre ellos: un archivo rechazado lanza
`SOURCE_INVALID`, y `tmxErrorLocation` lee el sitio a partir de ese error sin hacer un cast sobre
él ni sobre su `cause`. Solo se exportan las entradas bajo la huella de configuración actual, que
es el mismo conjunto que reutilizaría una ejecución.

## Una configuración sin archivo [#una-configuración-sin-archivo]

`loadConfig` acepta un `configOverride` en memoria, validado exactamente igual que un archivo cargado,
así que puedes manejar verbatra enteramente desde código sin ningún archivo de configuración en disco.

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