
Helix、docs、l10n这三个词凑到一起加上连字符就是我这一年里在GitHub上维护的项目名helix-docs-l10n。实际做的事情不复杂——把Helix编辑器的官方文档圈内人都叫docs本地化成中文。Helix这个Rust社区出品的终端编辑器这几年热度和Neovim打得有来有回功能上有不少新思路比如selection-first的多光标模型、内置LSP、Tree-sitter高亮。但官方文档只有英文一个版本对很多刚接触的用户来说光安装完想调出自己的第一份config就能卡半天。我做这个项目就是把docs翻译成中文把这扇门掰开一点。这篇文章的读者我设想有三类人一类是给开源项目做过i18n、想了解l10n落地的开发者一类是想在团队里维护一套中英文双语文档的工程师还有一类是自己也在犹豫要不要给某个技术文档做汉化的人。下面我会把项目定位、技术选型、完整的流水线搭建过程、翻译中的细节坑还有持续维护的节奏全部摊开讲。算是给同类项目留一份能照着抄的作业。1. 项目定位与核心思路1.1 为什么盯上Helix的文档Helix的文档质量在终端编辑器里属于第一梯队。mdBook生成的静态站结构清晰有键盘映射、配置说明、LSP接入、主题定制这些章节每个章节都是可操作的。越是这样越值得本地化——因为用户不是去读小说而是去查“到底怎么写config.toml才能让rust-analyzer跑起来”。但Helix的文档有个特点更新极快。快捷键版本之间会有调整内置命令列表在变语言配置的说明也在跟着上游Tree-sitter和LSP生态迭代。这意味着本地化不能做成“一次性翻译”——从官方仓库fork一份Markdown翻译完放那儿过两个月官方文档改动几十处你这边的版本就废了。所以项目一开始我就把“持续跟上上游”定位成核心问题而不是“把20个章节翻译完”。有人会问用机器翻译整站过一遍不更快吗确实快但机器翻译的结果没法保证术语统一更没法跟着上游更新做精准merge。比如“selection”这个核心词机器可能一会儿翻成“选择”一会儿翻成“选中区域”读者看完一头雾水。l10n的价值恰恰在于“有人对术语做长期统一管理”。1.2 为什么标题叫l10n而不是i18ni18n是国际化l10n是本地化。这两个词经常被混着用但在实际工程里分工完全不同。i18n是给软件本体设计多语言框架抽出键值对、做复数规则、设计字体布局。l10n是翻译具体内容把文案、把文档、把帮助手册变成目标语言。helix-docs-l10n这个项目不碰Helix二进制本身不往Rust代码里加任何多语言框架只处理book/src下的Markdown文档。所以名称里很明确是l10n。这个区别也决定了项目边界。翻译出来的中文文档和官方英文文档用同一个mdBook渲染引擎、同一套主题只是语言不同。界面里的命令名、快捷键、配置键全部保留原文因为终端里跑的是同一个Helix命令还是那个命令。1.3 目标用户和项目边界把这个项目做得“能给人用”比“翻译完”要难得多。我定义的目标用户是中文母语、英文阅读有障碍或不想读英文的Helix新手以及已经在用但想看文档确认某个快捷键的中阶用户。项目提供的不只是中文版的HTML页面还提供zh-CN.po翻译文件方便其他想做本地化的人参考工作流。边界也要划清楚不翻译GitHub issue不做社区问答翻译不涉及Helix release notes之外的杂项内容。文档是经过官方审校的单一事实来源输入规模可控质量也统一。把边界立好后面才不会项目做着做着变成一锅粥。2. 文档体系与技术选型2.1 官方文档结构和mdBook机制搞清楚要翻译的对象长什么样比选工具更重要。Helix官方文档是mdBook工程源文件在book/src目录配置文件是book.toml入口是src/SUMMARY.md。mdBook用SUMMARY.md生成左侧目录树再配合右侧正文页支持全文搜索、主题切换和代码高亮。目录结构大致这样book/ ├── book.toml └── src/ ├── SUMMARY.md ├── intro.md ├── installation.md ├── usage.md ├── keymap.md ├── commands.md ├── languages.md ├── themes.md ├── hooks.md └── ...每个Markdown文件就是一个页面。页与页之间用相对路径链接比如[安装指南](./installation.md)。代码块量大、行内代码尤其多这是翻译时最需要留意的地方。mdBook还支持{{#include}}语法嵌入同一仓库的其他文件这类内容会被原样渲染。本地化这类文档真正要处理的是三层东西正文文本、代码块、链接与锚点。正文可以翻译代码块基本不能动链接只能改显示文字不能改跳转目标。mdBook的架构决定了这三者会被区分对待所以选工具时必须考虑它能不能按这种粒度提取字符串。2.2 三条路线直接翻译、PO工作流、专业平台动手前我对比过三种方案这里用表格说清楚方案优点缺点适合场景fork一份直接翻Markdown简单直接改完就能看见效果上游更新无法合并手工diff是噩梦一次性文档比如某个固定版本文档PO文件 gettext工作流与源文件分离msgmerge自动同步上游变更纯文本可git diff前期工具链搭建有成本PO文件阅读性差长期维护、高频更新的项目文档Crowdin / Weblate平台协作界面友好贡献者不用理解git引入第三方依赖有免费额度限制数据在云端多人协作、翻译量大的主流项目我选了PO文件工作流原因就一个Helix文档以周为单位在变。直接翻译Markdown等于把上游仓库变成dead fork每次上游改动都要人工把几十个文件重新diff一遍这不是人力能扛的。PO文件把“原文”和“译文”分开存上游改动后一条msgmerge就能把新变更合并进翻译底稿已经翻译的条目自动保留只有真正改动的原文会被标记为fuzzy等你重新确认。这个机制相当于把“持续跟上源码”这件事从体力活降级成“每天花十分钟处理merge结果”。对个人维护者来说这才是l10n项目能活得下去的关键。2.3 工具链全景和安装最终敲定的工具链是这三样gettextPOSIX老牌翻译工具链提供msginit、msgmerge、msgfmt等命令是PO文件的标准处理工具。mdbook-i18n一个专门给mdBook做多语言的工具能从mdBook源目录提取PO文件也能把翻译后的PO回填成mdBook能构建的Markdown。mdbook官方文档的渲染引擎负责把翻译后的Markdown生成静态站点。安装命令根据不同系统有差别我自己的环境是macOSbrew install gettext mdbook cargo install mdbook-i18nLinux上对应的是apt install gettext mdbookmdbook-i18n同样用cargo装。这套组合不依赖任何在线翻译平台数据都是本地文件git就能管理离线也能干活。有人问为什么不用po4a。我也试过po4a在普通Markdown上表现不错但mdBook的SUMMARY.md和{{#include}}这种扩展语法po4a解析起来不是百分百合拍偶尔会把不该提取的代码块提取出来。mdbook-i18n生来就是干这个的对mdBook语法的理解更准确所以后来主力换成了它。3. 实操搭建翻译流水线3.1 仓库结构的设计整个项目放在独立仓库里不fork官方仓库的完整历史避免被上游的commit历史污染。目录结构如下helix-docs-l10n/ ├── book/ │ ├── book.toml │ └── src/ # 由同步脚本从上游拷贝的英文源文档 ├── i18n/ │ ├── zh-CN.po # 核心翻译文件 │ └── glossary.md # 术语表 ├── scripts/ │ ├── sync-upstream.sh │ ├── build-zh.sh │ └── check-links.sh └── .github/workflows/ └── nightly-sync.ymlbook目录是构建的中间产物也是mdbook-i18n的输入。i18n目录存放所有翻译状态文件。scripts放自动化脚本。GitHub Actions负责定时从上游拉文档。这个结构不是花架子——把源码、翻译、构建脚本分开每个目录的责任就清楚了。后面加人可以只改i18n目录不需要理解构建细节。3.2 提取可翻译字符串第一次搭流水线时我先手动把上游的book/src拿到本地git clone https://github.com/helix-editor/helix.git mkdir -p l10n mv helix/book l10n/然后让mdbook-i18n扫描源文档并生成PO文件cd l10n mdbook-i18n extract \ -s book/src \ -o ../i18n/zh-CN.po \ -l zh-CN生成的PO文件是个纯文本结构长这样msgid msgstr Project-Id-Version: helix-docs-l10n\n Content-Type: text/plain; charsetUTF-8\n Language: zh_CN\n msgid Installation msgstr 安装翻译的主要工作就是给每条msgid填上对应的msgstr。行号注释标在哪一句来自哪个文件的哪一行方便回查。提取完成后整个英文文档的可翻译字符串就变成了几千条msgid接下来才是真正的翻译体力活。3.3 翻译、构建、本地预览我翻译PO文件用的编辑器恰好就是Helix本身。打开zh-CN.po一条条往下走光标移动和区块选择在这种结构化文本上特别顺手。这里有个小技巧翻译时不要一次性把所有条目都清空而是按章节走一章翻完先跑一次构建确认这一章的页面渲染正常再继续。翻译完成后需要把PO回填到Markdown里生成中文源文档mdbook-i18n build \ -s book/src \ -l zh-CN \ -o book-zh/src这会在book-zh目录下生成一套翻译过的Markdown。我再用mdBook构建静态站点cd book-zh mdbook build然后本地起一个HTTP服务预览python3 -m http.server 8080 --directory book-zh/book浏览器打开localhost:8080逐页检查。这一步必须手动过一遍重点页面包括首页、快捷键表、配置页。构建后的中文站搜索功能也是可用的因为mdBook的索引基于构建结果生成目录结构完整的话搜索体验和英文站没有区别。3.4 上游同步让机器先跑Helix上游仓库几乎每周都有新commit。同步流程我写成了脚本放在scripts/sync-upstream.sh里核心逻辑是#!/usr/bin/env bash set -euo pipefail UPSTREAM_URLhttps://github.com/helix-editor/helix.git WORKDIR.upstream rm -rf $WORKDIR git clone --depth 1 $UPSTREAM_URL $WORKDIR rsync -a --delete $WORKDIR/book/src/ book/src/ rm -rf $WORKDIR同步完英文源文件后重新提取新的POmdbook-i18n extract -s book/src -o /tmp/upstream.pot -l zh-CN msgmerge --previous \ --lang zh-CN \ -o i18n/zh-CN.po \ i18n/zh-CN.po \ /tmp/upstream.potmsgmerge是这一步的核心。它拿最新的pot文件跟现有po文件做合并原文没变的条目译文原样保留。原文有改动的条目标记为fuzzy要求人工重新确认。新增的章节自动追加到文件尾部。上游删除的内容从翻译中移除。这样每次同步不管上游变更多大真正需要手工处理的只有“fuzzy 新增”这部分通常几条到几十条。相比一次性手动翻译工作量少了两个数量级。我用GitHub Actions每周日凌晨触发一次同步自动生成Pull Request在PR里把fuzzy条目处理掉merge后自动触发构建部署。这套机制跑了几个月上游的几十次变更都是小步跟进从来没有手忙脚乱过。4. 翻译质量与细节处理4.1 术语表先把词定下来翻译质量崩坏的最常见原因不是句子翻不对而是同一个词在不同章节里翻成了不同的词。Helix的核心概念尤其容易踩坑我开了一个glossary.md把高频术语统一起来英文术语统一译法说明buffer缓冲区保持与Vim社区译法一致selection选区Helix的selection模型不能译成“选中”register寄存器沿用通用译法picker选择器打开文件/命令的浮动列表multiple cursor多光标与行业术语一致command mode命令模式normal mode统一译为普通模式runtime运行时HELIX_RUNTIME指运行时目录这份术语表不只给翻译者看审校PR时也拿它做依据。如果有人把“selection”翻成“选择”可以直接引用术语表要求修改省去来回争论。术语表本身就是项目资产翻译越多越值钱。4.2 Markdown链接与锚点不能碰这是新手最容易犯的错。Markdown链接一旦翻译跳转目标整站导航就废了。比如原文查看[安装指南](./installation.md)了解更多。翻译时应该保留./installation.md查看[安装指南](./installation.md)了解更多。显示文字可以改成中文跳转路径必须原文保留。页内锚点同理#configuration这种锚点引用和定义两处都不能改。mdBook对链接校验很严格改坏任何一个链接构建时就会报Warning甚至404。我写了个check-links.sh构建后用linkchecker扫一遍所有内部链接确保没有失效锚点。这个脚本挂在CI里每次PR都会跑。4.3 代码块和行内代码的原则Helix文档里代码块的占比相当高。config.toml的配置示例、languages.toml的语言定义、Shell命令、LSP配置……这些内容大多是要复制到用户终端里实际运行的。我的原则是代码块内容一个字节都不翻译。代码块里的注释也一样比如# 设置默认语言 [keys.normal] x delete_selection我会保留英文注释或干脆不翻。原因很简单代码块翻译后用户复制贴进自己的config里就会得到跟文档不一致的结果。尤其配置分组名、命令名、键位名这些机器可读的键绝对不能动。PO提取工具通常已经做了代码块跳过但翻译者不能因此放松——任何手工编辑操作都可能把代码块带进译文。行内代码同理ESC就是ESC:w就是:wHELIX_RUNTIME不改成“运行时目录”。这是l10n项目里最硬的一条纪律。4.4 构建后的验证与发布每次发布新版本我固定跑三条验证链接完整性用linkchecker扫全站确认没有404。术语一致性用grep检查关键术语的译法是否统一比如全站不能出现“选择区域”和“选区”混用。实际试用在本地打开中文站把安装、配置、打开文件这一条完整路径走一遍。验证通过后再触发GitHub Pages部署。这个节奏配合上游同步基本上每周都能发布一个新版。发布记录我用CHANGELOG形式简单记内容包括同步的上游commit hash、翻译变更点、重置了多少条fuzzy。5. 常见问题速查与排查实录5.1 问题速查表把踩过的坑整理成一张表给你省点时间现象根本原因解决办法构建后搜索框搜不到中文mdBook构建时索引基于源语言确保mdbook build在book-zh目录运行部分页面404链接或锚点被翻译了恢复链接为原文路径用linkchecker排查PO文件里出现大量fuzzy上游对某一段做了大改用msgmerge --previous减少误标逐条确认翻译后的页面出现英文代码块代码块未被提取工具跳过检查po文件里是否包含代码段msgid重新extract中文文字显示为方框生成时编码声明丢失检查PO头部Content-Type的charsetUTF-8mdbook-i18n构建产物为空源Markdown目录结构错误确认src/SUMMARY.md存在且结构与源目录匹配5.2 三个真实排查记录第一个是乱码问题。最开始我本机生成的HTML在浏览器里显示方框查了半天发现是PO文件的头字段没有charsetUTF-8msgfmt把字符串按默认编码处理了。修复方式是在PO文件头部补上Content-Type: text/plain; charsetUTF-8\n重新生成后问题消失。第二个是锚点问题。我翻译到keymap.md时原文里有个#normal-mode锚点我把“normal mode”翻成“普通模式”顺手把锚点也改了。构建时所有指向这个锚点的链接全部404。最后靠git diff找回来花了一整天。从那以后“链接锚点绝不翻译”被写进了CONTRIBUTING。第三个是fuzzy误判。上游只是给文档加了一句前置说明但改动触发了PO里的整段markup变化导致二十几条译文被标记成fuzzy。当时还不熟悉msgmerge --previous只能手工一条条看。后来脚本里加上这个参数fuzzy误判率明显下降绝大多数被标fuzzy的条目译文其实都还能用只需一眼确认。6. 开源协作与维护节奏6.1 怎么让翻译者愿意参与个人翻译完几千条msgid不现实这个项目必须走社区路线。为了让别人愿意参与我把麻烦挡在前面写了详细的CONTRIBUTING.md包括如何安装工具链、如何提取PO、如何构建预览。采用“认领制”在issue里列出待翻译章节贡献者认领一个文件翻译完成后提PR。PR要求一个翻译者负责改一个审校者负责看两者角色不能是同一人。术语表放在最显眼的位置PR模板里附带“我是否使用了统一术语”的检查项。实际效果不错不少贡献者第一次提交时甚至不知道PO是什么格式但只要README写得清楚按步骤走一遍也能完成一个章节的翻译。6.2 CI自动化把重复劳动交给机器项目的CI由两个workflow组成name: nightly-sync on: schedule: - cron: 0 2 * * 0 jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Run sync run: ./scripts/sync-upstream.sh - name: Create PR run: ./scripts/create-pr.sh这个workflow每周日凌晨同步上游并自动生成PR。另一个workflow在PR合并后触发构建部署。CI的价值在于把“最机械、最容易忘”的同步工作完全自动化维护者只需要在PR里确认fuzzy条目人的精力只花在真正需要判断的翻译决策上。6.3 我的维护节奏最后分享一下这个项目实际的运转日历也算给想长期维护l10n的人一个参考每周一次上游同步PR花10到30分钟处理fuzzy和新增条目。每两周检查一次待认领issue处理贡献者提问。每月完整构建并全站检查一遍写CHANGELOG。每个版本跟随Helix发版节奏把文档变更和翻译同步一起处理。这套节奏坚持下来项目不会变成一锤子买卖也不会变成压倒人的负担。翻译本身只是整个项目里最不需要动脑的部分真正决定l10n项目生死的是同步机制和术语管理。做helix-docs-l10n这段时间我最深的体会是如果你也想给某个高频更新的开源项目做文档本地化先别急着打开编辑器翻第一章先花两天时间把PO工作流和自动同步搭好。等机器开始替你追上游、你只需要在PR里做判断的时候这个项目才算真正活着。另外还有一个小技巧初期不要贪多先只翻installation和keymap两个章节发出去让用户先用起来有问题及时反馈。用户反馈回来的“哪里看不懂”比你自己猜“该翻哪一章”要准得多。翻译永远可以迭代但方向一开始就要对。helix-docs-l10n走到今天靠的并不是谁的翻译水平有多高而是把更新、反馈、修正这条循环跑顺了。