本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2026年09月09日 统计字数: 8500字 阅读时间: 17分钟阅读 本文链接: https://soulteary.com/2026/09/09/phorge-modernization-part-17-restore-simplified-chinese.html ----- # Phorge 现代化改造实战(十七):恢复简体中文支持,用工具持续维护 2.7 万行翻译文件 本文是“Phorge 现代化改造实战”系列第十七篇。上一篇处理了数据库诊断的跨仓契约;这一篇回到用户直接看到的界面,把旧版简体中文翻译迁入当前 fork,并补上后续更新需要的维护工具。 ## 系列导航 1. [从没有官方镜像到 Docker Compose 跑起来:建立最小可运行基线](https://soulteary.com/2026/09/06/phorge-modernization-part-1-from-no-official-image-to-docker-compose.html); 2. [改进容器化的七个细节:补齐权限、持久化、依赖和探活](https://soulteary.com/2026/09/06/phorge-modernization-part-2-seven-containerization-details.html); 3. [接入 Stargate:把 Forward Auth 的信任边界做完整](https://soulteary.com/2026/09/06/phorge-modernization-part-3-stargate-forward-auth-trust-boundary.html); 4. [拆分模块到 Gorge:无侵入改造不等于不碰文件](https://soulteary.com/2026/09/06/phorge-modernization-part-4-split-modules-to-gorge.html); 5. [替换 diff 子进程:兼容不等于逐字一致](https://soulteary.com/2026/09/07/phorge-modernization-part-5-replace-diff-subprocess.html); 6. [替换实时通知服务:为什么 HTTP 501 反而表示正常](https://soulteary.com/2026/09/07/phorge-modernization-part-6-replace-realtime-notification-service.html); 7. [迁移邮件服务:先分清哪些失败不该重试](https://soulteary.com/2026/09/07/phorge-modernization-part-7-migrate-mail-service.html); 8. [迁移搜索服务:写进索引不等于搜得到](https://soulteary.com/2026/09/07/phorge-modernization-part-8-migrate-search-service.html); 9. [迁移文件存储:写得进去也要读得回来](https://soulteary.com/2026/09/07/phorge-modernization-part-9-migrate-file-storage.html); 10. [迁移 Webhook 投递服务:先解决重复投递](https://soulteary.com/2026/09/07/phorge-modernization-part-10-migrate-webhook-delivery.html); 11. [升级 Gorge 的 HTTP 框架:接口没变,行为也不能变](https://soulteary.com/2026/09/08/phorge-modernization-part-11-upgrade-http-framework.html); 12. [兼容 Elasticsearch 5、6、7:版本配置决定整个索引结构](https://soulteary.com/2026/09/08/phorge-modernization-part-12-elasticsearch-version-compatibility.html); 13. [联调六项外部服务:容器在运行,不代表业务已经切换](https://soulteary.com/2026/09/08/phorge-modernization-part-13-integrate-six-external-services.html); 14. [为 Go 服务建立统一的 Phorge API 入口:网关可以换框架,Conduit 协议不能变](https://soulteary.com/2026/09/08/phorge-modernization-part-14-unified-conduit-api-gateway.html); 15. [用 Go 接管 Phorge 工作队列:如何避免新旧消费者互相抢任务](https://soulteary.com/2026/09/08/phorge-modernization-part-15-take-over-work-queue-with-go.html); 16. [把数据库自省搬进 Gorge:跨仓契约不能靠手工同步](https://soulteary.com/2026/09/09/phorge-modernization-part-16-database-introspection-in-gorge.html); 17. **恢复简体中文支持:用工具持续维护 2.7 万行翻译文件。** ## 写在前面 Phorge 默认使用英文,上游仓库也没有提供完整的简体中文界面。我以前维护过一份中文翻译,这次把它迁入当前 fork:项目新增 `zh_CN` 语言定义和约 2.7 万行、上万个词条的翻译表,用户可以在个人设置中选择“中文(简体)”。默认语言仍是 `en_US`,现有安装升级后不会自动切换。 旧翻译文件解决了第一批中文文案,后续维护还有两个问题。 Phorge 持续跟进上游,新功能会带来新的界面字符串;存量译文中也可能留着英文占位、中英混排或不统一的术语。单靠人工在一个两万多行的 PHP 文件里查找,很容易漏掉新增内容,也很难安全地分批修改。 这次一起迁入了 `scripts/i18n/` 下的维护工具。它们负责查找缺失字符串、补入翻译键、筛选未完成译文、调用在线模型、保护格式占位符和保存处理进度。以后同步上游时,只需处理新增和未完成的部分。 ## 一、把翻译文件放进仓库,还要让运行时找到它 升级到包含中文功能的版本后,用户进入 **Settings → Account → Translation**(切换后显示为“翻译”),选择“中文(简体)”并保存,页面就会使用 `zh_CN`。没有中文译文的字符串继续回退到 `en_US`,不会影响页面渲染。 这条看似简单的切换,涉及到四个环节: | 环节 | 相关文件 | 作用 | | ------- | ------------------------------------------------------------------ | --------------------------------------- | | 语言和翻译数据 | `PhabricatorChineseLocale.php`、`PhabricatorChineseTranslation.php` | 定义 `zh_CN`、中文名称、英文回退与翻译映射 | | 类注册 | `__phutil_library_map__.php` | 让 Phorge 可以加载新增的 Locale 和 Translation 类 | | 设置与运行时 | `PhabricatorTranslationSetting.php`、`PhabricatorEnv.php` | 在语言下拉中显示中文,并在请求中加载中文翻译 | | 国际化校验 | `PhorgeInternationalizationValidator.php` | 避免校验工具把仓库自带的 `zh_CN` 判成未知语言 | 运行时加载必须单独接线。中文类由当前 Phorge 仓库提供,`libphutil` 的 `loadAllLocales()` 和 `loadLocale()` 无法自动发现它们。类映射只解决 PHP 自动加载,语言设置页和请求运行时仍有各自的发现逻辑。 当前实现先在设置页显式加入 `zh_CN`,保存时也允许这个值;请求开始后,`PhabricatorEnv::setLocaleCode()` 直接创建中文 Locale 和 Translation: ```php if ($locale_code === 'zh_CN') { $locale = new PhabricatorChineseLocale(); $translation = new PhabricatorChineseTranslation(); $translations = $translation->getTranslationMap(); } else { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); } ``` 代码还会把连字符改成下划线,并将 `zh-CN`、`zh_Hans`、`zh_Hant` 等 `zh_*` 语言代码归一为 `zh_CN`。这能兼容已有的中文设置,但目前也会让繁体中文使用简体译文。将来加入独立繁体翻译时,需要拆分这条归一规则。 ## 二、缺少翻译键和译文未完成,是两类问题 维护翻译时,先把缺口分成两类: | 状态 | 翻译表中的表现 | 用户看到的结果 | | ----- | ----------------- | --------- | | 缺少翻译键 | 找不到对应的英文源字符串 | 回退显示英文 | | 译文未完成 | 值与键相同,或中文中保留了大段英文 | 显示英文或中英混排 | `check_zh_missing.py` 处理第一类问题。它扫描 `src/` 下 PHP 文件中的静态 `pht('...')` 字符串,与中文翻译表中的键做差集,再按源码中的出现次数排序,高频文案会排在清单前面。 这个检查器是 `bin/i18n extract` 不便运行时的快速补充。它只识别静态的单引号 `pht()` 字符串,以及用 `.` 连接的同类字符串,并默认跳过超过 220 个字符的长文本。动态参数、运行时拼接和复杂 PHP 表达式仍要通过 Phorge 自带的国际化工具和实际界面检查发现。 `improve_zh_batch.py` 与 `translate_zh_api_batch.py` 处理第二类问题。前者使用短语表和前缀规则修正常见短文案;后者筛选“值与键相同”或明显中英混排的条目,交给在线模型翻译。 因此,检查结果中的 `missing=0` 只说明静态扫描发现的键已经进入翻译表。它无法证明每条译文都已经完成,也无法判断术语和上下文是否准确。 ## 三、把维护过程拆成可以重复执行的步骤 `scripts/i18n/` 里有五个脚本,各自负责一段工作: | 脚本 | 用途 | | --------------------------- | --------------------------- | | `check_zh_missing.py` | 对比源码与翻译表,生成缺失键清单 | | `gen_zh_batch.py` | 将缺失键写入翻译表,可使用人工 TSV 映射或简单规则 | | `improve_zh_batch.py` | 用词表和前缀规则处理一部分短文案 | | `translate_zh_api_batch.py` | 调用在线模型处理未翻译和中英混排条目 | | `run_zh_autofill.sh` | 循环执行检查与补键,直到没有缺失或达到轮次上限 | 在线翻译脚本只扫描翻译表中已经存在的条目,因此要先补键,再调用模型。`run_zh_autofill.sh` 可以循环完成前两步,但它不会发起在线翻译请求。 一轮常用流程如下: ```bash # 安装 OpenAI 兼容客户端,并通过环境变量提供密钥 python3 -m pip install openai export MOONSHOT_API_KEY=sk-xxx # 扫描高频缺失字符串 python3 scripts/i18n/check_zh_missing.py \ --top 1000 \ --out resources/i18n-zh-missing-top1000.txt # 将缺失键写入翻译表;规则无法处理时先保留英文值 python3 scripts/i18n/gen_zh_batch.py \ resources/i18n-zh-missing-top1000.txt \ --auto --apply --limit 1000 # 预览候选条目,再分批调用在线模型 python3 scripts/i18n/translate_zh_api_batch.py --dry-run python3 scripts/i18n/translate_zh_api_batch.py --limit 500 # 检查 PHP 语法并重新统计缺失键 php -l \ src/infrastructure/internationalization/translation/PhabricatorChineseTranslation.php python3 scripts/i18n/check_zh_missing.py \ --top 20 \ --out resources/i18n-zh-missing-top20.txt ``` 脚本只把待翻译的界面字符串发给模型接口,本身不访问任何 Phorge 数据库和用户数据。密钥从 `MOONSHOT_API_KEY` 环境变量读取,无需写进脚本或翻译文件,避免不必要的麻烦。 ## 四、在线翻译要控制候选范围和写回结果 在线模型适合批量处理重复文案,但它的输出不能直接覆盖翻译文件。脚本先控制候选范围,再保护格式标记,最后检查返回内容;任何一项不符合要求,这条结果都会被跳过。 ### 只挑未完成的普通字符串 `translate_zh_api_batch.py` 每次只选择两类条目:值与键完全相同的未翻译项,以及带有中文、同时仍保留较多英文的半成品。 ```python if key == value: return True cjk = count_cjk(value) latin_words = count_latin_words(value) if cjk == 0 or latin_words < 3: return False similarity = SequenceMatcher( a=key.lower(), b=value.lower(), ).ratio() return similarity >= 0.60 ``` 这套规则会漏掉少量只夹一两个英文词的半成品,同时可以减少对 PHID、Herald、Differential 等专有名词的误改。绝大多数已经完成的中文条目不会再次入选;执行前仍应先用 `--dry-run` 检查候选范围。 脚本当前处理单行和两行形式的普通字符串。带复数、性别分支的数组翻译仍需结合上下文人工维护,并交给 Phorge 的国际化校验器检查分支和占位符数量。 ### 占位符先打码,写回前再核对 Phorge 文案中经常出现 `%s`、`%d`、`%3$s`、`%%` 等格式符,也会出现 `[Calendar]`、`[File]`、`[Paste]` 这类需要原样保留的方括号标记。模型一旦删除或改写它们,轻则页面参数错位,重则触发 PHP 格式化错误。 脚本会先将这些内容替换成内部标记: | 阶段 | 示例 | | ----- | ----------------------------------------------- | | 英文原文 | `Welcome, %s. Open [Calendar].` | | 发送给模型 | `Welcome, __PH_TOKEN_0__. Open __PH_TOKEN_1__.` | | 还原后译文 | `欢迎,%s。打开 [Calendar]。` | 模型返回后,脚本先检查每个 `__PH_TOKEN_N__` 是否仍然存在,再恢复原标记,并重新比较格式符与方括号标记。缺失、增加或改写都会记入 `skipped_invalid`,该条结果不会写入 PHP 文件。 每条输入还带有唯一的 `[[[id]]]`。脚本按编号匹配译文,返回顺序发生变化也不影响写回;批量处理后仍需抽查译文是否对应原文。 ### 分批写回,并记录下一次从哪里继续 在线翻译默认每轮最多选择 500 条,每个请求发送 90 条。一个批次通过校验后,脚本立即写回翻译文件,再将下一次扫描的行号保存到进度文件。网络中断后重新执行同一命令,会从上次位置继续。 默认进度文件名是 `.progress.json`,也可以通过 `--progress-file` 指定。它只记录 `next_line` 和更新时间,不保存 API 密钥。如果不想在源码目录生成额外文件,可以把进度文件指向临时目录,例如 `--progress-file /tmp/phorge-zh-progress.json`。 大批量插入或删除词条会改变行号,此时用 `--reset-progress` 从头扫描。脚本会重新应用候选规则,运行前可以用 `--dry-run` 查看将要处理的条目。 ### Moonshot 扩展参数通过 `extra_body` 传递 我将目前脚本使用的模型版本进行了升级,当前脚本默认调用 `kimi-k2.6`,使用 `temperature=0.6`、`top_p=0.95` 和 `max_tokens=32768`,同时关闭思考模式: ```python completion = client.chat.completions.create( model="kimi-k2.6", messages=build_messages(batch), temperature=0.6, top_p=0.95, max_tokens=32768, response_format={"type": "text"}, stream=False, timeout=request_timeout, extra_body={"thinking": {"type": "disabled"}}, ) ``` `thinking` 是 Moonshot 的扩展字段,不属于 OpenAI Chat Completions 的标准参数,需要通过 `extra_body` 放入请求体。这个版本添加了参数覆盖功能,如果切换 `--base-url` 或 `--model` 后,应先运行小批次,检查返回编号、占位符、术语和标点是否符合预期,再扩大 `--limit`。 `--temperature`、`--top-p` 与 `--max-tokens` 也可以从命令行覆盖,脚本无需随模型调整反复改代码。 ## 五、验证翻译要同时看语法、覆盖和界面 自动处理结束后,我会检查四个层次: 1. **PHP 语法**:运行 `php -l`,确认两万多行的翻译文件仍能加载; 2. **键覆盖**:重新执行 `check_zh_missing.py`,确认新发现的静态文案已经进入翻译表; 3. **译文质量**:查看在线脚本剩余的候选项,抽查高频词与中英混排内容; 4. **实际界面**:切换到中文,检查导航、表单、错误提示、复数和带占位符的文案,再切回英文确认回退正常。 仓库中的 [`resources/i18n-zh-glossary.md`](https://github.com/soulteary/phorge/blob/main/resources/i18n-zh-glossary.md) 记录了“仓库”“修订”“动态”“策略”等高频词的统一译法,也列出了 PHID、Herald、Differential 等应当保留的产品名。批量翻译可以快速补齐大部分内容,术语和页面语境仍需要人工抽查。 静态检查器无法覆盖动态 `pht()` 参数,在线脚本也不会处理所有复数和性别数组。发布前的界面检查仍然必要,`missing=0` 只能作为覆盖指标之一。 ## 六、生产环境留意 OPcache `PhabricatorChineseTranslation.php` 约有 2.7 万行。多数环境可以直接加载;生产环境启用 OPcache 后,如果实际出现 `Failed to load symbol` 一类告警,可以通过 `opcache.blacklist_filename` 将这份翻译文件加入黑名单。 这项配置只用于处理已经出现的加载问题,无需默认启用。上线前先使用与生产一致的 PHP 和 OPcache 配置切换一次中文界面,再根据日志决定是否调整。 ## 最后 升级后,用户可以按需选择中文,英文默认值保持不变。语言设置页能够找到 `zh_CN`,运行时能够加载翻译表,缺少中文的文案也有明确的英文回退。 以后跟进上游时,维护工作只需围绕新增和未完成的词条展开:扫描差集、补键、翻译、校验。在线模型负责批量生成候选译文,脚本限制修改范围并拒绝格式损坏的结果,人工集中检查术语和具体页面语境。 –EOF