|
文档
预约演示平台
平台MCPCLIAPI
工作流
指南更新日志

欢迎

  • 概览
  • 身份验证
  • 错误与状态码
  • Webhook 签名

本地化

  • 概览
  • 创建任务
  • 锁定不可翻译的键
  • 查看任务组状态
  • 获取单个作业
  • 列出作业
  • Webhook 结果投递
  • 实时进度(WebSocket)

流水线

  • 概览
  • 本地化前 AI 预编辑
  • 人工审核
  • AI 审校(后编辑)
  • 改写成更自然的文案
  • 回译检查
  • 配置 Pipeline
  • 查看流水线运行记录

预配

  • 概览
  • 创建预配作业
  • 来源类型
  • AI 会提取哪些内容
  • Webhook 投递
  • 实时进度(WebSocket)

同步

  • 本地化
  • Recognize

引擎管理

  • Engine Suggestions

通过 WebSocket 实时跟踪进度

你创建了一个作业组。某个用户正盯着加载动画看,“正在翻译成 14 种语言……”这句话没错,却没什么用——它根本不会动。你真正想要的是让数字在用户眼前往上跳:先是 3 个已完成,然后 4 个,接着某个语言区域失败,最后全部结束。

轮询 作业组 也能实现,但请求会很频繁,而且每次轮询拿到的都是一份全新快照;你还得和上一次结果做 diff,才能知道到底变了什么。WebSocket 则反过来。连一次就够,之后每当某个语言区域处理完成,服务器都会主动推送事件——而且每条消息都携带完整的组状态,所以你只需要渲染快照,根本不用对增量做合并。哪怕丢了一帧、断线重连,或者标签页重开,下一条消息依然会把完整状态原样带回来。

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

如果你刚接触异步本地化,建议先看 概览。这里的 groupId,就是你在 创建作业 时拿到的那个。

本页内容

  • 消息类型
  • 消息负载
  • 如何接入 UI
  • 将 API 密钥保留在服务端

消息类型#

Socket 上会传输四种消息。每一种都会告诉你刚刚发生了什么,同时附带整个组的当前状态。

类型触发时机关键字段
snapshot初次连接时完整组状态
job.completed某个语言区域成功完成时jobId、locale,以及完整组状态
job.failed某个语言区域失败时jobId、locale、error,以及完整组状态
group.completed所有作业都已完成时groupId、status,以及完整组状态。发送完这条消息后,服务器会关闭连接。

每条消息都包含一个 snapshot 对象,用来表示当前组状态:totalJobs、completedJobs、completedWithWarningsJobs、failedJobs,以及一个以作业 ID 为键的 jobs 映射;映射中的每一项都带有各自的 locale 和 status。这些计数和 作业组端点 返回的是同一套数据——也就是说,无论是从 socket 收到快照,还是从 REST 端点轮询,看到的组进度都是一致的。

只渲染快照,不做增量合并

你不需要追踪哪些事件已经看过,也不需要补放漏掉的消息,更不用把局部更新合并进本地状态。每次收到消息时,直接读取 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,同时附带同样的完整快照——失败的语言区域会在 jobs 映射中显示为 status: "failed",其他语言区域仍会继续推送,socket 也会一直保持连接,直到 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;如果所有语言区域都有输出结果,但其中至少一个语言区域的某些可选 pipeline 阶段失败,则为 completed_with_warnings;如果部分语言区域成功、部分失败,则为 partial;如果全部失败,则为 failed。想了解这些状态对整个组分别意味着什么,请参阅 跟踪作业组。

遇到无法识别的内容时,直接按快照渲染

已知的消息类型可以按分支处理;遇到任何不认识的情况,就直接回退到基于 snapshot 重新渲染。因为每条消息都带着完整快照,所以即使客户端收到一个自己没有专门分支处理的数据帧,只要默认按快照绘制,状态依然是正确的。

如何接入 UI#

这个组就是你的进度模型。当你 创建作业 时,返回的 202 会给你一个 groupId 和一个 jobs 数组——每个语言区域对应一项。用这个响应来初始化进度记录,你就先拿到了后续 socket 会不断填充的那份结构:要累计的总数,以及一个从零开始的计数器。

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

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

接着,针对这个 groupId 打开 socket,并在每条消息到来时读取 snapshot 重新渲染。你会看到计数随着各个语言区域陆续完成而不断增长,并在收到 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 密钥保留在服务端#

这个 socket 使用你的 API 密钥进行认证,也就是 REST 端点所用的同一个 组织范围密钥。这也意味着,浏览器并不是打开它的地方——一旦把 API 密钥放进客户端 JavaScript,任何能查看源码的人都能借此访问你组织中的所有引擎。

从后端连接,不要直接在浏览器里连

应该由你的服务器来建立 WebSocket 连接,因为密钥本来就保存在那里;然后再通过你自己的通道——例如你可控的 WebSocket 或 server-sent events 流——把这些事件转发给浏览器。这样前端照样能拿到实时进度,而密钥始终不会离开你的基础设施。

这和 webhook 的模式是一样的:真正连接到 Lingo.dev 的一端在服务端,最终到达用户的内容,则由你自己的应用决定转发什么。

适用场景#

WebSocket 提供的是实时视图——它只绑定到一个组,并会在该组结束后关闭。若你需要一种更持久、服务器到服务器的传递方式,能够跨越标签页关闭或部署重启继续可靠运行,就把它和 webhooks 搭配起来:当运行过程展示在界面上时,socket 负责驱动 UI;而 webhook 会在每个结果落地的第一时间完成记录。两者都从同一个 create call 接起来后,用户能实时看到进度,而你的后端也能在无论是否有人盯着页面时都保留输出结果。

Webhook 传递
每个语言区域完成时,提供持久可靠的服务器到服务器传递
创建作业
提交待翻译内容,并获取这里用于连接的 groupId
跟踪作业组
了解组状态,以及部分完成对整个组意味着什么

这个页面对你有帮助吗?

Max PrilutskiyMax Prilutskiy·已更新 12 天前·2 分钟阅读