For real-time progress updates, connect a WebSocket to a job group. Every message includes a full state snapshot, so clients never need to maintain local state or worry about missed events.
GET /jobs/localization/groups/:groupId/wsMessage types#
| Type | When | Key fields |
|---|---|---|
snapshot | On initial connection | Full group state |
job.completed | A job finishes successfully | jobId, locale, plus full group state |
job.failed | A job fails | jobId, locale, error, plus full group state |
group.completed | All jobs are done | groupId, status, plus full group state |
Every message contains a snapshot object with the current group state: totalJobs, completedJobs, failedJobs, and a jobs map with per-job locale and status. The type field tells you what just happened. The snapshot tells you where things stand.
Message payloads#
On connect, the server sends the current state:
{
"type": "snapshot",
"snapshot": {
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"totalJobs": 3,
"completedJobs": 1,
"failedJobs": 0,
"jobs": {
"ljb_A1b2C3d4E5f6G7h8": { "locale": "de", "status": "completed" },
"ljb_B2c3D4e5F6g7H8i9": { "locale": "fr", "status": "processing" },
"ljb_C3d4E5f6G7h8I9j0": { "locale": "ja", "status": "queued" }
}
}
}As jobs finish, each event includes the updated snapshot:
{
"type": "job.completed",
"jobId": "ljb_B2c3D4e5F6g7H8i9",
"locale": "fr",
"snapshot": {
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"totalJobs": 3,
"completedJobs": 2,
"failedJobs": 0,
"jobs": {
"ljb_A1b2C3d4E5f6G7h8": { "locale": "de", "status": "completed" },
"ljb_B2c3D4e5F6g7H8i9": { "locale": "fr", "status": "completed" },
"ljb_C3d4E5f6G7h8I9j0": { "locale": "ja", "status": "processing" }
}
}
}{
"type": "job.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"locale": "ja",
"error": "Model timeout after 30 seconds",
"snapshot": { "..." : "..." }
}When every job has resolved, the server sends a final event and closes the connection:
{
"type": "group.completed",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"status": "partial",
"snapshot": { "..." : "..." }
}The status field is completed when all jobs succeeded, partial when some failed, or failed when all failed.
Example#
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":
// Full state on connect - initialize your progress UI
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;
}
});Keep your API key server-side
WebSocket connections require API key authentication. Connect from your backend, then proxy events to your frontend over your own WebSocket or server-sent events channel. This keeps the API key out of client-side code.
Wiring into your UI#
The job group provides a natural progress model. Store the groupId when you create jobs, then update your UI as WebSocket events arrive. A progress indicator ("3 of 14 languages ready") that updates in real time gives users confidence that translations are in progress.
// Content saved - kick off translations
async function onContentSaved(content) {
const response = await fetch("https://api.lingo.dev/jobs/localization", {
method: "POST",
headers: {
"X-API-Key": process.env.LINGO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
sourceLocale: "en",
targetLocales: ["de", "fr", "ja", "ko", "pt-BR"],
data: {
title: content.title,
steps: content.steps,
metadata: content.metadata,
},
callbackUrl: "https://your-app.com/webhooks/translations",
idempotencyKey: `${content.id}-v${content.version}`,
}),
});
const { groupId, jobs } = await response.json();
// Store for progress tracking in your UI
await db.translationProgress.create({
contentId: content.id,
groupId,
totalLanguages: jobs.length,
completedLanguages: 0,
});
}