Bilingual documentation via paired sibling files and a pairing gate
通过配对兄弟文件与配对门禁实现双语文档
This repo's documentation corpus is read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see [quality gates](2026-06-11-quality-gates.md) and [
English
Problem
This repo's documentation corpus is read by people and agents inside and outside the company, in both English and Chinese. Maintaining a second language by hand, with no mechanism, is how translations rot: one side moves on, the other silently lies, and no gate notices. The repo's standing answer to invariants of this kind is to encode them as a mechanical check (see quality gates and doc-sync enforcement), so the bilingual policy ships with one.
Decision
- Paired sibling files with equal authority. A documentation pair is three sibling files: English
foo.md, Chinesefoo.zh.md, and a consistency recordfoo.i18n.yaml. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: docs/i18n/README.md; translation rules: docs/i18n/translation-rules.md; terminology source of truth: docs/i18n/terminology.md. - A sidecar record of both blob hashes makes consistency checkable.
foo.i18n.yamlholds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (verify-translation-pairing --write <pair>, which requires naming the confirmed pairs — bulk re-record is an explicit--write --all) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR. verify-translation-pairingjoinsdoc-sync. The gate (scripts/verify-translation-pairing.ts) enforces: every discovered, non-excluded source has a complete pair; every existing pair is complete (all three files) and consistent (both hashes match, the Chinese side and every authored English source carry their switchers while listed generated English sources are exempt, structural signatures identical); and excluded generated, instruction, or bilingual-by-construction files stay unpaired. Relative document links whose targets belong to that active corpus use the target sibling matching the source locale, while the structure signature normalizes.mdand.zh.mdsiblings to one semantic target and retains the exact query/fragment suffix; the localized bilingual links decision owns that refinement. scripts/translation-pairing.manifest.json contains only explicit exclusions, so no requirement can bypass discovery and receive a weaker check. Source-oriented code gates consume a.zh.mdfence sequence as a derivative only when its unsuffixed sibling has the same tracked fences in the same order with byte-identical bodies; an incomplete, reordered, reclassified, or changed sequence stays independent, so the owning code gate or pairing gate reports the mismatch.- One corpus-wide requirement. Every document in scope requires a complete pair from creation; the policy has no per-file rollout state, date cutoff, or README-specific class. README discovery covers every case-insensitive README basename outside vendored, dependency, and ignored build-output trees, including future top-level directories. A site-published pair uses
pairedPages()so the root locale projects.zh.mdand/en/projects.md; creating a counterpart alone does not publish it. - Pairing records are metadata, not Cordis Loader configuration. Cordis configuration discovery accepts actual
.cordis.ymland.cordis.yamlfiles while excluding*.i18n.yaml, even when the document name containscordis. This preserves validation of executable Loader entries without parsing translation hashes as configuration. - Translation is agent work with human review. Routine changes use the direct one-pass path owned by the lightweight-translation decision. The extended translation skill retains delegated translation and the other heavier mechanisms for explicit user invocation; both paths defer to the documentation contracts as their sources of truth.
Verification
The verification contract covers each boundary independently. verify-translation-pairing pins pair completeness, hashes, switchers, and structure; project-doc-site.spec.ts pins locale-specific source selection for published pairs; cordis-config-files.spec.ts pins discovery of Loader YAML and exclusion of translation records; and the translation-prompt runnable snapshot pins the rendered system message, five reviewed example pairs, source request, and consumed response. Together these checks make pair drift, publication drift, configuration misclassification, and model-visible prompt drift review-visible.
Alternatives considered
- English as the canonical source with a fingerprint inside the translation —
.zh.mdfiles would carry an HTML comment recording the English source's blob hash, and translation would flow EN → ZH only. Rejected: the team wants Chinese-first authoring (write and review a Chinese Agent Note, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged. - Locale directories (
docs/en/+docs/zh/, the Kubernetes/ECharts model) — rejected: this repo has no docs-site framework to map locales to routes, moving every English file would churn every existing cross-reference, andverify-md-links/verify-doc-refswould need path-mapping logic instead of working unchanged. - A separate translation repo (the PingCAP
docs/docs-cnmodel) — rejected: right for a docs product with independent release trains, overkill for a monorepo's own documentation; it also puts the translation outside the reach of this repo's gates. - Interleaved bilingual files (single file, both languages) — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial inconsistency invisible.
- Commit-hash records (the MDN
l10n.sourceCommitmodel) — rejected in favor of blob hashes: a same-PR edit has no commit hash yet, so the MDN model cannot express "consistent as of the state this PR introduces", and verifying it requires git history instead of file content. - Comparing git timestamps of the pair (no record) — rejected: formatting-only edits would false-positive, and a counterpart committed after an unrelated edit would false-negative; content identity is the only signal that means what the gate claims.
Industry precedent
Paired sibling files with locale suffixes are the dominant Chinese big-tech convention (ant-design index.zh-CN.md/index.en-US.md; arco-design README.zh-CN.md with a top-of-file switcher; Apache ShardingSphere's 387 .cn.md/.en.md pairs) — but none of those repos enforce pairing or consistency in CI; the convention holds by review alone. Consistency automation exists outside China: MDN's l10n.sourceCommit front-matter fingerprint, Vue's Ryu-Cho action (upstream-commit watcher that opens issues/PRs for stale translations), Kubernetes' localization drift scripts, and Microsoft's Azure co-op-translator (source-hash-driven LLM re-translation in CI). This design combines the two: the Chinese-ecosystem file layout with a hash-pair gate, plus an agent-run workflow in place of a bot service.
Consequences
- Editing either side of a paired document obligates the same PR to update the counterpart and re-record the pair — the gate makes the doc-sync rule bilingual, and CI (not reviewer memory) carries the invariant.
- Every pair adds a third file to the tree. The record is machine-written (
--write), so the cost is directory noise, not maintenance effort; in exchange, "who confirmed these consistent, and when" is answerable from git blame on the yaml. - When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.
- Generated English documents remain derived from source and freshness-gated by their owning generators. A generated page with a reviewed Chinese counterpart participates in the three-file pairing workflow, with one structural exception: the generated English source has no language switcher because adding one would make the generator stale, while the Chinese counterpart links back to it. Generated pages without a reviewed counterpart remain explicit exclusions and use an English website projection.
- The exclusions-only manifest makes every current and future in-scope document mandatory through the same path. There is no explicit requirement, cutoff, or class entry that can fall outside discovery while appearing enforced.
- The recorded hashes double as the update tool: gen-translation-brief recovers either side's last-confirmed text from them and assembles the minimal-update briefing, so re-translation of whole files is never forced by the mechanism.
中文
问题
本仓库的文档语料会被公司内外的人和 agent(智能体)以中英两种语言阅读。在没有机制的情况下纯靠手工维护第二语言,正是译文腐烂的根源:一侧持续演进,另一侧默默失实,而没有门禁能够发现。对于这类不变式,本仓库一贯的做法是将其编码为机械检查(见质量门禁与 doc-sync(文档同步门禁)强制),因此双语政策随附一道门禁一起交付。
决策
- 配对兄弟文件,两种语言同权。 一对文档由三个兄弟文件组成:英文
foo.md、中文foo.zh.md,以及一份一致性记录foo.i18n.yaml。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 docs/i18n/README.md;翻译规则见 docs/i18n/translation-rules.md;术语真源见 docs/i18n/terminology.md。 - 伴随记录保存两侧 blob hash,使一致性可检查。
foo.i18n.yaml保存两侧文件在上一次确认一致时各自的完整 Git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(verify-translation-pairing --write <pair>,要求点名所确认的配对;批量重新记录是显式的--write --all)会产生一份可评审的 YAML diff:确认一致在 PR 中是一个显式、可见的动作。 verify-translation-pairing加入doc-sync。 门禁(scripts/verify-translation-pairing.ts)强制执行以下规则:每个已发现且未排除的源文档都有完整配对;每个现有配对都完整(三个文件齐全)且一致(两侧的 hash 均与记录匹配、中文侧和所有人工撰写的英文源都带语言切换行而清单内的生成英文源除外、结构签名一致);被排除的生成文档、指令文档或本身即双语的文档不得配对。目标属于该活跃语料的相对文档链接使用与源文件 locale 相同的目标兄弟文件;结构签名则把.md与.zh.md兄弟文件规范化为同一个语义目标,并保留完全相同的 query/fragment 后缀;该细化规则由双语文档链接本地化决策负责。scripts/translation-pairing.manifest.json 只包含显式排除项,因此任何要求都无法绕过发现流程而接受较弱的检查。只有当.zh.md围栏序列与其无后缀兄弟文件拥有顺序相同、正文按字节一致的同一组受跟踪围栏时,面向源码的代码门禁才会将其作为派生内容消费;不完整、顺序变更、重分类或已改动的序列仍会独立受检,因此由其所属的代码门禁或配对门禁报告不匹配。- 全语料统一要求。 范围内的每篇文档从创建起就必须有完整配对;政策没有逐文件推进状态、日期分界或 README 专用类别。README 发现会覆盖 vendor 源码、依赖目录与被忽略的构建产物目录之外所有文件名不区分大小写匹配 README 的文件,包括今后新增的顶层目录。发布到文档站的配对使用
pairedPages(),由根 locale 投影.zh.md,由/en/投影.md;仅创建对侧文件并不会发布它。 - 配对记录是元数据,而不是 Cordis Loader 配置。 Cordis 配置发现会接受实际的
.cordis.yml和.cordis.yaml文件,同时排除*.i18n.yaml,即使文档名中包含cordis也不例外。这样既能继续校验可执行的 Loader 配置项,又不会把翻译 hash 当作配置来解析。 - 翻译是 agent 的工作,由人评审。 常规改动采用由轻量翻译决策确立的直接单遍路径。扩展翻译 skill(技能)保留委派翻译和其他较重机制,供用户显式调用;两条路径均以文档契约为真源。
验证
验证约定分别覆盖每个边界。verify-translation-pairing 固定配对完整性、hash、语言切换行和结构;project-doc-site.spec.ts 固定已发布配对按 locale 选择对应源文件;cordis-config-files.spec.ts 固定 Loader YAML 的发现以及翻译记录的排除;翻译提示词可运行快照则固定渲染后的系统消息、五对经评审的示例、源请求和所消费的响应。这些检查共同使配对漂移、发布漂移、配置误分类和模型可见提示词漂移都可在评审中看见。
曾考虑的替代方案
- 英文为正典源、指纹放在译文内:
.zh.md文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。否决:团队需要中文先行的撰写方式(先写、先审中文 Agent Note,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖两侧的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。 - 语言目录(
docs/en/+docs/zh/,Kubernetes/ECharts 模式):否决。本仓库没有将 locale 映射到路由的文档站框架;如果移动所有英文文件,所有既有交叉引用都要随之修改;且verify-md-links/verify-doc-refs将需要路径映射逻辑,而非原样工作。 - 独立翻译仓库(PingCAP
docs/docs-cn模式):否决。适合有独立发布节奏的文档产品,对 monorepo 自身的文档而言过重;还会把译文置于本仓库门禁触及不到的地方。 - 中英混排单文件(一个文件、两种语言):否决。每个 diff 都翻倍,破坏一段一行约定的 diff 易读性,且局部不一致不可见。
- Commit hash 式记录(MDN
l10n.sourceCommit模式):否决,改用 blob hash。同一个 PR 内的改动还没有 commit hash,MDN 模式无法表达「与本 PR 引入的状态一致」,且校验它需要 git 历史而非文件内容。 - 比较配对两侧的 git 时间戳(无记录):否决。纯格式化的改动会误报,一次无关改动之后提交的对侧文件会漏报;只有内容同一性这个信号才与门禁的承诺名实相符。
业界先例
带语言后缀的配对兄弟文件是中国大厂的主流约定(ant-design 的 index.zh-CN.md/index.en-US.md;arco-design 的 README.zh-CN.md 加顶部切换行;Apache ShardingSphere 的 387 对 .cn.md/.en.md),但这些仓库都没有在 CI 中强制配对或一致性检查;约定纯靠评审维系。一致性自动化存在于中国以外:MDN 的 l10n.sourceCommit front-matter 指纹、Vue 的 Ryu-Cho action(监视上游 commit,为陈旧译文自动开 issue/PR)、Kubernetes 的本地化漂移脚本、微软 Azure co-op-translator(CI 中由源 hash 驱动的 LLM 重译)。本设计将两者结合:中文生态的文件布局,加上 hash 配对门禁,再加一个由 agent 运行的工作流替代 bot 服务。
后果
- 修改已配对文档的任一侧,同一个 PR 就有义务更新对侧并重新记录配对。门禁将 doc-sync 规则双语化,不变式由 CI(而非评审者的记忆)承载。
- 每个配对给目录树多添一个文件。记录由机器写入(
--write),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。 - 两侧说法冲突时,没有机械规则裁决谁赢,由 PR 评审裁决。这是同权的代价,且是有意接受的:另一个选项(正典语言)会禁止中文先行撰写。
- 生成的英文文档仍由源码派生,并由各自的生成器实施新鲜度门禁。有经评审中文对侧的生成页面遵循三文件配对工作流,但有一项结构例外:生成的英文源文件不含语言切换行,因为添加该行会使生成器新鲜度检查失败;中文对侧仍链接回英文源。没有经评审对侧的生成页面保留为显式排除项,并在网站上投影英文。
- 只含排除项的 manifest(元数据清单)通过同一路径,要求当前及今后纳入范围的每篇文档都必须配对。不存在显式要求、分界或类别条目可以落在发现范围之外,却看似已经强制执行。
- 记录的 hash 兼作更新工具:gen-translation-brief 会从中还原任一侧上次确认的文本并组装最小更新简报,因此这套机制从不强迫整篇重译。