首页
/ OpenClaw 文档 i18n 翻译流水线:防抖、增量翻译与聚合提交的完整设计

OpenClaw 文档 i18n 翻译流水线:防抖、增量翻译与聚合提交的完整设计

2026-09-04 21:00:46作者:何举烈Damon

OpenClaw 的文档体系采用「英文源文档 + 独立发布仓库」的架构:英文文档在源码仓库中随每次推送快速发布,而 20+ 种语言译本则通过一套带防抖(debounce)和增量(incremental)机制的自动化翻译流水线生成。读完本文,你将理解这套流水线的 11 步事件流、防抖冷却策略、基于 x-i18n.source_hash 的增量翻译判定、跨 job 的 artifact 契约与聚合提交机制,并能对照本仓库中的同步 Workflow、发布触发器和 Go 翻译工具源码,掌握每个设计决策背后的实现证据。

架构背景:为什么源仓库与译本仓库要分离

翻译工作流文档位于 docs/.i18n/translation-workflow.md,它明确自身是「docs 发布流水线的内部说明」,且 docs/.i18n 目录会被文档站点构建忽略、不对外发布。同一目录下的 docs/.i18n/README.md 解释了整体拆分逻辑:

  • 英文文档以 openclaw/openclaw(即本仓库)中的 docs/ 为唯一事实来源;
  • 生成产物(各语言 locale 树与实时翻译记忆 translation memory)存放在发布仓库 openclaw/docs,源仓库不再提交 docs/zh-CN/**docs/ja-JP/** 等生成目录;
  • 拆分的目的:把生成产物挡在主产品仓库外、让 Mintlify 保持单一发布文档树、保留内置语言切换器,并且即使某些 locale(如 thfa)暂不被 Mintlify 的 navigation.languages 接受,其译本与翻译记忆仍会被持续生成和保留。

翻译资产本身保留在源仓库的 docs/.i18n/ 下:每个语言一份术语表(如 glossary.zh-CN.json,另有 glossary.ja-JP.jsonglossary.de.json 等 20+ 份)和导航文件(zh-Hans-navigation.jsonja-navigation.json 等)。术语表在 Go 翻译工具中按极简结构解析:

type GlossaryEntry struct {
    Source string `json:"source"`
    Target string `json:"target"`
}

(见 scripts/docs-i18n/glossary.go

这一拆分直接决定了后文的部署策略:英文可以「每笔提交即部署」,而翻译只能低频、聚合地部署。

流水线设计目标

工作流文档开头列出了六条硬性目标,它们是整个流水线所有机制的出发点:

  1. 英文文档在每次源文档同步后快速部署;
  2. locale 翻译不因 main 上的每一个热点提交而运行;
  3. 翻译任务带防抖,一批密集的文档提交只触发一轮(one translation wave)翻译;
  4. locale job 只翻译「自上次成功的 locale 输出以来源 hash 发生变化」的页面;
  5. 成功的 locale 输出被一次性聚合提交——即使部分 locale job 失败;
  6. 每周一次的对账(reconciliation)重跑所有 locale/页面路径,修复漏译或不稳定的翻译。

完整事件流:从文档同步到线上冒烟

工作流定义了 11 步事件流,下面逐步展开并补充仓库内可验证的实现证据:

  1. 同步英文文档openclaw/openclaw 将英文文档同步进 openclaw/docs。本仓库侧的入口是 docs-sync-publish.yml:当 main 分支的 docs/** 及相关脚本变更时触发,checkout 源仓库与 openclaw/clawhub 文档源,克隆发布仓库,然后调用 node scripts/docs-sync-publish.mjs --target ... --source-repo ... --source-sha ... 执行镜像。
  2. 英文立即部署:GitHub Pages 直接从同步提交部署英文/源变更。
  3. 触发 Translate All:由同步提交、release dispatch、手动 dispatch 或每周定时任务触发。发布侧的触发脚本在源仓库有明确对应——docs-translate-trigger-release.ymlrelease: published 事件时,通过 gh api repos/openclaw/docs/dispatches 向发布仓库发送 event_type=translate-all-releaseclient_payload 携带 mode=incrementalrelease_tagsource_repositorysource_sha
  4. 协调器(coordinator)冷却等待:开始翻译前先等待一个冷却窗口(见下节)。
  5. 读取源元数据:冷却结束后,协调器读取当前 origin/main 的源元数据,即发布仓库中的 .openclaw-sync/source.json。该文件由同步脚本写入:scripts/docs-sync-publish.mjs 第 857 行执行 writeJson(path.join(targetRoot, ".openclaw-sync", "source.json"), metadata)
  6. 采用更新状态:若冷却期间到达更新一版文档同步,协调器改用更新的源状态。
  7. 并行 locale job:逐语言翻译 job 以 fail-fast: false 并行运行——单个 locale 失败不阻塞其他语言。
  8. 上传 artifact:每个 locale job 为其请求的源 SHA 上传一个 artifact(契约见下文)。
  9. finalizer 聚合:finalizer 下载可用 artifact、忽略过期或失败载荷,推送唯一一次聚合 i18n 提交。
  10. 触发 Pages 部署:聚合提交落地后,finalizer 只 dispatch 一次 Pages 部署。
  11. 线上冒烟:Pages workflow 在部署完成后再 dispatch live smoke,保证冒烟测试检查的是已部署站点,而不是与部署过程赛跑。

补充一点实现细节:docs-sync-publish.ymlconcurrency 配置特意采用「排队而非取消」(cancel-in-progress: false),提交注释解释了原因——cancel-in-progress 会饿死镜像仓库:每次 main 推送都取消在途同步,而取消的运行没有后继者,持续高合并速率下 openclaw/docs 永远无法前进;配合脚本内的 skip_stale_source(通过 git merge-base --is-ancestor 判断远端已包含当前源 SHA 时跳过),完成了幂等保证。

防抖策略:把提交风暴收敛成一轮翻译

防抖(debounce)是这套流水线的核心成本控制点,规则如下:

  • 协调器在文档同步或 release dispatch 之后等待 1 小时,再重新读取 origin/main
  • 默认冷却时长由发布仓库的仓库变量 OPENCLAW_DOCS_TRANSLATION_COOLDOWN_SECONDS 控制,默认值 3600
  • 仓库 dispatch 调用方可通过 client_payload.cooldown_seconds 覆盖它,手动运行可设置 cooldown_seconds 输入;
  • 如果等待期间 .openclaw-sync/source.json 发生了变化,协调器会从更新的状态重新计时
  • 如果 main 持续移动,等待上限由 OPENCLAW_DOCS_TRANSLATION_MAX_WAIT_SECONDS 封顶,默认等于冷却值;达到上限后翻译「最新观测到的状态」;
  • 手动运行与每周运行默认不等待,直接进入翻译。

这套「滑动冷却 + 上限封顶」的组合解决了两个矛盾:既不会在文档提交密集期每笔都跑全量翻译,也不会因为 main 一直有提交而让翻译无限顺延。

增量翻译:以 x-i18n.source_hash 为判定依据

每个被翻译的页面都在 front matter 中存储 x-i18n.source_hash,locale job 将当前英文页 hash 与已存储的 locale hash 对比。这一机制的写入端在 Go 翻译工具中可以直接看到——scripts/docs-i18n/process.goencodeFrontMatter 为每个译文页面生成完整的 x-i18n 元数据块:

frontData["x-i18n"] = map[string]any{
    "source_path":         relPath,
    "source_hash":         hashBytes(source),
    "provider":            docsI18nProvider(),
    "model":               docsI18nModel(),
    "workflow":            workflowVersion,
    "prompt_version":      promptVersion,
    "generated_at":        time.Now().UTC().Format(time.RFC3339),
    "postprocess_version": localizedLinkPostprocessPending,
}

除了 source_hash,该块还记录了 provider、model、workflow 版本、prompt 版本与生成时间——这意味着当 prompt 或工作流升级时,流水线有能力识别「哪些译文是用旧 prompt 生成的」。

正常(incremental)运行只翻译三类页面:

  • 缺失的 locale 页面(源页面存在但该语言尚无译文);
  • x-i18n.source_hash 过期的页面(英文源已修改);
  • 受源删除/裁剪影响的页面(英文页被删,对应译文需同步清除)。

两条边界规则:

  • docs/.i18n/** 下的内部文件不是翻译输入;仅变更内部 i18n 文件的 push 触发运行会在进入 locale 矩阵之前直接跳过;
  • 某个 locale job 失败时,其 artifact 被标记为失败且不携带载荷。finalizer 仍然提交成功的 locale;失败的 locale 保持「过期」状态——因为它的源 hash 仍然不匹配,会在下一次增量运行中被自动捡起重翻。这是「失败自愈」的关键:不需要重试队列,hash 不匹配本身就是待办列表。

翻译记忆(translation memory)以 JSONL 格式维护在 docs/.i18n/<locale>.tm.jsonlscripts/docs-i18n/tm.go 逐行解码该文件,只收录 CacheKey 非空且 Translated 非空白的条目——缓存键到译文片段的映射让重复出现的术语和句子在后续翻译中无需重新调用 LLM。

Artifact 契约:locale job 与 finalizer 之间的接口

locale job 与 finalizer 之间通过 artifact 解耦,命名格式为「语言 + 源 SHA」:

i18n-zh-cn-<source-sha>

每个 artifact 的内容结构固定:

metadata.json
changed-files.txt
deleted-files.txt
payload/docs/<locale>/**
payload/docs/.i18n/<locale>.tm.jsonl

其中 metadata.json 包含:locale、locale slug、源 SHA、pending 数量、changed 数量,以及任何失败原因。finalizer 会拒绝 source_sha 与当前 .openclaw-sync/source.json 不匹配的 artifact——这保证了即使一次旧触发的 artifact 迟到,也不会把过期译文写进当前源状态之上。

关于触发事件的兼容性:源仓库的 release workflow 只 dispatch 一个 translate-all-release 事件(见 docs-translate-trigger-release.yml),协调器仍接受旧版按 locale 分发的 release 事件以保持兼容,但这些事件只是兜底路径。

聚合提交:正常路径上唯一一次 locale push

finalizer 拥有正常路径上唯一的 locale push 权限:

  • 提交信息固定为:
chore(i18n): refresh translations
  • 该提交可能只包含部分 locale 集合——失败的语言缺席不影响其余语言落地;
  • job summary 会列出:已应用的语言、无变化的语言、缺失或失败的语言、过期 artifact、无效 artifact。

这个「部分成功可提交、失败者下轮补齐」的语义,与增量翻译的 hash 判定形成闭环:失败 locale 的页面 hash 不匹配,天然进入下一轮待翻集合。

每周对账:修复 LLM 不稳定性的兜底机制

每周运行使用 full 模式:强制对所有 locale、所有源页面做一次全量对账,而不是只依赖变化的源 hash。

此外,术语表变更也会强制全量对账——因为 glossary 指引可能影响源 hash 并未变化的页面(同一个英文句子在新术语表下应翻译成不同的表达)。术语表正是 scripts/docs-i18n/glossary.go 加载的 glossary.<locale>.json(源/目标词条对),其变更等价于翻译指引变更,所以必须全量重校。

每周运行的预期行为:

  • 重新生成或校验每个 locale 页面;
  • 裁剪过期的 locale 页面;
  • 按需刷新翻译记忆;
  • 仍然使用并行 locale job;
  • 仍然只提交一个聚合结果;
  • 仍然容忍单个 locale 失败。

工作流文档对此的定位很明确:每周运行是LLM 输出不稳定、部分失败和漏掉的增量更新的修复机制(repair mechanism)。增量路径追求成本最低,full 路径保证最终一致,两者互补。

部署策略:英文高频,译本低频

部署侧的规则是整条流水线的收口:

  • 英文从源同步提交部署——每笔文档提交都是一次快速英文部署;
  • 译文在聚合 i18n 提交之后部署。finalizer 之所以手动 dispatch GitHub Pages 一次,是因为 GitHub 会抑制来自 GITHUB_TOKEN 提交的常规 push 触发 workflow 运行——GITHUB_TOKEN 推送无法自触发 Pages 工作流,必须显式 dispatch;
  • Pages workflow 在部署之后再 dispatch live smoke,让冒烟测试检查已部署站点而非与部署过程赛跑;
  • 预期效果:一个「热点文档日」应该产生大量快速的英文部署,但只有少量 locale 部署;
  • 如果 Mintlify 这类外部部署提供方监听每一次 push,聚合 i18n 提交就是「负载削减器」(load reducer)。文档特别警告:不要恢复按 locale 向 main 的 push,否则会击穿这套低频部署设计。

关键文件索引

文件 作用
docs/.i18n/translation-workflow.md 翻译流水线权威说明(本文主体来源)
docs/.i18n/README.md i18n 资产总览、源/发布仓库拆分缘由与 locale 清单
docs/.i18n/glossary.zh-CN.json 各语言术语表(glossary),翻译约束输入
.github/workflows/docs-sync-publish.yml 英文文档镜像到发布仓库的同步 workflow(含幂等跳过与重试)
.github/workflows/docs-translate-trigger-release.yml release 发布时向发布仓库 dispatch translate-all-release
scripts/docs-sync-publish.mjs 同步脚本,写入 .openclaw-sync/source.json 源元数据(L857)
scripts/docs-i18n/process.go 译文页面 x-i18n front matter(含 source_hash)生成
scripts/docs-i18n/tm.go 翻译记忆 JSONL 的加载与缓存键匹配
scripts/docs-i18n/glossary.go 术语表解析(source/target 词条对)

小结:这套设计值得借鉴的三个模式

从本仓库的实现看,OpenClaw 文档翻译流水线展示了 LLM 驱动的内容生成在 CI 中的三个通用工程模式:

  1. 以内容 hash 为增量基线:不维护「待翻队列」,source_hash 不匹配即待办——失败、遗漏、新增页面全部收敛到同一判定逻辑,天然幂等且可自愈;
  2. 防抖 + 上限封顶的批处理:滑动冷却合并提交风暴,MAX_WAIT 封顶防止无限顺延,手动/定时任务旁路冷却,兼顾成本与时效;
  3. 并行部分成功 + 聚合提交 + 定期全量对账fail-fast: false 让单语言故障不阻塞整体,finalizer 单点持有 push 权,weekly full 模式兜住 LLM 的不稳定性——增量路径求快,对账路径求对。

对任何需要为多语言文档站点接入 LLM 翻译的团队,「英文高频部署 + 译文聚合低频部署 + hash 增量 + 每周全量对账」这套组合是一个经过本仓库 workflow 与 Go 工具源码双重佐证的完整参考实现。

登录后查看全文
热门项目推荐
相关项目推荐