Saltar al contenido
Saltar al contenido

Docs / Guías

Página traducida automáticamente

Esta página se tradujo automáticamente, así que puede contener errores o sonar un poco rara. La versión en inglés es la fuente de la verdad. Leer el original en inglés.

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 cubre la primera ejecución. Esta página sigue desde ahí con recetas completas para copiar en un script; la referencia del SDK 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

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 le puso precio.

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 explica el método, y RunSummary enumera lo que deja fuera cada EstimateCaveatCode de caveats.

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.

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.

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

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.

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.

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.

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

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.

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, y uno que detuvo a media ejecución, el aviso 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

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.

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 enumera cada uno de los demás errores que lanza.

Genera un catálogo tipado

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.

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 enumera todo lo que rechaza una ejecución que escribe.

Construye un pseudolocale

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.

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

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.

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 enumera lo que lanza cada una.

Valores escritos por personas

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.

Recorre la cola de revisión

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.

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 tiene las variantes por lotes y cada código que lanzan.

Lee todos los valores de golpe

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.

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

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.

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 para saber qué pueden editar los traductores y cómo se validan las filas.

El ciclo de ida y vuelta de TMX

importTmx y exportTmx mueven la memoria de traducción 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.

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

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.

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 });
Editar en GitHub

En esta página