|
Документация
Заказать демоПлатформа
ПлатформаMCPCLIAPI
Процессы
РуководстваЖурнал изменений

Добро пожаловать

  • Обзор
  • Аутентификация
  • Ошибки и коды статуса
  • Подписи webhook

Локализация

  • Обзор
  • Создать задачи
  • Заблокировать непереводимые ключи
  • Отслеживание группы заданий
  • Получить одно задание
  • Список заданий
  • Доставка через вебхук
  • Прогресс в реальном времени (WebSocket)

Пайплайн

  • Обзор
  • AI-редактирование перед локализацией
  • Проверка человеком
  • AI-оценка (post-edit)
  • Перефразирование для естественного звучания
  • Проверка обратным переводом
  • Настройка пайплайна
  • Как отслеживать запуски пайплайна

Развёртывание

  • Обзор
  • Создать задание развёртывания
  • Типы источников
  • Что извлекает AI
  • Доставка webhook
  • Прогресс в реальном времени (WebSocket)

Синхронный

  • Локализация
  • Распознавание

Управление движком

  • Предложения для движка

Прогресс в реальном времени через WebSocket

Вы создали группу задач. Где-то пользователь смотрит на спиннер, а надпись «переводим на 14 языков…» вроде бы правдива, но бесполезна — ничего не меняется. Хочется, чтобы счётчик рос у него на глазах: 3 готово, потом 4, потом одна локаль упала, потом всё завершилось.

Опрос группы задач решает задачу, но это шумно: каждый запрос возвращает новый снимок состояния, который ещё нужно сравнить с предыдущим, чтобы понять, что именно изменилось. WebSocket разворачивает модель в обратную сторону. Подключаетесь один раз, и сервер присылает событие каждый раз, когда по локали есть итог, — при этом каждое сообщение содержит полное состояние группы, так что вы просто рендерите снимок состояния и не сводите дельты. Пропустили кадр, переподключились, перезапустили вкладку — следующее сообщение снова покажет всю картину целиком.

text
GET /jobs/localization/groups/:groupId/ws

Впервые работаете с асинхронной локализацией? Начните с Обзор. groupId здесь — тот самый, который вы получили, когда создали задачи.

На этой странице

  • Типы сообщений
  • Структура сообщений
  • Как встроить это в ваш UI
  • Храните API-ключ на стороне сервера

Типы сообщений#

По сокету идут четыре типа сообщений. Каждое сообщает, что только что произошло, и одновременно передаёт текущее состояние всей группы.

ТипКогдаКлючевые поля
snapshotПри первом подключенииПолное состояние группы
job.completedЛокаль успешно завершиласьjobId, locale и полное состояние группы
job.failedЛокаль завершилась с ошибкойjobId, locale, error и полное состояние группы
group.completedВсе задачи получили итоговый статусgroupId, status и полное состояние группы. После этого сообщения сервер закрывает соединение.

Каждое сообщение содержит объект snapshot с текущим состоянием группы: totalJobs, completedJobs, completedWithWarningsJobs, failedJobs и карту jobs с ключами по ID задач, где у каждой записи есть свои locale и status. Эти счётчики совпадают с теми, что возвращает endpoint группы задач, — поэтому снимок из сокета и результат опроса REST endpoint одинаково показывают, насколько продвинулась группа.

рендерите снимок состояния, а не сводите изменения

Вам не нужно отслеживать, какие события вы уже видели, воспроизводить пропущенные сообщения или сливать частичные обновления с локальным состоянием. Просто читайте snapshot в каждом сообщении и отрисовывайте UI по нему. При переподключении сначала снова приходит snapshot, поэтому клиент, который только что подключился, и клиент, который слушал всё это время, сходятся к одному и тому же состоянию.

Структура сообщений#

Ниже — точные фреймы, которые отправляет сервер. ID показаны в реальном формате (ljg_ для группы, ljb_ для каждой задачи); snapshot сокращён до "..." только там, где повторяет уже показанную структуру.

При подключении сервер отправляет текущее состояние:

json
{
  "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" }
    }
  }
}

По мере завершения локалей событие указывает, какая локаль изменилась, и включает обновлённый снимок состояния:

json
{
  "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" }
    }
  }
}

Сбой приходит обычным сообщением, а не разрывом соединения. job.failed содержит локаль и error, а также тот же полный снимок состояния: у локали с ошибкой будет status: "failed" в карте jobs, все остальные локали продолжают поступать, а сокет работает дальше вплоть до group.completed:

