Vous avez créé un groupe de tâches. Quelque part, un utilisateur regarde un spinner, et « traduction vers 14 langues… » est techniquement vrai, mais totalement inutile — rien ne bouge. Ce que vous voulez, c’est voir le compteur grimper sous ses yeux : 3 prêtes, puis 4, puis une langue en échec, puis terminé.
Interroger le groupe de tâches vous y amène, mais c’est verbeux, et chaque requête vous renvoie un nouvel instantané qu’il faut comparer au précédent pour comprendre ce qui a vraiment changé. Le WebSocket renverse cette logique. Une seule connexion, puis le serveur pousse un événement chaque fois qu’une langue est résolue — et chaque message contient l’état complet du groupe, donc vous affichez l’instantané, sans jamais réconcilier de delta. Une trame perdue, une reconnexion, un onglet relancé : le message suivant vous redonne toute la vérité.
GET /jobs/localization/groups/:groupId/wsVous débutez avec la localisation asynchrone ? Commencez par la Vue d'ensemble. Le groupId ici est celui que vous avez reçu lorsque vous avez créé les tâches.
Sur cette page
- Types de messages
- Charges utiles des messages
- L’intégrer à votre interface
- Conservez votre clé API côté serveur
Types de messages#
Quatre types de messages transitent par le socket. Chacun vous indique ce qui vient de se produire tout en vous fournissant l’état actuel de l’ensemble du groupe.
| Type | Quand | Champs clés |
|---|---|---|
snapshot | À la connexion initiale | État complet du groupe |
job.completed | Une langue se termine avec succès | jobId, locale, plus l’état complet du groupe |
job.failed | Une langue échoue | jobId, locale, error, plus l’état complet du groupe |
group.completed | Toutes les tâches sont résolues | groupId, status, plus l’état complet du groupe. Le serveur ferme la connexion après ce message. |
Chaque message contient un objet snapshot avec l’état actuel du groupe : totalJobs, completedJobs, completedWithWarningsJobs, failedJobs, ainsi qu’une map jobs indexée par ID de tâche, chacune avec son locale et son status. Ces compteurs sont les mêmes que ceux renvoyés par le point de terminaison du groupe de tâches — un instantané venant du socket et un poll du point de terminaison REST s’accordent donc sur l’avancement du groupe.
affichez l’instantané, sans jamais réconcilier
Vous n’avez jamais besoin de suivre les événements déjà vus, de rejouer les messages manqués ni de fusionner une mise à jour partielle dans l’état local. Lisez snapshot à chaque message et affichez votre interface à partir de là. Une reconnexion renvoie d’abord snapshot, de sorte qu’un client qui vient d’arriver et un client à l’écoute depuis le début convergent vers le même état.
Charges utiles des messages#
Voici les trames exactes envoyées par le serveur. Les identifiants ont leur format réel (ljg_ pour le groupe, ljb_ pour chaque tâche) ; le snapshot est abrégé avec "..." uniquement lorsqu’il répète une structure déjà montrée.
À la connexion, le serveur envoie l’état actuel :
{
"type": "snapshot",
"snapshot": {
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"totalJobs": 3,
"completedJobs": 1,
"completedWithWarningsJobs": 0,
"failedJobs": 0,
"jobs": {
"ljb_A1b2C3d4E5f6G7h8": { "locale": "de", "status": "completed" },
"ljb_B2c3D4e5F6g7H8i9": { "locale": "fr", "status": "processing" },
"ljb_C3d4E5f6G7h8I9j0": { "locale": "ja", "status": "queued" }
}
}
}À mesure que chaque langue se termine, l’événement précise la langue concernée et inclut l’instantané mis à jour :
{
"type": "job.completed",
"jobId": "ljb_B2c3D4e5F6g7H8i9",
"locale": "fr",
"snapshot": {
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"totalJobs": 3,
"completedJobs": 2,
"completedWithWarningsJobs": 0,
"failedJobs": 0,
"jobs": {
"ljb_A1b2C3d4E5f6G7h8": { "locale": "de", "status": "completed" },
"ljb_B2c3D4e5F6g7H8i9": { "locale": "fr", "status": "completed" },
"ljb_C3d4E5f6G7h8I9j0": { "locale": "ja", "status": "processing" }
}
}
}Un échec est un message normal, pas une connexion interrompue. job.failed contient la langue et un error, ainsi que le même instantané complet — la langue en échec affiche status: "failed" dans la map jobs, toutes les autres langues continuent d’arriver, et le socket continue jusqu’à group.completed :
{
"type": "job.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"locale": "ja",
"error": "Model timeout after 30 seconds",
"snapshot": { "...": "..." }
}Lorsque toutes les tâches sont résolues, le serveur envoie un événement final et ferme la connexion :
{
"type": "group.completed",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"status": "completed",
"snapshot": { "...": "..." }
}Le status final vaut completed lorsque chaque langue a réussi, completed_with_warnings lorsque chaque langue a produit une sortie mais qu’une ou plusieurs étapes facultatives du pipeline ont échoué pour au moins l’une d’entre elles, partial lorsque certaines langues ont réussi et d’autres ont échoué, et failed lorsqu’elles ont toutes échoué. Pour savoir ce que chacun de ces états signifie pour le groupe dans son ensemble, voir Suivre un groupe de tâches.
Affichez à partir de l’instantané dès que quelque chose vous échappe
Traitez les types de messages que vous connaissez, puis, pour tout ce que vous ne reconnaissez pas, retombez sur un rendu à partir de snapshot. Chaque message contient un instantané complet, donc un client qui choisit par défaut de l’afficher reste correct même face à une trame pour laquelle il n’a pas de branche spécifique.
L’intégrer à votre interface#
Le groupe est votre modèle de progression. Quand vous avez créé les tâches, la réponse 202 vous a renvoyé un groupId et un tableau jobs — une entrée par langue. Initialisez votre suivi de progression à partir de cette réponse, et vous obtenez la structure que le socket viendra remplir : le total à atteindre et un compteur qui démarre à zéro.
const { groupId, jobs } = await response.json();
await db.translationProgress.create({
contentId: content.id,
groupId,
totalLanguages: jobs.length,
completedLanguages: 0,
});Ouvrez ensuite le socket sur ce groupId, puis à chaque message, lisez snapshot et réaffichez. Regardez le compteur grimper à mesure que les langues aboutissent, puis arrêtez-vous quand group.completed arrive :
import WebSocket from "ws";
const groupId = "ljg_A1b2C3d4E5f6G7h8";
const ws = new WebSocket(
`wss://api.lingo.dev/jobs/localization/groups/${groupId}/ws`,
{ headers: { "X-API-Key": process.env.LINGO_API_KEY } }
);
ws.on("message", (raw) => {
const event = JSON.parse(raw);
const { snapshot } = event;
switch (event.type) {
case "snapshot":
console.log(`${snapshot.completedJobs}/${snapshot.totalJobs} complete`);
break;
case "job.completed":
console.log(`${event.locale} ready (${snapshot.completedJobs}/${snapshot.totalJobs})`);
break;
case "job.failed":
console.error(`${event.locale} failed: ${event.error}`);
break;
case "group.completed":
console.log(`All translations done: ${event.status}`);
ws.close();
break;
}
});Avec un groupe de trois langues, cela affiche l’exécution en temps réel :
1/3 complete
fr ready (2/3)
ja failed: Model timeout after 30 seconds
All translations done: partialLe compteur a progressé tout seul, une langue a échoué sans faire tomber le flux, et partial vous a indiqué où l’exécution s’est arrêtée — exactement ce qu’il faut pour transformer votre spinner en une vraie barre de progression. À noter : la boucle n’accumule jamais d’état. Chaque branche lit le snapshot du message en cours, donc le même code reste correct à la première connexion, à chaque mise à jour comme après reconnexion.
Conservez votre clé API côté serveur#
Le socket s’authentifie avec votre clé API, la même clé à portée organisation que celle utilisée par les points de terminaison REST. Le navigateur n’est donc pas le bon endroit pour l’ouvrir — une clé API exposée dans le JavaScript client donne accès à chaque moteur de votre organisation à quiconque affiche le code source.
Connectez-vous depuis votre backend, pas depuis le navigateur
Ouvrez le WebSocket depuis votre serveur, là où la clé se trouve déjà, puis rediffusez les événements vers le navigateur via votre propre canal — un WebSocket ou un flux d’événements envoyés par le serveur que vous contrôlez. Votre frontend reçoit la progression en direct ; votre clé ne quitte jamais votre infrastructure.
C’est le même modèle que pour les webhooks : la connexion qui touche Lingo.dev est côté serveur, et ce qui parvient à l’utilisateur dépend de ce que votre application choisit de relayer.
Où cela s’intègre#
Le WebSocket est la vue en direct — il est lié à un seul groupe et se ferme lorsque ce groupe est terminé. Pour une transmission durable de serveur à serveur qui résiste à la fermeture d’un onglet ou à un déploiement, associez-le à des webhooks : le socket alimente l’interface tant que l’exécution est à l’écran, tandis que le webhook enregistre chaque résultat dès qu’il arrive. Déclenchez les deux depuis le même appel de création et vos utilisateurs voient la progression au moment où elle se produit, pendant que votre backend conserve la sortie, qu’il y ait quelqu’un pour la regarder ou non.
