CI y códigos de salida

Bloquea una canalización según el estado de las traducciones con check o diff, lee el contrato de códigos de salida y consume la salida JSON.

Página traducida automáticamente

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

Esta guía muestra cómo hacer que CI falle cuando las traducciones se desincronizan, y cómo leer lo que verbatra informa cuando eso pasa. Las herramientas son el contrato de códigos de salida que sigue cada comando y la salida --json que tus scripts pueden analizar.

verbatra es una dependencia de desarrollo, así que los comandos que ejecutas en local se ejecutan igual en CI. El reparto habitual:

Los códigos de salida

El código de salida es el contrato sobre el que ramifica tu paso de CI:

CódigoSignificado
0éxito: translate o import terminó cada locale completo, check encontró todos los locales sincronizados, diff no encontró cambios pendientes, export escribió su libro, init preparó el proyecto, watch o studio se detuvo limpiamente, o se imprimió --help o --version
1la ejecución llegó al final, pero el resultado no está limpio: translate o import terminó con al menos un locale fallido o parcial, check encontró un locale desincronizado, diff encontró una clave ausente o cambiada, o studio falló al apagar su servidor
2no se pudo ejecutar: un error de toda la ejecución (configuración mala, origen ilegible, archivo de bloqueo corrupto), un error de uso (un valor --locales vacío o desconocido, un --debounce o --port inválido), init sin un proveedor que pueda resolver o incapaz de componer una configuración válida, un arranque o una parada fallidos de watch, o studio incapaz de cargar la configuración, de importar @verbatra/studio o de arrancar su servidor
130watch o studio fue detenido a la fuerza por una segunda interrupción

Algunos casos límite que conviene conocer:

  • Un locale parcial cuenta como fallo. Significa que el archivo se escribió pero todavía faltan algunas claves, normalmente porque un sublote del proveedor falló o la puerta de integridad rechazó una traducción. Sale con 1 exactamente igual que un locale fallido, porque un locale a medio traducir en disco no es un estado que una canalización deba dejar pasar. Lee partial en el resumen para distinguir los dos casos.
  • Un archivo de bloqueo corrupto es un error de toda la ejecución tanto en translate como en import, nunca un único locale fallido. Es un solo archivo compartido, así que un archivo de bloqueo que se corrompe con la ejecución en marcha la detiene con 2 en vez de seguir con los locales restantes. Lo que se escribió antes del corte permanece en disco: repara el archivo de bloqueo y vuelve a ejecutar el comando.
  • Una sola interrupción es una parada limpia, y tanto watch como studio salen con 0 ante ella. Más allá de eso no des por hecho que se comportan igual: si la propia parada falla, watch sale con 2 y studio sale con 1.
  • Una ejecución fallida durante watch aparece como un registro en el flujo de salida, nunca como un código de salida distinto de cero.
  • export no tiene modo de fallo por locale: sale con 0 o 2, nunca con 1.
  • Hay un modo de fallo que queda fuera del contrato: un fallo de análisis que no es un error de uso se vuelve a lanzar, y el binario no lo captura, así que se aplica el comportamiento por defecto de Node ante un rechazo no gestionado en lugar de cualquiera de los cuatro códigos.

check o diff: elegir la puerta

check y diff ejecutan el mismo cálculo de solo lectura sobre tu origen, tus archivos de destino y el archivo de bloqueo. La diferencia está en lo que informan:

# counts per locale: exit 1 if any locale is missing or stale
verbatra check

# key lists per locale: exit 1 if any locale has keys to add or re-translate
verbatra diff

Usa check cuando el código de salida es todo lo que necesitas. Usa diff cuando quieras las claves exactas detrás del desfase, por ejemplo, para publicarlas en un comentario del pull request. Las claves huérfanas (en un archivo de destino pero desaparecidas del origen) aparecen en la salida de diff pero nunca provocan por sí solas el código de salida 1.

Ambos aceptan --locales de,fr para bloquear un subconjunto. Pasar --locales sin ningún locale válido es un error de uso y sale con 2, así que una errata nunca puede poner la puerta en verde. translate, watch y export aceptan el mismo flag, que es como traduces un locale cada vez contra un proveedor con límite de tasa.

Salida JSON

Seis comandos aceptan --json para una salida legible por máquina en stdout: translate, watch, check, diff, export e import. Cada registro ocupa una línea, y cada registro va dentro del mismo envoltorio, así que ramificas sobre un solo campo y nunca tienes que adivinar de qué comando son los datos que tienes delante:

type Envelope<TResult> =
  | { ok: true; version: 1; command: string; result: TResult }
  | { ok: false; version: 1; command: string | null; code: string; message: string };

version es la versión de esta forma de envoltorio, no la versión del paquete. Es un entero, así que lo comparas con === en lugar de analizar un rango, y solo cambia cuando un campo existente cambia de significado o desaparece. Pueden aparecer campos nuevos sin incrementarla, así que ignora los que no reconozcas.

Una ejecución que falla por completo escribe exactamente un registro ok: false en stdout y sale con 2. Su code es el mismo código de error estable que lleva la línea de stderr, y es sobre lo que ramificas:

