本文是“Phorge 现代化改造实战”系列第十七篇。上一篇处理了数据库诊断的跨仓契约;这一篇回到用户直接看到的界面,把旧版简体中文翻译迁入当前 fork,并补上后续更新需要的维护工具。

系列导航

  1. 从没有官方镜像到 Docker Compose 跑起来:建立最小可运行基线
  2. 改进容器化的七个细节:补齐权限、持久化、依赖和探活
  3. 接入 Stargate:把 Forward Auth 的信任边界做完整
  4. 拆分模块到 Gorge:无侵入改造不等于不碰文件
  5. 替换 diff 子进程:兼容不等于逐字一致
  6. 替换实时通知服务:为什么 HTTP 501 反而表示正常
  7. 迁移邮件服务:先分清哪些失败不该重试
  8. 迁移搜索服务:写进索引不等于搜得到
  9. 迁移文件存储:写得进去也要读得回来
  10. 迁移 Webhook 投递服务:先解决重复投递
  11. 升级 Gorge 的 HTTP 框架:接口没变,行为也不能变
  12. 兼容 Elasticsearch 5、6、7:版本配置决定整个索引结构
  13. 联调六项外部服务:容器在运行,不代表业务已经切换
  14. 为 Go 服务建立统一的 Phorge API 入口:网关可以换框架,Conduit 协议不能变
  15. 用 Go 接管 Phorge 工作队列:如何避免新旧消费者互相抢任务
  16. 把数据库自省搬进 Gorge:跨仓契约不能靠手工同步
  17. 恢复简体中文支持:用工具持续维护 2.7 万行翻译文件。

写在前面

Phorge 默认使用英文,上游仓库也没有提供完整的简体中文界面。我以前维护过一份中文翻译,这次把它迁入当前 fork:项目新增 zh_CN 语言定义和约 2.7 万行、上万个词条的翻译表,用户可以在个人设置中选择“中文(简体)”。默认语言仍是 en_US,现有安装升级后不会自动切换。

旧翻译文件解决了第一批中文文案,后续维护还有两个问题。

Phorge 持续跟进上游,新功能会带来新的界面字符串;存量译文中也可能留着英文占位、中英混排或不统一的术语。单靠人工在一个两万多行的 PHP 文件里查找,很容易漏掉新增内容,也很难安全地分批修改。

这次一起迁入了 scripts/i18n/ 下的维护工具。它们负责查找缺失字符串、补入翻译键、筛选未完成译文、调用在线模型、保护格式占位符和保存处理进度。以后同步上游时,只需处理新增和未完成的部分。

一、把翻译文件放进仓库,还要让运行时找到它

升级到包含中文功能的版本后,用户进入 Settings → Account → Translation(切换后显示为“翻译”),选择“中文(简体)”并保存,页面就会使用 zh_CN。没有中文译文的字符串继续回退到 en_US,不会影响页面渲染。

这条看似简单的切换,涉及到四个环节:

环节 相关文件 作用
语言和翻译数据 PhabricatorChineseLocale.phpPhabricatorChineseTranslation.php 定义 zh_CN、中文名称、英文回退与翻译映射
类注册 __phutil_library_map__.php 让 Phorge 可以加载新增的 Locale 和 Translation 类
设置与运行时 PhabricatorTranslationSetting.phpPhabricatorEnv.php 在语言下拉中显示中文,并在请求中加载中文翻译
国际化校验 PhorgeInternationalizationValidator.php 避免校验工具把仓库自带的 zh_CN 判成未知语言

运行时加载必须单独接线。中文类由当前 Phorge 仓库提供,libphutilloadAllLocales()loadLocale() 无法自动发现它们。类映射只解决 PHP 自动加载,语言设置页和请求运行时仍有各自的发现逻辑。

当前实现先在设置页显式加入 zh_CN,保存时也允许这个值;请求开始后,PhabricatorEnv::setLocaleCode() 直接创建中文 Locale 和 Translation:

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-CNzh_Hanszh_Hantzh_* 语言代码归一为 zh_CN。这能兼容已有的中文设置,但目前也会让繁体中文使用简体译文。将来加入独立繁体翻译时,需要拆分这条归一规则。

二、缺少翻译键和译文未完成,是两类问题

维护翻译时,先把缺口分成两类:

状态 翻译表中的表现 用户看到的结果
缺少翻译键 找不到对应的英文源字符串 回退显示英文
译文未完成 值与键相同,或中文中保留了大段英文 显示英文或中英混排

check_zh_missing.py 处理第一类问题。它扫描 src/ 下 PHP 文件中的静态 pht('...') 字符串,与中文翻译表中的键做差集,再按源码中的出现次数排序,高频文案会排在清单前面。

这个检查器是 bin/i18n extract 不便运行时的快速补充。它只识别静态的单引号 pht() 字符串,以及用 . 连接的同类字符串,并默认跳过超过 220 个字符的长文本。动态参数、运行时拼接和复杂 PHP 表达式仍要通过 Phorge 自带的国际化工具和实际界面检查发现。

improve_zh_batch.pytranslate_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 可以循环完成前两步,但它不会发起在线翻译请求。

一轮常用流程如下:

# 安装 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 每次只选择两类条目:值与键完全相同的未翻译项,以及带有中文、同时仍保留较多英文的半成品。

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 条。一个批次通过校验后,脚本立即写回翻译文件,再将下一次扫描的行号保存到进度文件。网络中断后重新执行同一命令,会从上次位置继续。

默认进度文件名是 <translation-file>.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.6top_p=0.95max_tokens=32768,同时关闭思考模式:

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 记录了“仓库”“修订”“动态”“策略”等高频词的统一译法,也列出了 PHID、Herald、Differential 等应当保留的产品名。批量翻译可以快速补齐大部分内容,术语和页面语境仍需要人工抽查。

静态检查器无法覆盖动态 pht() 参数,在线脚本也不会处理所有复数和性别数组。发布前的界面检查仍然必要,missing=0 只能作为覆盖指标之一。

六、生产环境留意 OPcache

PhabricatorChineseTranslation.php 约有 2.7 万行。多数环境可以直接加载;生产环境启用 OPcache 后,如果实际出现 Failed to load symbol 一类告警,可以通过 opcache.blacklist_filename 将这份翻译文件加入黑名单。

这项配置只用于处理已经出现的加载问题,无需默认启用。上线前先使用与生产一致的 PHP 和 OPcache 配置切换一次中文界面,再根据日志决定是否调整。

最后

升级后,用户可以按需选择中文,英文默认值保持不变。语言设置页能够找到 zh_CN,运行时能够加载翻译表,缺少中文的文案也有明确的英文回退。

以后跟进上游时,维护工作只需围绕新增和未完成的词条展开:扫描差集、补键、翻译、校验。在线模型负责批量生成候选译文,脚本限制修改范围并拒绝格式损坏的结果,人工集中检查术语和具体页面语境。

–EOF