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 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:
- Bloquea un pull request con
verbatra checkoverbatra diff, ya sea directamente (esta página) o a través de la GitHub Action concommand: checkocommand: diff. Ambos son de solo lectura: sin llamada al proveedor, sin clave API, salida1si hay desfase. - Traduce en cada push con
verbatra translate, ya sea directamente (esta página) o a través de la GitHub Action. - Haz una comprobación previa de ambos con
verbatra doctor. Es el paso más barato de todos: valida la configuración, el formato, el proveedor, la variable de la clave y el archivo fuente, nunca llama a un proveedor y nunca lee el valor de una clave. Además sigue dando un informe en un proyecto cuyos archivos de locale todavía no están en su sitio, donde los demás comandos solo pueden fallar por completo.
Los códigos de salida
El código de salida es el contrato sobre el que ramifica tu paso de CI:
| Código | Significado |
|---|---|
0 | éxito: translate o import terminó cada locale completo, check encontró todos los locales sincronizados, diff no encontró cambios pendientes, doctor no encontró ningún problema de configuración del proyecto, export escribió su libro, init preparó el proyecto, watch o studio se detuvo limpiamente, o se imprimió --help o --version |
1 | la 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, doctor encontró al menos una comprobación fallida, o studio falló al apagar su servidor |
2 | no 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 |
130 | watch o studio fue detenido a la fuerza por una segunda interrupción |
Algunos casos límite que conviene conocer:
Disponible a partir de 0.9.0
- 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
1exactamente igual que un locale fallido, porque un locale a medio traducir en disco no es un estado que una canalización deba dejar pasar. Leepartialen el resumen para distinguir los dos casos. - Un archivo de bloqueo corrupto es un error de toda la ejecución tanto en
translatecomo enimport, 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 con2en 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
watchcomostudiosalen con0ante ella. Más allá de eso no des por hecho que se comportan igual: si la propia parada falla,watchsale con2ystudiosale con1. - Una ejecución fallida durante
watchaparece como un registro en el flujo de salida, nunca como un código de salida distinto de cero. exportno tiene modo de fallo por locale: sale con0o2, nunca con1.doctorlee una configuración rota de forma distinta al resto de comandos: una config que la búsqueda no encuentra, o una que no supera la validación, es una comprobación fallida y salida1, porque informar precisamente de eso es el trabajo del comando. Sale con2solo cuando no puede ejecutarse en absoluto, por ejemplo con una ruta--configexplícita que no existe.- 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 diffUsa 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
Siete comandos aceptan --json para una salida legible por máquina en stdout: translate, watch, check, diff, export, import y doctor. 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 de un comando ya resuelto van 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. Dos clases de fallo no siguen esa gramática, porque ocurren antes de que exista una ejecución que pueda llevar un código: un error al analizar los argumentos imprime el propio error: unknown option '--nope' de commander, sin prefijo y sin código, e init rechaza un proveedor ausente con verbatra: --provider is required (...), con prefijo pero sin código. Ambos salen con 2. Comprueba primero el código de salida y trata el código de error como opcional, o un analizador escrito solo contra la gramática estructurada no encajará con ninguno de los dos. 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: the workbook for xlsx, the output directory for csv and tsv
locales: { locale: string; rows: number }[];
}verbatra doctor --json lleva el informe de la configuración del proyecto. El ok de nivel superior es true exactamente cuando el comando sale con 0:
interface DoctorResult {
ok: boolean; // true exactly when the command exits 0
checks: DoctorCheck[]; // one per check, always in the same order
}
interface DoctorCheck {
id: "config" | "format-adapter" | "provider" | "api-key" | "source-file";
title: string; // short human-readable name, stable across runs
status: "pass" | "fail" | "skipped"; // only "fail" makes ok false
detail: string; // why it reached that verdict; never an API key value
}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 checkLa GitHub Action ejecuta la misma puerta sin ningún paso de instalación: conserva el checkout y sustituye el resto por la acción, con command: check (o command: diff):
- uses: actions/checkout@<commit-sha>
- uses: verbatra/action@<commit-sha> # fija la línea v1 mantenida activamente
with:
version: 0.9.3
command: checkPara 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 estos jobs tú mismo, la GitHub Action envuelve translate, check y diff por igual, y añade anotaciones y un resumen del job.
Instalaciones congeladas y claves
- Instala desde el lockfile.
pnpm install --frozen-lockfile(onpm ci) fija la versión exacta de@verbatra/clique 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,GOOGLE_TRANSLATE_API_KEY). Guarda la clave en el almacén de secretos de tu CI y mapéala enenv. Los mensajes de error nombran la variable pero nunca contienen el valor de una clave. - Las puertas de solo lectura no necesitan clave.
check,diffyexportnunca llaman a un proveedor, así que deja los secretos completamente fuera de esos jobs.doctortampoco llama a ningún proveedor y nunca lee el valor de una clave, pero informa de una variable de clave sin definir como comprobación fallida, así que mapea la clave en cualquier job donde quieras que esa comprobación pase.
translate, watch, studio y doctor 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:.
Estimar el coste
Dimensiona una ejecución de traducción antes de gastar: cuenta las claves con una ejecución en seco, conviértelas en peticiones y tokens, y ponles precio con las tarifas de tu proveedor.
GitHub Action
Ejecuta verbatra en GitHub Actions con la acción compuesta: entradas, cableado de secretos, anotaciones y el resumen del job.