CI et codes de sortie
Conditionne un pipeline à l'état des traductions avec check ou diff, lis le contrat des codes de sortie et consomme la sortie JSON.
Page traduite automatiquement
Ce guide montre comment faire échouer la CI quand les traductions se désynchronisent, et comment lire ce que verbatra rapporte quand ça arrive. Les outils : le contrat des codes de sortie que chaque commande suit et la sortie --json que tes scripts peuvent parser.
verbatra est une dépendance de développement, donc les commandes que tu lances en local tournent pareil en CI. Le découpage habituel :
- Contrôle une pull request avec
verbatra checkouverbatra diff. Les deux sont en lecture seule : aucun appel de fournisseur, aucune clé d'API requise, sortie1en cas de dérive. - Traduis lors d'un push avec
verbatra translate, soit directement (cette page), soit via la GitHub Action.
Les codes de sortie
Le code de sortie est le contrat sur lequel ton étape CI se branche :
| Code | Signification |
|---|---|
0 | succès : translate ou import a terminé chaque locale complète, check a trouvé chaque locale synchronisée, diff n'a trouvé aucun changement en attente, export a écrit son classeur, init a échafaudé le projet, watch ou studio s'est arrêté proprement, ou --help ou --version a été affiché |
1 | l'exécution est allée au bout, mais le résultat n'est pas propre : translate ou import a terminé avec au moins une locale en échec ou partielle, check a trouvé une locale désynchronisée, diff a trouvé une clé absente ou modifiée, ou studio a échoué en arrêtant son serveur |
2 | impossible de s'exécuter : une erreur globale (config invalide, source illisible, fichier de verrouillage corrompu), une erreur d'utilisation (une valeur --locales vide ou inconnue, un --debounce ou --port invalide), init sans fournisseur résoluble ou incapable de composer une config valide, un démarrage ou un arrêt de watch en échec, ou studio incapable de charger la config, d'importer @verbatra/studio ou de démarrer son serveur |
130 | watch ou studio a été arrêté de force par une seconde interruption |
Quelques cas limites à connaître :
- Une locale partielle compte comme un échec. Cela veut dire que le fichier a été écrit mais qu'il manque encore des clés, en général parce qu'un sous-lot du fournisseur a échoué ou que la barrière d'intégrité a refusé une traduction. Elle sort avec
1exactement comme une locale échouée, parce qu'une locale à moitié traduite sur le disque n'est pas un état qu'un pipeline devrait laisser passer. Lispartialsur le résumé pour distinguer les deux. - Un fichier de verrouillage corrompu est une erreur globale pour
translatecomme pourimport, jamais une seule locale en échec. C'est un fichier unique et partagé : s'il se corrompt en cours d'exécution, celle-ci s'arrête avec2au lieu de poursuivre avec les locales restantes. Ce qui a été écrit avant l'interruption reste sur le disque : répare le fichier de verrouillage puis relance la commande. - Une interruption unique est un arrêt propre, et
watchcommestudiosortent avec0dans ce cas. Au-delà, ne suppose pas qu'ils se comportent pareil : si l'arrêt lui-même échoue,watchsort avec2etstudiosort avec1. - Une exécution échouée pendant
watchapparaît comme un enregistrement sur le flux de sortie, jamais comme un code de sortie non nul. exportn'a pas de mode d'échec par locale : il sort avec0ou2, jamais1.- Un mode d'échec sort du contrat : une erreur d'analyse qui n'est pas une erreur d'utilisation est relancée, et le binaire ne l'attrape pas, donc le comportement par défaut de Node face à un rejet non géré s'applique au lieu de l'un des quatre codes.
check ou diff : choisir le contrôle
check et diff exécutent le même calcul en lecture seule sur ta source, tes fichiers cibles et le fichier de verrouillage. La différence est ce qu'ils rapportent :
# 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 diffUtilise check quand le code de sortie te suffit. Utilise diff quand tu veux les clés exactes derrière la dérive, par exemple pour les poster dans un commentaire de pull request. Les clés orphelines (dans un fichier cible mais disparues de la source) apparaissent dans la sortie de diff mais ne déclenchent jamais le code de sortie 1 à elles seules.
Les deux acceptent --locales de,fr pour contrôler un sous-ensemble. Passer --locales sans aucune locale valide est une erreur d'utilisation et sort avec 2, donc une coquille ne peut jamais mettre le contrôle au vert. translate, watch et export acceptent la même option, et c'est ainsi que tu traduis une locale à la fois face à un fournisseur à débit limité.
Sortie JSON
Six commandes acceptent --json pour une sortie lisible par machine sur stdout : translate, watch, check, diff, export et import. Chaque enregistrement tient sur une ligne, et chaque enregistrement est entouré de la même enveloppe : tu te branches sur un seul champ et tu n'as jamais à deviner de quelle commande viennent les données que tu tiens :
type Envelope<TResult> =
| { ok: true; version: 1; command: string; result: TResult }
| { ok: false; version: 1; command: string | null; code: string; message: string };version est la version de cette forme d'enveloppe, pas la version du paquet. C'est un entier, tu le compares donc avec === plutôt que d'analyser une plage, et il ne change que lorsqu'un champ existant change de sens ou disparaît. De nouveaux champs peuvent apparaître sans incrément, ignore donc ceux que tu ne reconnais pas.
Une exécution qui échoue dans son ensemble écrit exactement un enregistrement ok: false sur stdout et sort avec 2. Son code est le même code d'erreur stable que porte la ligne stderr, c'est donc sur lui que tu te branches :
{ "ok": false, "version": 1, "command": "translate", "code": "CONFIG_INVALID", "message": "..." }command vaut null uniquement quand l'échec s'est produit avant qu'une sous-commande soit résolue.
Les erreurs vont en plus sur stderr sous forme d'une ligne structurée (verbatra: error [CODE] message) dans les deux modes, inchangée, donc un script qui lit le code de sortie et stderr n'a rien à changer. Sans --json, une exécution échouée n'écrit toujours strictement rien sur stdout. Les enregistrements de progression et d'attente de verrouillage vont toujours sur stderr, donc stdout ne porte que des enveloppes.
Le reste de cette section décrit le result que chaque commande place dans une enveloppe de succès.
verbatra translate --json et verbatra import --json portent 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
}Chaque LocaleSummary porte les listes de clés par locale (traduites, inchangées, orphelines, retenues, signalées pour révision, et plus) ; voir la référence SDK pour l'anatomie complète.
verbatra watch --json affiche une enveloppe par exécution en NDJSON (un objet JSON par ligne), avec command: "watch". Une exécution réussie est un enregistrement ok: true portant le RunSummary de cette exécution ; une exécution échouée est un enregistrement ok: false portant son code et son message. Une exécution échouée n'est qu'un enregistrement sur le flux : elle n'arrête pas la surveillance et ne change pas le code de sortie.
verbatra check --json porte un document d'état. Le inSync de premier niveau est vrai exactement quand la commande sort avec 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 porte des listes de clés au lieu de comptes. Le hasPendingChanges de premier niveau est vrai exactement quand la commande sort avec 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 porte le chemin où le classeur a été écrit et le nombre de lignes par locale :
{
path: string; // absolute path of the written workbook
locales: { locale: string; rows: number }[];
}Un job GitHub Actions avec la CLI
Un contrôle de dérive sur les pull requests, en lançant la CLI directement. check n'appelle jamais de fournisseur, donc ce job n'a besoin d'aucune clé d'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 checkPour traduire en CI à la place, remplace la dernière étape par translate et passe la clé du fournisseur depuis ton coffre de secrets sous la variable d'environnement que ton fournisseur attend (voir Fournisseurs) :
- run: pnpm exec verbatra translate --json
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Si tu préfères ne pas écrire ce job toi-même, la GitHub Action enveloppe la variante translate avec des annotations et un résumé de job.
Installations gelées et clés
- Installe depuis le lockfile.
pnpm install --frozen-lockfile(ounpm ci) épingle la version exacte de@verbatra/clienregistrée dans ton lockfile, donc une exécution CI est reproductible et ne peut pas tirer une version plus récente en silence. La CLI exige Node>=22.14.0. - Les clés sont des variables d'environnement, jamais des options. La CLI ne prend aucun argument de clé et ne lit aucune clé depuis la config ; les fournisseurs lisent uniquement leur variable d'environnement (
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEY,DEEPL_API_KEY). Stocke la clé dans le coffre de secrets de ta CI et mappe-la dansenv. Les messages d'erreur nomment la variable mais ne contiennent jamais de valeur de clé. - Les contrôles en lecture seule n'ont pas besoin de clé.
check,diffetexportn'appellent jamais de fournisseur, donc garde les secrets entièrement hors de ces jobs.
translate, watch et studio chargent aussi .env.local puis .env depuis le répertoire de travail avant de s'exécuter, les vraies variables d'environnement l'emportant toujours ; check, diff, export et import ne chargent aucun fichier .env, donc en CI, tu t'appuieras normalement sur env: seul.
Estimer le coût
Dimensionne une exécution de traduction avant de dépenser : compte les clés avec une exécution à blanc, convertis-les en requêtes et en tokens, puis chiffre-les avec les tarifs de ton fournisseur.
GitHub Action
Lance verbatra translate dans GitHub Actions avec l'action composite : entrées, câblage des secrets, annotations et résumé de job.