json
{
  "type": "job.failed",
  "jobId": "ljb_C3d4E5f6G7h8I9j0",
  "locale": "ja",
  "error": "Model timeout after 30 seconds",
  "snapshot": { "...": "..." }
}

Когда все задачи получили итоговый статус, сервер отправляет финальное событие и закрывает соединение:

json
{
  "type": "group.completed",
  "groupId": "ljg_A1b2C3d4E5f6G7h8",
  "status": "completed",
  "snapshot": { "...": "..." }
}

Итоговый status будет completed, если все локали завершились успешно; completed_with_warnings, если каждая локаль дала результат, но один или несколько необязательных этапов пайплайн завершились ошибкой хотя бы для одной из них; partial, если часть локалей завершилась успешно, а часть — с ошибкой; и failed, если с ошибкой завершились все. Что каждый из этих статусов означает для группы в целом, см. в Track a job group.

Если не распознаёте сообщение, рендерите по снимку состояния

Разбирайте известные вам типы сообщений через switch, а для всего незнакомого просто заново рендерите UI по snapshot. Каждое сообщение содержит полный снимок состояния, поэтому клиент, который по умолчанию отрисовывается на его основе, остаётся корректным даже для фрейма, для которого у него нет отдельной ветки обработки.

Как встроить это в ваш UI#

Группа — это ваша модель прогресса. Когда вы создали задачи, ответ 202 вернул вам groupId и массив jobs — по одной записи на локаль. Возьмите этот ответ за основу для своей структуры прогресса, и у вас уже будет форма, которую затем заполнит сокет: общее количество для отсчёта и счётчик, стартующий с нуля.

javascript
const { groupId, jobs } = await response.json();

await db.translationProgress.create({
  contentId: content.id,
  groupId,
  totalLanguages: jobs.length,
  completedLanguages: 0,
});

Дальше откройте сокет для этого groupId, а в каждом сообщении читайте snapshot и перерисовывайте UI. Наблюдайте, как счётчик растёт по мере завершения локалей, и останавливайтесь, когда придёт group.completed:

javascript
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;
  }
});

Если запустить это на группе из трёх локалей, вывод будет показывать ход выполнения в реальном времени:

text
1/3 complete
fr ready (2/3)
ja failed: Model timeout after 30 seconds
All translations done: partial

Счётчик рос сам по себе, одна локаль завершилась с ошибкой, не уронив поток, а partial показал, чем в итоге закончился запуск, — именно то, что нужно, чтобы спиннер превратился в полноценный индикатор прогресса. Обратите внимание: цикл вообще не накапливает состояние — каждая ветка читает данные из snapshot в текущем сообщении, поэтому один и тот же код корректно работает при первом подключении, на каждом обновлении и после переподключения.

Храните API-ключ на стороне сервера#

Сокет аутентифицируется вашим API-ключом — тем же ключом уровня организации, который используют REST endpoints. А значит, открывать его из браузера не стоит: API-ключ в клиентском JavaScript даёт доступ к каждому движок в вашей организации любому, кто откроет исходный код.

Подключайтесь с бэкенда, а не из браузера

Открывайте WebSocket со своего сервера, где ключ уже хранится, а затем прокидывайте события в браузер по своему каналу — через WebSocket или поток server-sent events, который вы контролируете. Фронтенд получает прогресс в реальном времени, а ключ не покидает вашу инфраструктуру.

Это повторяет модель webhook: соединение с Lingo.dev устанавливается на стороне сервера, а до пользователя доходит только то, что ваше приложение решит передать дальше.

Где это использовать#

WebSocket — это живое представление: он привязан к одной группе и закрывается, когда работа по ней завершена. Для надёжной доставки от сервера к серверу, которая переживёт закрытие вкладки или деплой, используйте его в паре с webhooks: сокет ведёт UI, пока выполнение видно на экране, а webhook фиксирует каждый результат в момент появления. Подключите оба механизма из одного и того же create call, и пользователи будут видеть прогресс по мере выполнения, а ваш бэкенд сохранит результат независимо от того, следит ли кто-то за процессом.

Доставка через webhook
Надёжная доставка каждого результата по локали от сервера к серверу по мере завершения
Создать задачи
Отправьте контент на перевод и получите groupId, к которому здесь нужно подключаться
Track a job group
Статусы группы и что частичное завершение означает для группы в целом

Эта страница была полезной?

Max PrilutskiyMax Prilutskiy·Обновлено 12 дней назад·5 минут чтения