Você criou um grupo de jobs. Em algum lugar, um usuário está olhando para um spinner, e “traduzindo para 14 idiomas…” é verdade, mas não ajuda em nada — nunca sai do lugar. O que você quer é ver a contagem subir na frente dele: 3 prontos, depois 4, depois um idioma com falha, depois concluído.
Fazer polling do grupo de jobs resolve, mas gera tráfego demais, e cada poll traz um novo snapshot que você precisa comparar com o anterior para saber o que de fato mudou. O WebSocket vira esse jogo. Você conecta uma vez, e o servidor envia um evento sempre que um idioma é resolvido — e cada mensagem traz o estado completo do grupo, então você renderiza o snapshot, sem nunca reconciliar um delta. Se um frame se perder, se você reconectar ou reiniciar a aba, a próxima mensagem volta a trazer a verdade completa.
GET /jobs/localization/groups/:groupId/wsAinda é novo em localização assíncrona? Comece pela Visão geral. O groupId aqui é o mesmo que você recebeu quando criou os jobs.
Nesta página
- Tipos de mensagem
- Payloads de mensagem
- Como integrar isso à sua UI
- Mantenha sua chave de API no servidor
Tipos de mensagem#
Quatro tipos de mensagem passam pelo socket. Cada um mostra o que acabou de acontecer e traz junto o estado atual do grupo inteiro.
| Tipo | Quando | Campos principais |
|---|---|---|
snapshot | Na conexão inicial | Estado completo do grupo |
job.completed | Um idioma é concluído com sucesso | jobId, locale, mais o estado completo do grupo |
job.failed | Um idioma falha | jobId, locale, error, mais o estado completo do grupo |
group.completed | Todos os jobs foram resolvidos | groupId, status, mais o estado completo do grupo. O servidor fecha a conexão depois dessa mensagem. |
Cada mensagem contém um objeto snapshot com o estado atual do grupo: totalJobs, completedJobs, completedWithWarningsJobs, failedJobs e um mapa jobs indexado por ID de job, cada um com seu locale e status. Essas contagens são as mesmas reportadas pelo endpoint do grupo de jobs — então um snapshot vindo do socket e um poll no endpoint REST concordam sobre quanto o grupo já avançou.
renderize o snapshot, nunca reconcilie
Você nunca precisa rastrear quais eventos já viu, reproduzir mensagens perdidas nem mesclar uma atualização parcial ao estado local. Leia snapshot em cada mensagem e pinte sua UI a partir dele. Em uma reconexão, snapshot é reenviado primeiro, então um cliente que acabou de entrar e outro que está ouvindo desde o começo convergem para o mesmo estado.
Payloads de mensagem#
Estes são os frames exatos que o servidor envia. Os IDs têm formatos reais (ljg_ para o grupo, ljb_ para cada job); o snapshot aparece abreviado como "..." apenas onde repete a estrutura já mostrada.
Ao conectar, o servidor envia o estado atual:
{
"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" }
}
}
}À medida que cada idioma é concluído, o evento identifica o idioma que mudou e inclui o snapshot atualizado:
{
"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" }
}
}
}Uma falha é uma mensagem normal, não uma conexão encerrada. job.failed traz o idioma e um error, além do mesmo snapshot completo — o idioma com falha aparece como status: "failed" no mapa jobs, todos os outros idiomas continuam chegando por streaming, e o socket segue até group.completed:
{
"type": "job.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"locale": "ja",
"error": "Model timeout after 30 seconds",
"snapshot": { "...": "..." }
}Quando todos os jobs forem resolvidos, o servidor envia um evento final e fecha a conexão:
{
"type": "group.completed",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"status": "completed",
"snapshot": { "...": "..." }
}O status terminal é completed quando todos os idiomas são concluídos com sucesso, completed_with_warnings quando todos os idiomas geram saída, mas uma ou mais etapas opcionais do pipeline falham em pelo menos um deles, partial quando alguns idiomas têm sucesso e outros falham, e failed quando todos falham. Para entender o que cada um deles significa para o grupo como um todo, veja Track a job group.
Renderize a partir do snapshot sempre que houver algo que você não reconheça
Faça switch nos tipos de mensagem que você conhece e, para qualquer coisa que não reconhecer, volte a renderizar a partir de snapshot. Toda mensagem traz um snapshot completo, então um cliente que, por padrão, pinta a UI a partir dele continua correto mesmo ao receber um frame para o qual não tem um branch específico.
Como integrar isso à sua UI#
O grupo é o seu modelo de progresso. Quando você criou os jobs, o 202 devolveu um groupId e um array jobs — uma entrada por idioma. Inicialize seu registro de progresso com essa resposta e você já terá o formato que o socket vai preencher: o total a acompanhar e um contador começando em zero.
const { groupId, jobs } = await response.json();
await db.translationProgress.create({
contentId: content.id,
groupId,
totalLanguages: jobs.length,
completedLanguages: 0,
});Depois, abra o socket para esse groupId e, em cada mensagem, leia snapshot e renderize de novo. Veja o contador subir à medida que os idiomas forem chegando e pare quando group.completed chegar:
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;
}
});Executando em um grupo de três idiomas, isso imprime a execução em tempo real:
1/3 complete
fr ready (2/3)
ja failed: Model timeout after 30 seconds
All translations done: partialO contador avançou sozinho, um idioma falhou sem derrubar o stream, e partial mostrou onde a execução terminou — exatamente o que seu spinner precisa para virar uma barra de progresso de verdade. Repare que o loop nunca acumula estado: cada branch lê o snapshot da mensagem em mãos, então o mesmo código funciona corretamente na primeira conexão, em cada atualização e na reconexão.
Mantenha sua chave de API no servidor#
O socket se autentica com sua chave de API, a mesma chave com escopo de organização usada pelos endpoints REST. Isso significa que o navegador é o lugar errado para abri-lo — uma chave de API no JavaScript do cliente dá acesso a toda engine da sua organização para qualquer pessoa que visualizar o código-fonte.
Conecte pelo seu backend, não pelo navegador
Abra o WebSocket a partir do seu servidor, onde a chave já está, e então distribua os eventos ao navegador pelo seu próprio canal — um WebSocket ou stream de server-sent events sob seu controle. Seu frontend recebe progresso em tempo real; sua chave nunca sai da sua infraestrutura.
Isso espelha o modelo de webhook: a conexão que toca o Lingo.dev fica no servidor, e o que chega ao usuário é o que o seu próprio app decidir encaminhar.
Onde isso se encaixa#
O WebSocket é a visualização em tempo real — fica vinculado a um grupo e se fecha quando esse grupo termina. Para uma entrega durável, de servidor para servidor, que sobreviva ao fechamento de uma aba ou a um deploy, combine-o com webhooks: o socket move a UI enquanto a execução está na tela, e o webhook registra cada resultado no momento em que ele chega. Conecte os dois a partir da mesma chamada de criação e seus usuários verão o progresso conforme ele acontece, enquanto seu backend preserva a saída independentemente de quem estiver acompanhando.
