Git 翻译本地化自动化工作流:基于 po/AGENTS.md 的 PO 文件维护、AI 辅助翻译与质量评审全解
本文以 Git 仓库 po/AGENTS.md 文档为主体,系统讲解 Git 本地化(l10n)工作流的完整技术细节:PO/POT 文件的结构与元数据、术语表与占位符的保真规则、msgattrib/msgfmt/msgcat 等 gettext 工具链的用法,以及四条官方 AI 辅助维护流程(生成 POT、更新 PO、批量翻译、质量评审)的完整脚本与循环逻辑。读完后你将能够独立完成一个语言 PO 文件从模板同步到翻译交付的全部操作,并理解每个步骤在源码层面(Makefile 规则、msgmerge 调用)的真实含义。
一、po/AGENTS.md 的定位与适用场景
po/AGENTS.md 是一份面向 AI Agent 的"本地化维护作业说明",规定 AI 助手在为 Git 翻译执行例行维护任务(housekeeping)时必须遵循的步骤、脚本和红线。文档开篇明确指出:使用 AI 是可选的,许多成功的 l10n 团队并不依赖 AI;AI 生成的输出始终应视为草稿,需要由同时理解技术上下文和目标语言的人审核批准。
它与 po/README.md 是互补关系。README 的 "AI-assisted translation and review" 一节明确指向本文档,并给出提示词范例:"Translate po/XX.po with reference to po/AGENTS.md"(将 XX 替换为你的语言代码)。po/ 目录下当前包含 20 个语言的翻译文件(如 zh_CN.po、zh_TW.po、de.po 等),以及记录各语言团队联系人的 TEAMS 文件。
AGENTS.md 将任务分为四块:
- 生成或更新
po/git.pot(模板文件); - 更新
po/XX.po(同步上游新字符串); - 翻译
po/XX.po; - 评审翻译质量。
其中 3、4 两条流程包含带状态文件的循环工作流和权威 shell 脚本,是全文最核心的实战部分。
二、本地化工作流的背景知识
文档要求:执行任何维护任务前,必须先理解以下概念。这些概念是后文所有脚本与规则的基础。
2.1 语言代码记法(XX、ll、ll_CC)
XX 是语言代码占位符,取两种形式之一:ll(ISO 639 两字母语言代码)或 ll_CC(语言+地区,如 de、zh_CN)。它出现在 PO 文件头部元数据中(如 "Language: zh_CN\n"),并用于 PO 文件命名:po/XX.po。po/README.md 补充说明:CC 是 ISO 3166 地区代码,例如 de(德语)、zh_CN(简体中文)。
2.2 Header Entry(头条目)
每个 po/XX.po 的第一个条目是 header entry:它的 msgid 为空字符串,翻译元数据(项目名、语言、复数规则、编码等)全部存放在 msgstr 中:
msgid ""
msgstr ""
"Project-Id-Version: Git\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
关键红线:翻译过程中不得编辑 header 的 msgstr。它只承载元数据,必须保持原样——这也是 Task 3 校验脚本会检查的隐含前提(后续 git-po-helper compare --msgid 与元数据完整性检查依赖于此)。
2.3 Glossary Section(术语表区)
PO 文件可以在 header entry 之前(即第一个 msgid "" 之前)以注释形式放置术语表,给出术语统一译法。以仓库中的 po/zh_CN.po 为例,其开头就有一段真实的术语表:
# Git glossary for Chinese translators
#
# English | Chinese
# ---------------------------------+--------------------------------------
# 3-way merge | 三路合并
# amend | 修订
# annotated tag | 附注标签
# branch | 分支
# checkout | 检出
# commit | 提交
# commit message | 提交说明
# fast-forward | 快进
# HEAD | HEAD(头指针,亦即当前分支)
# hook | 钩子
# ...
重要规则:翻译或评审时必须阅读并遵守术语表;术语表只存在于 # 注释中,保留该注释块原样不动。术语一致性也是第六节质量检查清单中"Terminology"一项的判定依据。
2.4 PO 条目结构:单行与多行
PO 条目是 msgid / msgstr 配对;复数消息追加 msgid_plural 和 msgstr[n]。msgid 是不可变的源文本,msgstr 是目标译文。每一侧可以是单个引号字符串,也可以是多行块——多行形式中首行通常是 msgid "" / msgstr "",真实文本拆到后续若干引号行,由 Gettext 在解析时拼接。
单行条目:
msgid "commit message"
msgstr "提交说明"
多行条目:
msgid ""
"Line 1\n"
"Line 2"
msgstr ""
"行 1\n"
"行 2"
关键陷阱:文档特别警告不要用 grep '^msgstr ""' 去找未翻译条目——多行 msgstr 块同样以 msgstr "" 开头,grep 会产生大量假阳性。正确做法是用 msgattrib 按状态筛选(见 2.5 节)。
2.5 定位未翻译、fuzzy 与 obsolete 条目
使用 msgattrib 列出三类条目:
- 未翻译(untranslated):
msgattrib --untranslated --no-obsolete po/XX.po - fuzzy:
msgattrib --only-fuzzy --no-obsolete po/XX.po - obsolete(
#~标记的废弃条目):msgattrib --obsolete --no-wrap po/XX.po
2.6 翻译 fuzzy 条目
fuzzy 条目表示源文本(msgid)已变化、旧译文不可靠,需要重新翻译。两种格式下的标记方式不同:
- PO 文件:条目注释区有
#, fuzzy标签; - JSON 文件(GETTEXT JSON,见 4.4 节):条目含
"fuzzy": true。
翻译原则:重新翻译 msgstr(复数条目还有 msgstr[n]),绝不修改 msgid 或 msgid_plural。翻译完成后清除 fuzzy 标记:PO 中删除 #, fuzzy 标签;JSON 中省略 fuzzy 字段或置为 false。
这与 po/README.md 的 "Fuzzy translation" 一节相互印证:fuzzy 翻译在 msgfmt 编译时被忽略,多数情况下是运行 msgmerge 更新 XX.po 时自动标记的,修正翻译后必须移除 fuzzy 标签。
2.7 保留特殊字符
译文中必须原样保留:
- 转义序列:
\n、\"、\\、\t; - printf 风格占位符:
%s、%d等; - 引号。
只有当需要改变占位符相对顺序时才允许使用位置化语法(见 2.9 节)。
2.8 保留引号(quotation marks)
部分语言使用语言专属的 UTF-8 引号(弯引号/智能引号)而非 ASCII 直引号。必须原样保留这些字符,不得转换为 ASCII 直引号。
文档给出的受保护引号清单(非穷举):
| 字符 | Unicode | 名称 | 使用语言 |
|---|---|---|---|
| „ | U+201E | DOUBLE LOW-9 QUOTATION MARK | 保加利亚语、德语等 |
| " | U+201C | LEFT DOUBLE QUOTATION MARK | 保加利亚语等 |
| " | U+201D | RIGHT DOUBLE QUOTATION MARK | 英语、德语等 |
| ' | U+2018 | LEFT SINGLE QUOTATION MARK | 英语等 |
| ' | U+2019 | RIGHT SINGLE QUOTATION MARK | 英语等 |
| « | U+00AB | LEFT-POINTING DOUBLE ANGLE QUOTATION MARK | 法语、俄语等 |
| » | U+00BB | RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK | 法语、俄语等 |
| ‹ | U+2039 | SINGLE LEFT-POINTING ANGLE QUOTATION MARK | 法语等 |
| › | U+203A | SINGLE RIGHT-POINTING ANGLE QUOTATION MARK | 法语等 |
为什么这在 PO 文件中是致命问题:PO 格式里 ASCII 直引号 "(U+0022)是字符串定界符。如果译文中的弯引号被错误转换成了 "(U+0022),PO 解析器会把它当作字符串结束,导致:
- 字符串截断:
msgstr值在多余的引号处被截短; - 语法错误:
msgfmt --check在该行报解析错误; - 数据丢失:误引号之后的内容被错误解析或丢失。
规则:
- 永远不要用 ASCII 直引号
"(U+0022)或'(U+0027)替换语言专属引号; - 该规则适用于翻译 PO 文件、PO 多行字符串、GETTEXT JSON 的
msgstr数组值; - 生成建议译文(
suggest_msgstr,见 7.3 节评审格式)时同样适用; - 若源
msgid用的是 ASCII 直引号,除非目标语言惯例要求其他引号,译文中原样保留。
2.9 占位符重排(Placeholder Reordering)
当译文需要改变占位符相对 msgid 的顺序时,必须使用位置化语法 %n$(n 为 1 起始的实参下标),保证每个实参仍绑定到正确的值。宽度/精度修饰符要保留,且 %n$ 要放在它们之前。
例 1(带精度占位符的重排):
msgid "missing environment variable '%s' for configuration '%.*s'"
msgstr "配置 '%3$.*2$s' 缺少环境变量 '%1$s'"
%s → 实参 1 → %1$s;%.*s 需要精度(实参 2)和字符串(实参 3)→ %3$.*2$s。
这个 msgid 在仓库源码中真实存在:config.c 第 522 行的 die(_("missing environment variable '%s' for configuration '%.*s')"), ...,说明该条目会随 make po-update 进入各语言 PO 文件,翻译时必须按上述位置化规则处理。
例 2(多行、五个 %s 重排):
msgid ""
"Path updated: %s renamed to %s in %s, inside a directory that was renamed in "
"%s; moving it to %s."
msgstr ""
"路径已更新:%1$s 在 %3$s 中被重命名为 %2$s,而其所在目录又在 %4$s 中被重命"
"名,因此将其移动到 %5$s。"
原顺序 1,2,3,4,5;译文顺序 1,3,2,4,5。多行要求:每一行必须是一个完整的引号字符串。
例 3(无需重排):
msgid "MIDX %s must be an ancestor of %s"
msgstr "MIDX %s 必须是 %s 的祖先"
译文实参顺序仍是 1,2,因此不需要 %n$。规则的另一面:如果没有发生占位符重排,绝不能引入 %n$ 语法,必须保持原有的非位置化占位符(%s、%d 等)。这是 msgfmt --check 能检出的典型错误来源。
2.10 校验 PO 文件格式
用以下命令检查 PO 文件:
msgfmt --check -o /dev/null po/XX.po
常见校验错误包括:引号未闭合、转义序列缺失、占位符语法非法、多行条目畸形、多行字符串换行错误。失败时 msgfmt 会打印出错行号,到该行修复即可。po/README.md 的 "Testing your changes" 一节补充:顶层运行 make 时(GNU gettext 系统上)会对改动的 PO 文件执行 msgfmt --check,该选项能发现很多常见错误,例如缺失的 printf 格式串、译文与原文在新行开头/结尾上的不一致。
三、模板生成管线:Task 1 与 Task 2 的源码级原理
3.1 Task 1:生成或更新 po/git.pot
文档规定:被要求生成或更新 po/git.pot 时:
- 直接执行
make po/git.pot,执行前不必检查文件是否存在; - 执行后不做验证,跑完即视为任务完成。
这条"直接执行、不验证"的指令看似简单,其背后是 Makefile 中一条完整的 xgettext 抽取管线。从 Makefile 可以看到:
- 参与抽取的文件分三类:
LOCALIZED_C(C 源码)、LOCALIZED_SH(shell 脚本,固定含 git-sh-setup.sh)、LOCALIZED_PERL(Perl 脚本); - 每个源文件先用
xgettext --omit-header抽取到中间文件.build/pot/po/<文件>.po;C 文件若有 Git 自定义的PRItime类型,会先用sed替换为PRIuMAX再抽取(gettext 工具无法识别PRItime); po/git.pot最终由.build/pot/git.header(由xgettext对/dev/null生成、再改写 charset/Language-Team 等头部字段、并追加Plural-Forms占位行)与全部中间 PO 文件一起,用msgcat拼接而成(po/git.pot: .build/pot/git.header $(LOCALIZED_ALL_GEN_PO)规则,msgcat $^ >$@)。
这也解释了 po/README.md 的说明:po/git.pot 不再入库,而是动态生成;另有一个更小的 po/git-core.pot(仅覆盖 checkout/clone/push/reset 等核心文件的 5000+ 条消息中的最小集),用于 make po-init 初始化新语言。
3.2 Task 2:更新 po/XX.po
被要求更新 po/XX.po 时:
- 直接执行
make po-update PO_FILE=po/XX.po,执行前不必读取或检查文件内容; - 执行后不做验证、翻译或评审,跑完即完成。
对照 Makefile 的 po-update 规则可看到其真实行为:
- 先依赖
po/git.pot目标(即先跑 Task 1 的完整抽取管线); - 校验
PO_FILE必须以po/开头并匹配po/%.po模式,且文件必须已存在(不存在则提示改用make po-init PO_FILE=po/XX.po); - 执行
msgmerge $(MSGMERGE_FLAGS) $(PO_FILE) po/git.pot,把 POT 中的新字符串、变化的字符串同步进XX.po。
po/README.md 进一步说明 msgmerge 使用 --add-location --backup=off -U 参数:--add-location 会添加源码位置行(#: file.c:123 形式注释),帮助翻译工具定位上下文。而提交前通常要用 clean filter 去掉位置信息以节省仓库体积(README 的 "Preparing a XX.po file for commit" 一节描述了 filter.gettext-no-location / gettext-no-line-number 两个 filter 的配置方式)。
注意分工边界:Task 1/2 只负责"机器同步",同步后新产生的 fuzzy/未翻译条目交给 Task 3 的翻译循环处理。
四、工具层:git-po-helper 子命令
文档假定环境中可能存在 git-po-helper(Git l10n 社区助手工具),它提供质量检查(git-l10n PR 约定)与AI 辅助翻译(面向自动化工作流的子命令)。所有维护任务在可用时优先用它,否则退回纯 gettext 工具。po/README.md 的 "PO helper" 一节说明它还能校验贡献是否符合项目约定(PO 语法、提交信息、可改动路径等),l10n 协调者用它检查贡献:
git-po-helper check-po po/XX.po
git-po-helper check-commits <rev-list-opts>
以下四个子命令/能力是 AGENTS.md 工作流的直接依赖。
4.1 msg-select:按条目索引拆分大 PO 文件
当 PO 文件太大、不适合一次性翻译或评审时,用 git-po-helper msg-select 按条目索引切分:
- 条目 0 是 header(默认包含;
--no-header可省略); - 条目 1、2、3… 是内容条目;
- 区间格式:
--range "1-50"(第 1 到 50 条)、--range "-50"(前 50 条)、--range "51-"(第 51 条到末尾);快捷参数:--head N(前 N 条)、--tail N(后 N 条)、--since N(第 N 条到末尾); - 输出格式:默认 PO;
--json输出 GETTEXT JSON(格式见 4.4 节); - 状态过滤:
--translated、--untranslated、--fuzzy按状态过滤(OR 关系);--no-obsolete排除废弃条目,--with-obsolete包含(默认);--only-same/--only-obsolete只取单一状态。区间作用于过滤后的列表。
# 前 50 条(header + 条目 1–50)
git-po-helper msg-select --range "-50" po/in.po -o po/out.po
# 条目 51–100
git-po-helper msg-select --range "51-100" po/in.po -o po/out.po
# 第 101 条到末尾
git-po-helper msg-select --range "101-" po/in.po -o po/out.po
# 条目 1–50 且不含 header(仅内容)
git-po-helper msg-select --range "1-50" --no-header po/in.po -o po/frag.po
# 输出 JSON;选取未翻译与 fuzzy 条目,排除 obsolete
git-po-helper msg-select --json --untranslated --fuzzy --no-obsolete po/in.po >po/filtered.json
4.2 compare:带完整条目上下文的 PO 对比
git-po-helper compare 展示 PO 变更,且带完整条目上下文(git diff 做不到这一点)。输出重定向到文件:无新增/变更条目时为空,否则包含一个合法的 PO header。
# 本地改动(HEAD vs 工作区)的完整上下文
git-po-helper compare po/XX.po -o po/out.po
# 某个具体提交内的变更(父提交 vs 该提交)
git-po-helper compare --commit <commit> po/XX.po -o po/out.po
# 某提交至今的变更(提交 vs 工作区)
git-po-helper compare --since <commit> po/XX.po -o po/out.po
# 两个提交之间
git-po-helper compare -r <commit1>..<commit2> po/XX.po -o po/out.po
# 两个工作区文件
git-po-helper compare po/old.po po/new.po -o po/out.po
# 检查 msgid 一致性(检测篡改);无输出即目标与源一致
git-po-helper compare --msgid po/old.po po/new.po >po/out.po
选项汇总:
| 选项 | 含义 |
|---|---|
| (无) | 对比 HEAD 与工作区(本地改动) |
--commit <commit> |
对比提交的父提交与该提交 |
--since <commit> |
对比提交与工作区 |
-r x..y |
对比修订 x 与修订 y |
-r x.. |
对比修订 x 与工作区 |
-r x |
对比 x 的父提交与 x |
4.3 msg-cat:合并多个 PO/JSON 文件
git-po-helper msg-cat 把 PO、POT 或 gettext JSON 输入合并成一个流。重复 msgid 按文件顺序保留第一次出现。用 -o <file> 写文件,或写标准输出(-o - 或省略);--json 选 JSON 输出,否则为 PO。
# JSON 转 PO(例如翻译完成后)
git-po-helper msg-cat --unset-fuzzy -o po/out.po po/in.json
# 合并多个 PO 文件
git-po-helper msg-cat -o po/out.po po/in-1.po po/in-2.json
4.4 GETTEXT JSON 格式
GETTEXT JSON 是 git-po-helper 定义的内部格式,专为 AI 模型批量处理翻译及相关任务而设计;msg-select、msg-cat、compare 均能读写该格式。
顶层结构:
{
"header_comment": "string",
"header_meta": "string",
"entries": [ /* 条目对象数组 */ ]
}
| 字段 | 说明 |
|---|---|
header_comment |
第一个 msgid "" 之上的各行(注释、术语表),直接拼接。 |
header_meta |
header 条目 msgstr 的编码形式(Project-Id-Version、Plural-Forms 等)。 |
entries |
PO 条目列表,顺序与源一致。 |
条目对象(entries 的每个元素):
| 字段 | 类型 | 说明 |
|---|---|---|
msgid |
string | 单数消息 ID。PO 转义编码(如 \n → \\n)。 |
msgstr |
[]string | 译文形式,只能是 JSON 数组。细节见下。 |
msgid_plural |
string | msgid 的复数形式。非复数时省略。 |
comments |
[]string | 注释行(#、#.、#:、#, 等)。 |
fuzzy |
bool | 条目带 fuzzy 标志时为 true。 |
obsolete |
bool | #~ 废弃条目为 true;为 false 时省略。 |
msgstr 数组(必须遵守的形状):
- 永远是字符串 JSON 数组,不能是单个字符串。一个元素 = 单数(PO 的
msgstr/msgstr[0]);多个元素 = 按顺序排列的复数形式(msgstr[0]、msgstr[1]…)。 - 条目未翻译时省略该键或用空数组。
示例(单行条目):
{
"header_comment": "# Glossary:\\n# term1\\tTranslation 1\\n#\\n",
"header_meta": "Project-Id-Version: git\\nContent-Type: text/plain; charset=UTF-8\\n",
"entries": [
{
"msgid": "Hello",
"msgstr": ["你好"],
"comments": ["#. Comment for translator\\n", "#: src/file.c:10\\n"],
"fuzzy": false
}
]
}
示例(复数条目):
{
"msgid": "One file",
"msgid_plural": "%d files",
"msgstr": ["一个文件", "%d 个文件"],
"comments": ["#, c-format\\n"]
}
示例(翻译前的 fuzzy 条目):
{
"msgid": "Old message",
"msgstr": ["旧翻译。"],
"comments": ["#, fuzzy\\n"],
"fuzzy": true
}
GETTEXT JSON 翻译注意事项:
- 保留结构:
header_comment、header_meta、msgid、msgid_plural不得改动; - fuzzy 条目:从 fuzzy PO 条目抽取出的条目带
"fuzzy": true。翻译后在输出 JSON 中删除fuzzy字段或置false。合并步骤使用--unset-fuzzy,该参数同样能移除fuzzy字段; - 占位符:原样保留
%s、%d等;重排时使用%n$(见 2.9 节)。
五、Task 3:翻译 po/XX.po 的完整循环工作流
翻译 po/XX.po 时按以下步骤执行。脚本依据环境自动选择 gettext 或 git-po-helper;JSON 导出(可用时)支持批量翻译而非逐条处理。
工作流环路:步骤 1→2→3→4→5→6→7 构成循环。步骤 6 成功后必须进入步骤 7,再回到步骤 1。唯一通往步骤 8 的路径是:步骤 2 发现 po/l10n-pending.po 为空。不得跳过步骤 7 或在步骤 6 之后直接跳步骤 8。
5.1 步骤 1:提取待翻译条目
直接执行以下权威脚本(文档明确"不要自行重新实现")。它生成包含待翻译消息的 po/l10n-pending.po:
l10n_extract_pending () {
test $# -ge 1 || { echo "Usage: l10n_extract_pending <po-file>" >&2; return 1; }
PO_FILE="$1"
PENDING="po/l10n-pending.po"
PENDING_FUZZY="${PENDING}.fuzzy"
PENDING_REFER="${PENDING}.fuzzy.reference"
PENDING_UNTRANS="${PENDING}.untranslated"
rm -f "$PENDING"
if command -v git-po-helper >/dev/null 2>&1
then
git-po-helper msg-select --untranslated --fuzzy --no-obsolete -o "$PENDING" "$PO_FILE"
else
msgattrib --untranslated --no-obsolete "$PO_FILE" >"${PENDING_UNTRANS}"
msgattrib --only-fuzzy --no-obsolete --clear-fuzzy --empty "$PO_FILE" >"${PENDING_FUZZY}"
msgattrib --only-fuzzy --no-obsolete "$PO_FILE" >"${PENDING_REFER}"
msgcat --use-first "${PENDING_UNTRANS}" "${PENDING_FUZZY}" >"$PENDING"
rm -f "${PENDING_UNTRANS}" "${PENDING_FUZZY}"
fi
if test -s "$PENDING"
then
msgfmt --stat -o /dev/null "$PENDING" || true
echo "Pending file is not empty; there are still entries to translate."
else
echo "No entries need translation."
return 1
fi
}
# 运行提取。示例:l10n_extract_pending po/zh_CN.po
l10n_extract_pending po/XX.po
脚本要点:有 git-po-helper 时一条 msg-select 完成"未翻译 + fuzzy"的筛选;无该工具时用 msgattrib 分路提取——未翻译条目直接取;fuzzy 条目用 --clear-fuzzy --empty 清掉 fuzzy 标志并清空旧译文后参与合并,同时把带 fuzzy 标志的原始副本另存为 .fuzzy.reference(供重翻时参考旧译文);最后 msgcat --use-first 合并。
5.2 步骤 2:检查生成文件
若 po/l10n-pending.po 为空或不存在,说明翻译已全部完成,跳到步骤 8。否则进入步骤 3。
5.3 步骤 3:准备一个翻译批次
批量化是为了让每次运行保持小规模,使模型在有限上下文内完成翻译。翻译前必须直接执行以下权威脚本。根据脚本产出哪个文件分支:存在 po/l10n-todo.json 走步骤 4a;存在 po/l10n-todo.po 走步骤 4b。
l10n_one_batch () {
test $# -ge 1 || { echo "Usage: l10n_one_batch <po-file> [min_batch_size]" >&2; return 1; }
PO_FILE="$1"
min_batch_size=${2:-100}
PENDING="po/l10n-pending.po"
TODO_JSON="po/l10n-todo.json"
TODO_PO="po/l10n-todo.po"
DONE_JSON="po/l10n-done.json"
DONE_PO="po/l10n-done.po"
rm -f "$TODO_JSON" "$TODO_PO" "$DONE_JSON" "$DONE_PO"
ENTRY_COUNT=$(grep -c '^msgid ' "$PENDING" 2>/dev/null || echo 0)
ENTRY_COUNT=$((ENTRY_COUNT > 0 ? ENTRY_COUNT - 1 : 0))
if test "$ENTRY_COUNT" -gt $min_batch_size
then
if test "$ENTRY_COUNT" -gt $((min_batch_size * 8))
then
NUM=$((min_batch_size * 2))
elif test "$ENTRY_COUNT" -gt $((min_batch_size * 4))
then
NUM=$((min_batch_size + min_batch_size / 2))
else
NUM=$min_batch_size
fi
BATCHING=1
else
NUM=$ENTRY_COUNT
BATCHING=
fi
if command -v git-po-helper >/dev/null 2>&1
then
if test -n "$BATCHING"
then
git-po-helper msg-select --json --head "$NUM" -o "$TODO_JSON" "$PENDING"
echo "Processing batch of $NUM entries (out of $ENTRY_COUNT remaining)"
else
git-po-helper msg-select --json -o "$TODO_JSON" "$PENDING"
echo "Processing all $ENTRY_COUNT entries at once"
fi
else
if test -n "$BATCHING"
then
awk -v num="$NUM" '/^msgid / && count++ > num {exit} 1' "$PENDING" |
tac | awk '/^$/ {found=1} found' | tac >"$TODO_PO"
echo "Processing batch of $NUM entries (out of $ENTRY_COUNT remaining)"
else
cp "$PENDING" "$TODO_PO"
echo "Processing all $ENTRY_COUNT entries at once"
fi
fi
}
# 准备一个批次;若批次超出 agent 处理能力,调小第 2 个参数
l10n_one_batch po/XX.po 100
脚本要点:ENTRY_COUNT 通过 grep -c '^msgid ' 计数再减 1(排除 header 条目);批量大小按剩余量自适应——超过 min_batch_size 的 8 倍时取 2×min,超过 4 倍时取 1.5×min,否则取 min;有 git-po-helper 时输出 JSON(--json --head N),无则用 awk/tac 从 PO 文本中截取前 N 个条目块。
5.4 步骤 4a/4b:翻译批次
4a. 翻译 JSON 批次(po/l10n-todo.json → po/l10n-done.json):
- 任务:把
po/l10n-todo.json(输入,GETTEXT JSON)翻译为po/l10n-done.json(输出,GETTEXT JSON)。格式细节与翻译规则见 4.4 节; - 参考术语表:从批处理文件的
header_comment读取术语表(见 2.3 节),用于统一术语; - 翻译要求:遵循第六节质量检查清单。正确处理转义序列(
\n、\"、\\、\t)、占位符与引号,与msgid保持一致;JSON 中读写时对这些序列做正确的转义/反转义。只修改msgstr与msgstr[n](复数条目);清除 fuzzy 标志(省略或置false)。不得修改msgid或msgid_plural。
4b. 翻译 PO 批次(po/l10n-todo.po → po/l10n-done.po):
- 任务:把
po/l10n-todo.po(输入,GETTEXT PO)翻译为po/l10n-done.po(输出,GETTEXT PO); - 参考术语表:从 pending 文件头部读取术语表(见 2.3 节),用于统一术语;
- 翻译要求:遵循质量检查清单。与
msgid一样保留转义序列、占位符、引号。只修改msgstr与msgstr[n];完成后从注释中删除#, fuzzy标签。不得修改msgid或msgid_plural。
5.5 步骤 5:校验 po/l10n-done.po
运行以下校验脚本。失败时按错误与说明修复,重跑直至通过:
l10n_validate_done () {
DONE_PO="po/l10n-done.po"
DONE_JSON="po/l10n-done.json"
PENDING="po/l10n-pending.po"
if test -f "$DONE_JSON" && { ! test -f "$DONE_PO" || test "$DONE_JSON" -nt "$DONE_PO"; }
then
git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO" "$DONE_JSON" || {
echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2
return 1
}
fi
# Check 1: msgid should not be modified
MSGID_OUT=$(git-po-helper compare -q --msgid --assert-no-changes \
"$PENDING" "$DONE_PO" 2>&1)
MSGID_RC=$?
if test $MSGID_RC -ne 0 || test -n "$MSGID_OUT"
then
echo "ERROR [msgid modified]: The following entries appeared after" >&2
echo "translation because msgid was altered. Fix in $DONE_PO." >&2
echo "$MSGID_OUT" >&2
return 1
fi
# Check 2: PO format (see "Validating PO File Format" for error handling)
MSGFMT_OUT=$(msgfmt --check -o /dev/null "$DONE_PO" 2>&1)
MSGFMT_RC=$?
if test $MSGFMT_RC -ne 0
then
echo "ERROR [PO format]: Fix errors in $DONE_PO." >&2
echo "$MSGFMT_OUT" >&2
return 1
fi
echo "Validation passed."
}
l10n_validate_done
校验包含两项:Check 1(msgid 未被修改)——git-po-helper compare -q --msgid --assert-no-changes 对比 pending 与 done 文件的 msgid,任何改动都会列出对应条目;Check 2(PO 格式)——msgfmt --check 按 2.10 节规则校验。
失败时的处理原则:直接在 po/l10n-done.po 中修复(不建议改 po/l10n-done.json,因为那会多一层 JSON→PO 转换),按错误提示分类处理:
[msgid modified]:所列条目的msgid被改动,恢复为与po/l10n-pending.po一致;[PO format]:msgfmt报告行号,就地修复错误(常见错误见 2.10 节)。
5.6 步骤 6:把翻译结果合并回 po/XX.po
运行以下脚本。失败时修复错误所指出的文件:[JSON to PO conversion] → po/l10n-done.json;[msgcat merge] → po/l10n-done.po。重跑直至成功。
l10n_merge_batch () {
test $# -ge 1 || { echo "Usage: l10n_merge_batch <po-file>" >&2; return 1; }
PO_FILE="$1"
DONE_PO="po/l10n-done.po"
DONE_JSON="po/l10n-done.json"
MERGED="po/l10n-done.merged"
PENDING="po/l10n-pending.po"
PENDING_REFER="${PENDING}.fuzzy.reference"
TODO_JSON="po/l10n-todo.json"
TODO_PO="po/l10n-todo.po"
if test -f "$DONE_JSON" && { ! test -f "$DONE_PO" || test "$DONE_JSON" -nt "$DONE_PO"; }
then
git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO" "$DONE_JSON" || {
echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2
return 1
}
fi
msgcat --use-first "$DONE_PO" "$PO_FILE" >"$MERGED" || {
echo "ERROR [msgcat merge]: Fix errors in $DONE_PO and re-run." >&2
return 1
}
mv "$MERGED" "$PO_FILE"
rm -f "$TODO_JSON" "$TODO_PO" "$DONE_JSON" "$DONE_PO" "$PENDING_REFER"
}
# 运行合并。示例:l10n_merge_batch po/zh_CN.po
l10n_merge_batch po/XX.po
脚本要点:若存在更新的 JSON 则先转 PO;核心合并是 msgcat --use-first "$DONE_PO" "$PO_FILE"——以翻译批次优先,覆盖主文件对应条目,其余条目(未进入本批次的)原样保留;合并后写回 po/XX.po 并清理本批次的 todo/done 中间文件与 fuzzy 参考文件。
5.7 步骤 7:循环
必须回到步骤 1(提取条目)重跑循环。不得跳过此步或进入步骤 8。步骤 8 只在步骤 2 判定没有待翻译条目时才被走到。
5.8 步骤 8:循环退出后的收尾校验
仅在循环退出后执行以下命令,校验整个 PO 文件并打印统计报告。流程到此结束:
msgfmt --check --stat -o /dev/null po/XX.po
六、质量检查清单(Quality Checklist)
无论人工还是 AI 翻译,每条译文对照以下清单逐项检查:
- 准确性:忠实于原文含义;无遗漏、无歪曲;
- fuzzy 条目:完整重翻并清除 fuzzy 标志(见 2.6 节);
- 术语:与术语表(见 2.3 节)或领域标准一致;
- 语法与流畅度:目标语言中正确自然;
- 占位符:原样保留变量(
%s、{name}、$1);重排时使用位置化参数(见 2.9 节); - 特殊字符:原样保留转义序列(
\n、\"、\\、\\t)与占位符;保留语言专属引号(„、"、"、'、' 等弯引号),不得转换为 ASCII 直引号(见 2.7/2.8 节); - 复数与性:形式正确、性数一致;
- 语境适配:适合 UI 空间、语气与用途(如错误信息 vs 工具提示);
- 文化适宜性:无冒犯或歧义内容;
- 一致性:与同一源文本的既往译文一致;
- 技术完整性:代码、路径、命令、品牌、专有名词不翻译;
- 可读性:清晰、简洁、对用户友好。
七、Task 4:翻译质量评审工作流
评审对象可以是完整的 po/XX.po、某个提交、或某提交以来的变更。被要求评审时按以下步骤执行。
工作流约束:按顺序执行。不得用 git show、git diff、git format-patch 等获取变更——它们会破坏 PO 上下文,提取变更只能使用 git-po-helper compare。没有 git-po-helper 时应拒绝该任务。步骤 3→4→5→6→7 循环:步骤 6 之后必须进入步骤 7(回到步骤 3)。通往步骤 8 的唯一路径是:步骤 4 发现 po/review-todo.json 缺失或为空(没有待评审批次),或步骤 1 发现 po/review-result.json 已存在。
7.1 步骤 1:检查既有评审(断点续传支持)
按以下顺序判定:
- 若
po/review-input.po不存在 → 全新开始,进入步骤 2(提取条目); - 否则若
po/review-result.json存在 → 进入步骤 8(循环退出后); - 否则若
po/review-done.json存在 → 进入步骤 6(重命名结果); - 否则若
po/review-todo.json存在 → 进入步骤 5(评审当前批次); - 否则 → 进入步骤 3(准备一个批次)。
7.2 步骤 2:提取评审条目
用期望的范围运行 git-po-helper compare,把输出重定向到 po/review-input.po。选项参见 4.2 节"Comparing PO files"(如评审某提交用 --commit,评审提交以来的变更用 --since,评审两提交之间用 -r x..y)。
7.3 步骤 3:准备一个评审批次
批量化让每次运行保持小规模,使模型能在有限上下文内完成评审。直接执行以下权威脚本(不要自行重新实现):
review_one_batch () {
min_batch_size=${1:-100}
INPUT_PO="po/review-input.po"
PENDING="po/review-pending.po"
TODO="po/review-todo.json"
DONE="po/review-done.json"
BATCH_FILE="po/review-batch.txt"
if test ! -f "$INPUT_PO"
then
rm -f "$TODO"
echo >&2 "cannot find $INPUT_PO, nothing for review"
return 1
fi
if test ! -f "$PENDING" || test "$INPUT_PO" -nt "$PENDING"
then
rm -f "$BATCH_FILE" "$TODO" "$DONE"
rm -f po/review-result*.json
cp "$INPUT_PO" "$PENDING"
fi
ENTRY_COUNT=$(grep -c '^msgid ' "$PENDING" 2>/dev/null || echo 0)
ENTRY_COUNT=$((ENTRY_COUNT > 0 ? ENTRY_COUNT - 1 : 0))
if test "$ENTRY_COUNT" -eq 0
then
rm -f "$TODO"
echo >&2 "No entries left for review"
return 1
fi
if test "$ENTRY_COUNT" -gt $min_batch_size
then
if test "$ENTRY_COUNT" -gt $((min_batch_size * 8))
then
NUM=$((min_batch_size * 2))
elif test "$ENTRY_COUNT" -gt $((min_batch_size * 4))
then
NUM=$((min_batch_size + min_batch_size / 2))
else
NUM=$min_batch_size
fi
else
NUM=$ENTRY_COUNT
fi
BATCH=$(cat "$BATCH_FILE" 2>/dev/null || echo 0)
BATCH=$((BATCH + 1))
echo "$BATCH" >"$BATCH_FILE"
git-po-helper msg-select --json --head "$NUM" -o "$TODO" "$PENDING"
git-po-helper msg-select --since "$((NUM + 1))" -o "${PENDING}.tmp" "$PENDING"
mv "${PENDING}.tmp" "$PENDING"
echo "Processing batch $BATCH ($NUM entries out of $ENTRY_COUNT)"
}
# 参数控制批次大小;若批次文件过大可调小
review_one_batch 100
脚本要点:以输入 PO 的时间戳做"pending 是否过期"判断,过期则重置全部中间状态(含已产出的 review-result*.json);批次大小策略与 Task 3 相同(8×/4× 分档);每批次用 msg-select --json --head N 截取 JSON 写入 review-todo.json,同时把 --since N+1 之后的部分回写 pending,实现消费式前进;review-batch.txt 记录已完成批次编号,用于步骤 6 的结果文件命名。
7.4 步骤 4:检查 todo 文件
若 po/review-todo.json 不存在或为空,评审完成,进入步骤 8。否则进入步骤 5。
7.5 步骤 5:评审当前批次
评审 po/review-todo.json 中的翻译,把发现写入 po/review-done.json:
- 使用"本地化工作流背景知识"(第二节)掌握 PO/JSON 结构、占位符与术语规则;
- 若
header_comment含术语表,遵循它保证一致性; - 不评审 header(
header_comment、header_meta); - 对其余每个条目,按第六节质量检查清单,把条目的
msgstr数组(译文形式)对照msgid/msgid_plural检查; - 按 7.6 节的"评审结果 JSON 格式"写 JSON;无问题时写
{"issues": []}。必须写出po/review-done.json——它是批次完成的标记。
7.6 步骤 6:重命名结果文件
把 po/review-done.json 重命名为 po/review-result-<N>.json,N 取 po/review-batch.txt 中的值(刚完成的批次号)。执行以下脚本:
review_rename_result () {
TODO="po/review-todo.json"
DONE="po/review-done.json"
BATCH_FILE="po/review-batch.txt"
if test -f "$DONE"
then
N=$(cat "$BATCH_FILE" 2>/dev/null) || { echo "ERROR: $BATCH_FILE not found." >&2; return 1; }
mv "$DONE" "po/review-result-$N.json"
echo "Renamed to po/review-result-$N.json"
fi
rm -f "$TODO"
}
review_rename_result
7.7 步骤 7:循环
必须回到步骤 3(准备一个批次)重跑循环。不得跳过或进入步骤 8。步骤 8 只有在步骤 4 发现 po/review-todo.json 缺失或为空时才能到达。
7.8 步骤 8:循环退出后的收尾
仅在循环退出后直接执行以下命令。它会合并评审结果、应用建议译文并展示报告。流程到此结束:
git-po-helper agent-run review --report po
不得做清理或删除中间文件;保留它们以便检查或断点续跑。
7.9 评审结果 JSON 格式
Review result JSON 定义翻译评审报告的结构。对每个存在翻译问题的条目创建一个 issue 对象:
- 把原条目的
msgid、可选msgid_plural、可选msgstr数组(原译文形式)拷贝进 issue 对象。形状与 GETTEXT JSON 相同:msgstr存在时永远是 JSON 数组(单数一个元素,复数多个按序); - 在
description中汇总该条目发现的所有问题; - 按问题严重程度设置
score,0 到 3(0 = 严重;1 = 较大;2 = 轻微;3 = 完美,无问题)。分数越低问题越严重; - 建议译文放入
suggest_msgstr,也是 JSON 数组:单数一个字符串,复数多个字符串按序排列。这是git-po-helper应用建议的必需字段; - 只收录有问题的条目(score 小于 3)。整批无问题时写
{"issues": []}。
示例(含问题):
{
"issues": [
{
"msgid": "commit",
"msgstr": ["委托"],
"score": 0,
"description": "Terminology error: 'commit' should be translated as '提交'",
"suggest_msgstr": ["提交"]
},
{
"msgid": "repository",
"msgid_plural": "repositories",
"msgstr": ["版本库", "版本库"],
"score": 2,
"description": "Consistency issue: suggest using '仓库' consistently",
"suggest_msgstr": ["仓库", "仓库"]
}
]
}
issue 对象(issues 数组的元素)字段说明:
msgid(复数条目另有可选msgid_plural):原始源文本;msgstr(可选):原译文形式的 JSON 数组(含义同 GETTEXT JSON 条目中的msgstr);suggest_msgstr:建议译文形式的 JSON 数组;必须是数组(如单数为["提交"])。复数条目按序使用多个元素;score:0–3(0 = 严重;1 = 较大;2 = 轻微;3 = 完美,无问题);description:问题的简要概括。
注意示例中 score 0 的用例恰好触犯第六节清单的"术语"项——"commit" 在 po/zh_CN.po 的术语表中明确统一为"提交",正是术语表驱动评审的典型场景。
八、人工译者始终拥有最终决定权
文档以一条原则收尾:Git 的翻译是人工主导的;各语言团队负责人与贡献者对翻译质量和一致性负最终责任。
AI 生成的输出永远应视为草稿,必须由同时理解技术上下文和目标语言的人审核并批准。最好的效果来自 AI 的效率与人类判断、文化洞察、社区协作的结合。这与 po/README.md 中"AI assistants are optional; treat their output as a draft"的表述一致,也与 po/TEAMS 中各语言团队(bg、ca、de、zh_CN、zh_TW 等)均有明确 Leader/Members 的组织结构相呼应——无论是否引入 AI 辅助,语言团队仍是本地化工作流的责任主体。
附录:核心路径速查
| 路径 | 作用 |
|---|---|
| po/AGENTS.md | 本文主体:AI Agent 本地化维护作业说明 |
| po/README.md | l10n 贡献总指南(流程、约定、i18n 接口、PO helper) |
| po/TEAMS | 各语言团队仓库与负责人名录 |
| po/zh_CN.po | 简体中文 PO 实例(含完整术语表) |
| Makefile | po/git.pot、po/git-core.pot、po-init、po-update 的构建规则 |
| config.c | 2.9 节占位符重排示例的真实 msgid 来源 |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00