首页
/ Git 翻译本地化自动化工作流:基于 po/AGENTS.md 的 PO 文件维护、AI 辅助翻译与质量评审全解

Git 翻译本地化自动化工作流:基于 po/AGENTS.md 的 PO 文件维护、AI 辅助翻译与质量评审全解

2026-09-04 21:12:47作者:冯梦姬Eddie

本文以 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.pozh_TW.pode.po 等),以及记录各语言团队联系人的 TEAMS 文件。

AGENTS.md 将任务分为四块:

  1. 生成或更新 po/git.pot(模板文件);
  2. 更新 po/XX.po(同步上游新字符串);
  3. 翻译 po/XX.po
  4. 评审翻译质量。

其中 3、4 两条流程包含带状态文件的循环工作流和权威 shell 脚本,是全文最核心的实战部分。

二、本地化工作流的背景知识

文档要求:执行任何维护任务前,必须先理解以下概念。这些概念是后文所有脚本与规则的基础。

2.1 语言代码记法(XX、ll、ll_CC)

XX 是语言代码占位符,取两种形式之一:ll(ISO 639 两字母语言代码)或 ll_CC(语言+地区,如 dezh_CN)。它出现在 PO 文件头部元数据中(如 "Language: zh_CN\n"),并用于 PO 文件命名:po/XX.popo/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_pluralmsgstr[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
  • fuzzymsgattrib --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]),绝不修改 msgidmsgid_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 解析器会把它当作字符串结束,导致:

  1. 字符串截断msgstr 值在多余的引号处被截短;
  2. 语法错误msgfmt --check 在该行报解析错误;
  3. 数据丢失:误引号之后的内容被错误解析或丢失。

规则

  • 永远不要用 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 时:

  1. 直接执行 make po/git.pot,执行前不必检查文件是否存在;
  2. 执行后不做验证,跑完即视为任务完成。

这条"直接执行、不验证"的指令看似简单,其背后是 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 时:

  1. 直接执行 make po-update PO_FILE=po/XX.po,执行前不必读取或检查文件内容;
  2. 执行后不做验证、翻译或评审,跑完即完成。

对照 Makefilepo-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 JSONgit-po-helper 定义的内部格式,专为 AI 模型批量处理翻译及相关任务而设计;msg-selectmsg-catcompare 均能读写该格式。

顶层结构

{
  "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_commentheader_metamsgidmsgid_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.jsonpo/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 中读写时对这些序列做正确的转义/反转义。只修改 msgstrmsgstr[n](复数条目);清除 fuzzy 标志(省略或置 false)。不得修改 msgidmsgid_plural

4b. 翻译 PO 批次po/l10n-todo.popo/l10n-done.po):

  • 任务:把 po/l10n-todo.po(输入,GETTEXT PO)翻译为 po/l10n-done.po(输出,GETTEXT PO);
  • 参考术语表:从 pending 文件头部读取术语表(见 2.3 节),用于统一术语;
  • 翻译要求:遵循质量检查清单。与 msgid 一样保留转义序列、占位符、引号。只修改 msgstrmsgstr[n];完成后从注释中删除 #, fuzzy 标签。不得修改 msgidmsgid_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 showgit diffgit 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_commentheader_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.potpo/git-core.potpo-initpo-update 的构建规则
config.c 2.9 节占位符重排示例的真实 msgid 来源
登录后查看全文
热门项目推荐
相关项目推荐