{ "ok": false, "version": 1, "command": "translate", "code": "CONFIG_INVALID", "message": "..." }

command es null solo cuando el fallo ocurrió antes de resolver un subcomando.

Los errores van además a stderr como una línea estructurada (verbatra: error [CODE] message) en ambos modos, sin cambios, así que un script que lee el código de salida y stderr no necesita ninguna actualización. Sin --json, una ejecución fallida sigue sin escribir absolutamente nada en stdout. Los registros de progreso y de espera de bloqueo van siempre a stderr, así que stdout no lleva más que envoltorios.

El resto de esta sección describe el result que cada comando pone dentro de un envoltorio de éxito.

verbatra translate --json y verbatra import --json llevan un RunSummary:

interface RunSummary {
  dryRun: boolean;          // whether this was a dry run (no provider calls, no writes)
  locales: LocaleSummary[]; // one entry per target locale, in config order
  succeeded: string[];      // locales whose run succeeded
  partial: string[];        // locales written with keys still missing; these exit 1 too
  failed: string[];         // locales whose run failed
  usage?: UsageSummary;     // summed input/output tokens; absent when no call reported usage
  budget?: RunBudget;       // the token-budget outcome; present only when maxTokens is configured
}

Cada LocaleSummary lleva las listas de claves por locale (traducidas, sin cambios, huérfanas, retenidas, marcadas para revisión y más); consulta la referencia del SDK para la anatomía completa.

verbatra watch --json imprime un envoltorio por ejecución como NDJSON (un objeto JSON por línea), con command: "watch". Una ejecución correcta es un registro ok: true que lleva el RunSummary de esa ejecución; una ejecución fallida es un registro ok: false que lleva su código y su mensaje. Una ejecución fallida es solo un registro en el flujo: ni detiene el watcher ni cambia el código de salida.

verbatra check --json lleva un documento de estado. El inSync de nivel superior es true exactamente cuando el comando sale con 0:

interface CheckSummary {
  inSync: boolean;             // true exactly when the command exits 0
  locales: LocaleCheckSummary[];
}

interface LocaleCheckSummary {
  locale: string;
  missing: number;             // in source, absent from target
  stale: number;               // source changed since last translated
  upToDate: number;            // target matches the recorded baseline
  inSync: boolean;             // missing === 0 && stale === 0
}

verbatra diff --json lleva listas de claves en lugar de contadores. El hasPendingChanges de nivel superior es true exactamente cuando el comando sale con 1:

interface DiffSummary {
  hasPendingChanges: boolean;  // true exactly when the command exits 1
  locales: LocaleDiff[];
}

interface LocaleDiff {
  locale: string;
  missing: string[];           // in source, absent from target: would be added
  changed: string[];           // source changed since last translated: would be re-translated
  orphaned: string[];          // in target, absent from source: reported only
  hasPendingChanges: boolean;  // missing.length > 0 || changed.length > 0
}

verbatra export --json lleva la ruta donde quedó el libro y el número de filas por locale:

{
  path: string;                // absolute path of the written workbook
  locales: { locale: string; rows: number }[];
}

Un job de GitHub Actions con la CLI

Una puerta contra el desfase en los pull requests, ejecutando la CLI directamente. check nunca llama a un proveedor, así que este job no necesita ninguna clave API:

name: i18n
on: pull_request

permissions:
  contents: read

jobs:
  check-translations:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<commit-sha>
      - uses: pnpm/action-setup@<commit-sha>
      - uses: actions/setup-node@<commit-sha>
        with:
          node-version: 22
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec verbatra check

Para traducir en CI en su lugar, cambia el último paso por translate y pasa la clave del proveedor desde tu almacén de secretos como la variable de entorno que tu proveedor espera (consulta Proveedores):

      - run: pnpm exec verbatra translate --json
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

Si prefieres no escribir este job tú mismo, la GitHub Action envuelve la variante de translate con anotaciones y un resumen del job.

Instalaciones congeladas y claves

  • Instala desde el lockfile. pnpm install --frozen-lockfile (o npm ci) fija la versión exacta de @verbatra/cli que registra tu lockfile, así que una ejecución de CI es reproducible y no puede traer en silencio una versión más nueva. La CLI requiere Node >=22.14.0.
  • Las claves son variables de entorno, nunca flags. La CLI no acepta ningún argumento de clave y no lee ninguna clave de la configuración; los proveedores leen solo su variable de entorno (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, DEEPL_API_KEY). Guarda la clave en el almacén de secretos de tu CI y mapéala en env. Los mensajes de error nombran la variable pero nunca contienen el valor de una clave.
  • Las puertas de solo lectura no necesitan clave. check, diff y export nunca llaman a un proveedor, así que deja los secretos completamente fuera de esos jobs.

translate, watch y studio también cargan .env.local y luego .env desde el directorio de trabajo antes de ejecutarse, y las variables de entorno reales siempre ganan; check, diff, export e import no cargan archivos .env, así que en CI normalmente dependerás solo de env:.

Edit on GitHub