Establish EN->ZH bilingual documentation for the README and docs tree: - docs/i18n/README.md — the pairing contract: sibling foo.md <-> foo.zh.md, English canonical, blob-hash source fingerprints, language switchers, scope/exclusions, and a manifest-driven rollout ratchet. - docs/i18n/translation-rules.md — how to translate: faithfulness, structure preservation, terminology discipline over docs/i18n/terminology.md, and typography rules grounded in MDN/K8s/Vue/clreq conventions. - .agents/skills/dsh-translate-docs — the committed agent workflow, following the dsh-code-review pattern of deferring to docs as sources of truth. - scripts/verify-translation-pairing.ts + manifest — a doc-sync gate: required pairs exist; every existing .zh.md is fresh (fingerprint = current source blob), switcher-linked, structure-matched, and non-orphaned; excluded (generated) docs stay unpaired. --list prints the translation work list. - RFC (implemented/process) recording the decision and the alternatives. - Dogfood: README.zh.md and the two i18n docs translated under their own rules. Gates: doc-sync green including the new gate; red/green proven for stale fingerprint, orphan, and excluded-file violations.
5.2 KiB
Bilingual documentation via paired sibling files and a pairing gate
Context
This repo's README and docs tree are 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: the English file moves on, the Chinese file 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, English canonical. The translation of
foo.mdisfoo.zh.mdin the same directory; English is the only authoring language and translation flows EN → ZH. Policy: docs/i18n/README.md; translation rules: docs/i18n/translation-rules.md; terminology source of truth: docs/i18n/terminology.md. - A blob-hash fingerprint makes freshness checkable. The first line of every
.zh.mdrecords the repo-relative path and the first 12 hex digits of the git blob hash of the English source it renders. Staleness is then a pure content comparison — no history lookup — and the hash is computable for a source edited in the same PR, which a commit-hash fingerprint (the MDNl10n.sourceCommitmodel) is not. verify-translation-pairingjoinsdoc-sync. The gate (scripts/verify-translation-pairing.ts) enforces: required pairs exist, every existing translation is fresh/switched/structure-matched/non-orphaned, and excluded (generated or bilingual-by-construction) files stay unpaired. Therequiredlist in scripts/translation-pairing.manifest.json is a ratchet: each merged translation batch adds its files, so coverage only grows.- Translation is agent work with human review. The committed workflow is .agents/skills/dsh-translate-docs, following the same pattern as dsh-code-review: the skill carries the workflow and defers to the docs as sources of truth.
Alternatives considered
- 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 staleness invisible.
- Commit-hash fingerprints (MDN
l10n.sourceCommit) — rejected in favor of blob hashes: a same-PR source edit has no commit hash yet, so the MDN model cannot express "translated against the version this PR introduces", and verifying it requires git history instead of file content. - Comparing git timestamps of the pair (no fingerprint) — rejected: formatting-only English edits would false-positive, and a translation committed after an unrelated English 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 freshness in CI; the convention holds by review alone. Freshness 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 fingerprint gate, plus a committed agent skill in place of a bot service.
Consequences
- Editing an English doc that has a
.zh.mdsibling obligates the same PR to update the translation — the gate makes the doc-sync rule bilingual, and CI (not reviewer memory) carries the invariant. - Generated docs (
cordis-catalog/,tool-catalog/,module-graph.md) are never paired; their generators emit English only, and the gate rejects a stray translation of them. - Rollout is incremental by design: documents outside
requiredare visible backlog (--list), not red CI, so translation lands in reviewable batches without a big-bang PR. - The fingerprint doubles as the update tool (
git cat-file -p <hash>recovers the exact translated-from text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism.