# General Translation Knowledge Base: 如何翻译 PO 和 POT 文件？
URL: https://generaltranslation.com/zh/kb/translate-po-files.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 填写每一个 msgstr（包括 Plural-Forms 头部字段要求的每种复数形式），并保持 msgid、msgctxt 和占位符不变。运行 msgfmt --check 可检查头部信息、复数形式数量，以及带有 format 标志的条目中的占位符；此外还需单独检查是否有空的 msgstr。General Translation 的 CLI 会根据你的 POT 模板，为每种语言生成一个翻译好的 PO 文件。
Category: 翻译
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

POT (Portable Object Template) 文件保存从代码中提取的源字符串，而每个 PO (Portable Object) 文件则将这些字符串与某一种语言的译文一一对应。翻译就是填写每一个 `msgstr` (该语言有几种复数形式，就要填几个) ，同时让 `msgid`、`msgctxt` 和占位符与提取时保持完全一致。之后，再将每个 PO 文件编译成程序加载的二进制 `.mo` 目录。这种格式虽然简单，出错时却悄无声息：凡是 gettext 无法使用的内容，它都会直接显示源文本。

## POT 和 PO 文件从何而来

所有基于 gettext 的技术栈都遵循相同的四个步骤，只是命令名称各不相同：

| 步骤               | GNU gettext | Babel (Flask、FastAPI)  |
| ---------------- | ----------- | ---------------------- |
| 将源字符串提取到模板中      | `xgettext`  | `pybabel extract`      |
| 为新语言创建 PO 文件     | `msginit`   | `pybabel init`         |
| 将模板变更合并到现有 PO 文件 | `msgmerge`  | `pybabel update`       |
| 将 PO 文件编译为 `.mo` | `msgfmt`    | `pybabel compile`      |

Django 将前三步合并为 `makemessages`，直接生成 PO 文件，再通过 `compilemessages` 进行编译。翻译工作在合并之后、编译之前进行：模板本身不对应任何语言，因此只有 PO 文件才包含译文。 (完整的 gettext 流程，包括源字符串变更时 `msgmerge` 如何处理，请参阅 [General Translation 与 GNU gettext 对比](/kb/general-translation-vs-gettext)。)

## PO 条目包含哪些内容

下面是一个波兰语 PO 文件：先用 `xgettext` 从一个简短的 Python 脚本中提取出模板，再由 General Translation 的 CLI 据此生成 (文件头已略作删减) ：

```text title="po/pl.po"
msgid ""
msgstr ""
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;\n"

#: inbox.py:8
#. TRANSLATORS: Heading of the inbox page.
msgid "Inbox"
msgstr "Skrzynka odbiorcza"

#: inbox.py:9
#, python-format
msgid "You have %(num)d unread message."
msgid_plural "You have %(num)d unread messages."
msgstr[0] "Masz %(num)d nieprzeczytaną wiadomość."
msgstr[1] "Masz %(num)d nieprzeczytane wiadomości."
msgstr[2] "Masz %(num)d nieprzeczytanych wiadomości."

#: inbox.py:10
msgctxt "email action"
msgid "Archive"
msgstr "Archiwizuj"
```

[GNU gettext 手册](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html)对各部分的定义如下：

* **`msgid` 和 `msgid_plural`** 是源文本，其中 `msgid` 同时也是查找键。切勿在 PO 文件中修改它们：修改后的 `msgid` 在代码中匹配不到任何内容，用户看到的就会是源文本。
* **`msgstr`** 是译文，也是你唯一需要填写的行。复数条目包含从 `msgstr[0]` 到“该语言复数形式数量减一”的各行。
* **`msgctxt`** 用于区分文字相同但含义不同的源字符串，例如作为邮件操作的“Archive” (归档) 与作为归档文件夹的“Archive”。它来自 `pgettext`，每个上下文都有各自的翻译，且应保持提取时的原样。
* **`#.` 提取注释**是开发者留下的备注 (由 `xgettext --add-comments` 从代码中提取) ，**`#:` 引用**指向源代码行，**`# ` 译者注释**则由译者自行填写。
* **`#,` 标志**分为两类。`fuzzy` 是工作流标志，表示译文可能有误，需要人工检查。`c-format`、`python-format` 等格式标志由 `xgettext` 设置，用于告知 `msgfmt` 比对源文本与译文中的占位符。
* **文件头**是 `msgid` 为空的条目。其中的 `Language`、`Content-Type` 字符集和 `Plural-Forms` 字段作用于整个文件。

## Plural-Forms 如何决定译文条数

`Plural-Forms` 给出复数形式的数量 (`nplurals`) ，以及一个将计数 `n` 映射到形式索引的类 C 表达式 ([GNU gettext 手册：复数形式](https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html)) 。在 GNU gettext 1.0 中，`msginit` 为日语写入 `nplurals=1; plural=0;`，为德语写入 `nplurals=2; plural=(n != 1);`，为法语写入 `nplurals=2; plural=(n > 1);` (法语中零按单数处理) ，为波兰语和俄语则写入三种形式。阿拉伯语需要六种。而对于阿拉伯语，同样的 `msginit` 会在文件头中留下占位符 `nplurals=INTEGER; plural=EXPRESSION;`，因此在开始翻译之前，务必先检查文件头。

编译好波兰语文件后，Python 的 `ngettext` 会根据计数选择不同的槽位：

```text title="Output"
count  slot        output
1      msgstr[0]   Masz 1 nieprzeczytaną wiadomość.
3      msgstr[1]   Masz 3 nieprzeczytane wiadomości.
5      msgstr[2]   Masz 5 nieprzeczytanych wiadomości.
22     msgstr[1]   Masz 22 nieprzeczytane wiadomości.
```

英语源文本只有两种形式，因此如果逐条翻译成两个波兰语字符串，`msgstr[2]` 在数量为 5 或 12 时就没有正确的内容可以显示。

## 翻译 PO 文件时容易出错的地方

以下问题在编译或运行时都不会导致崩溃，因此常常被带入生产环境：

* **fuzzy 条目永远不会呈现给用户。** 除非传入 `--use-fuzzy`，否则 `msgfmt` 不会把它们写入 `.mo` 文件，而手册称这种做法&quot;通常是错误的&quot;。将复数条目标记为 fuzzy 后，波兰语构建输出的是英文的 &quot;You have 5 unread messages.&quot;。请逐一审校每个 fuzzy 条目，然后移除该标志。
* **空的复数槽位可能什么都不输出。** `msgfmt` 编译了一个 `msgstr[2]` 为空的条目，并将其计为已翻译，结果 Python 在计数为 5 时输出了一个空行。只有当条目带有格式标志时，`--check` 才会拒绝它，因为此时空槽位缺少占位符；若没有标志，该条目就能通过检查。
* **占位符被遗漏或改名。** 缺少 `%(num)d` 的译文会丢失计数。`msgfmt --check` 只能在带有格式标志的条目上发现这一问题。
* **引号和转义采用 C 语言风格。** `msgstr` 中的 `"` 必须写成 `\"`；未转义的引号会导致语法错误，字符串中的换行应写成 `\n`。
* **模板中的字符集只是一个占位符。** `xgettext` 会在 POT 头中写入 `charset=CHARSET`，而 `msgfmt --check` 会拒绝仍保留该值的 PO 文件。`msginit` 和 `pybabel` 会写入 UTF-8；如果某个工具直接翻译模板的副本，请先对模板运行 `msgconv --to-code=UTF-8`。

`msgfmt --check` 会报告它能检测到的问题，`--statistics` 则会统计最终进入构建的内容：

```text title="msgfmt --check"
$ msgfmt --check -o /dev/null missing-placeholder.po
missing-placeholder.po:30: a format specification for argument 'num' doesn't exist in 'msgstr[2]'
msgfmt: found 1 fatal error

$ msgfmt --check -o /dev/null two-forms.po
two-forms.po:7: nplurals = 3...
two-forms.po:30: ...but some messages have only 2 plural forms
msgfmt: found 1 fatal error

$ msgfmt --check --statistics -o inbox.mo fuzzy.po
2 translated messages, 1 fuzzy translation.
```

在 CI 中对每个 PO 文件运行它，只要 fuzzy 计数大于零，就视为工作尚未完成。

没有任何 gettext 工具会报告只有一个空槽位的复数条目：如果该条目没有格式标志，`msgfmt --check` 会直接放行，`msgattrib --untranslated` 也会将其跳过。用 [Babel](https://babel.pocoo.org/) (`pip install babel`) 写一个简短的脚本，即可在发现任何空翻译时让构建失败：

```python title="check_po.py"
import sys

from babel.messages.pofile import read_po

failed = False
for path in sys.argv[1:]:
    with open(path, "rb") as f:
        catalog = read_po(f)
    for message in catalog:
        if not message.id or message.fuzzy:
            continue  # 跳过文件头条目，以及 msgfmt 本来就会忽略的 fuzzy 条目
        forms = message.string if isinstance(message.string, tuple) else (message.string,)
        if not all(forms):
            print(f"{path}:{message.lineno}: empty translation for {message.id!r}")
            failed = True
sys.exit(1 if failed else 0)
```

```text title="Output"
$ python check_po.py po/pl.po
po/pl.po:17: empty translation for ('You have %(num)d unread message.', 'You have %(num)d unread messages.')
```

## 翻译 PO 文件的几种方式

| 方式                                                                                  | 由谁翻译                      | 复数形式槽位                      | 译者可获得的上下文                           | 模板变更时                                                                                                     |
| ----------------------------------------------------------------------------------- | ------------------------- | --------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
| 手动编辑或使用 [Poedit](https://poedit.com/features/)                                      | 你自己或译者                    | Poedit 会标记错误的复数形式和缺失的 token | 文件中的注释和引用位置                         | 运行 `msgmerge`，然后审校 fuzzy 条目                                                                               |
| [Weblate](https://docs.weblate.org/en/latest/formats/gettext.html) 或其他翻译管理系统 (TMS)  | 译者，可选配机器翻译建议              | 支持；fuzzy 对应“需要编辑”           | 注释、引用位置，以及 TMS 中的截图或词汇表             | 由其 msgmerge 插件或代码仓库同步更新 PO 文件                                                                             |
| GNU gettext 的 `msgpre`                                                              | 通过 ollama 运行的本地 LLM，生成草稿  | 生成草稿，供译者审校                  | PO 文件本身包含的信息                        | 针对新条目重新运行 ([gettext 1.0 预翻译](https://www.gnu.org/software/gettext/manual/html_node/Pretranslating.html))  |
| 基于机器翻译 API 或 LLM 编写的脚本                                                              | 模型                        | 你的脚本必须输出文件头要求的每个槽位          | 取决于你的脚本发送了什么                        | 重新运行，并自行验证输出                                                                                              |
| [General Translation 的 CLI](/docs/cli/reference/formats/po-pot-files)               | General Translation 的翻译服务 | 覆盖目标语言的所有槽位，并检查占位符          | `msgctxt`、注释、引用位置、标志，以及你的词汇表和自定义提示词 | 复用未变更的译文，仅翻译新增或变更的条目                                                                                      |

Poedit 的机器预翻译是 Pro 功能；编辑、翻译记忆和各项检查在免费版中即可使用。 (TMS 往返流程的对比，请参阅 [General Translation 与 Weblate 对比](/kb/general-translation-vs-weblate)。) 

如果你自己翻译一两种语言，请使用 Poedit。如果由专业译者负责消息目录，且需要任务分配和审校队列，请使用 Weblate 或其他 [TMS](/kb/what-is-a-translation-management-system)。只有在你愿意自行负责验证时，才使用脚本或 `msgpre`。如果希望每次构建时都将 PO 文件翻译成所有语言，并与其他产品共享术语，请使用 General Translation。

## 使用 General Translation 翻译 PO 文件

General Translation 的 CLI `gt` 会读取 POT (或 source-language PO) 文件，并为每个区域设置生成一个翻译后的 PO 文件。请在 `gt.config.json` 中添加 `pot` 条目。以下配置已在 `gt` 2.26.11 中验证，CLI 自身的路径解析会将 `po/messages.pot` 映射为 `po/de.po`、`po/pl.po` 和 `po/pt_BR.po`：

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["de", "pl", "pt_BR"],
  "customMapping": { "pt_BR": { "code": "pt-BR" } },
  "files": {
    "pot": {
      "include": ["po/messages.pot"],
      "transform": { "match": "^po/messages\\.pot$", "replace": "po/{locale}.po" },
      "transformationFormat": "PO"
    }
  }
}
```

* **`pot`** 可同时读取 PO 和 POT 文件 (写作 `POT` 也可以) 。`gt` 自 2.14.22 起支持 PO 和 POT。
* **`"transformationFormat": "PO"`** 为必填项。若未设置，输出的仍是未翻译的模板。
* **[`transform`](/docs/cli/reference/config#locale-placeholders)** 用于为每个输出文件命名；`{locale}` 即 `locales` 中所写的区域设置，[`customMapping`](/docs/cli/reference/config#custom-mapping) 则将 gettext 的 `pt_BR` 映射到标准代码 `pt-BR`。
* **Babel 和 Django 布局**无需使用 `transform`：将 `include` 指向英文目录，并在其所在文件夹的路径中使用 `[locale]` 占位符，例如 `translations/` 下的 `[locale]/LC_MESSAGES/*.po`，CLI 便会将各语言的目录写入相邻位置，例如 `pl/LC_MESSAGES/messages.po`。

然后在提取与编译之间运行 CLI：

```bash
xgettext --language=Python --keyword=pgettext:1c,2 --add-comments=TRANSLATORS -o po/messages.pot app.py
msgconv --to-code=UTF-8 -o po/messages.pot po/messages.pot
npx gt translate
msgfmt --check -o locale/pl/LC_MESSAGES/messages.mo po/pl.po
```

请先通过 [`npx gt login`](/docs/cli/reference/commands/login) 和项目 ID 配置凭据，在 CI 中则使用项目 API 密钥 (参见 [CLI 凭据](/docs/cli/guides/configuring#credentials)) 。如果你的 PO 文件中已有审校过的译文，请在首次运行 [`npx gt translate`](/docs/cli/reference/commands/translate) 之前，先用 [`npx gt upload`](/docs/cli/reference/commands/upload) 导入这些译文。

对于每个条目，General Translation 会翻译 `msgid` 文本，并将该条目的 `msgctxt`、`#.` 注释、`#:` 引用位置和标志一并作为上下文发送。它会设置输出文件的 `Language` 头；如果文件包含复数条目，还会写入目标语言的 `Plural-Forms` 规则，并按该规则填充所需的全部 `msgstr[n]`：波兰语三个，阿拉伯语六个。每条复数译文都会经过检查，确认没有空缺项，且各个形式都未遗漏 `%(num)d`、`%s` 或 `{name}` 等占位符。注释、引用位置、标志和 `msgctxt` 会沿用模板中的内容，未更改的条目在下次运行时会直接复用之前的译文。

由于源是 `msgid`，因此最好在代码中以源文本作为键 (这也是 gettext 的默认做法) 。审校人员可以[在仪表板中编辑条目](/docs/platform/dashboard/guides/reviewing-translations)，而借助[上下文分组](/docs/platform/dashboard/guides/defining-context-for-translations)中的词汇表和自定义提示词，可以让 &quot;Archive&quot; 这类术语与你的 Web 应用和文档保持一致。如果有人在本地编辑了 PO 文件，请在下次运行前使用 [`npx gt save-local`](/docs/cli/reference/commands/save-local) 进行同步。有关特定框架的配置，请参阅 [General Translation 与 Django 翻译框架对比](/kb/general-translation-vs-django-i18n)、[General Translation 与 Babel 和 Flask-Babel 对比](/kb/general-translation-vs-flask-babel)，以及[如何翻译 Flask 或 FastAPI 应用？](/kb/translate-python-app)。

## 常见问题

### PO 文件和 POT 文件有什么区别？

POT 文件是模板：所有 `msgstr` 均为空，文件头中也不指定语言。PO 文件则是针对某一种语言的副本，包含 `Language` 文件头、该语言的 `Plural-Forms` 规则以及译文。

### 如果 General Translation 负责写入 PO 文件，我还需要 msgmerge 吗？

如果以 POT 模板作为源，目标语言就不再需要。每次运行都会基于当前模板重新写入每种语言的 PO 文件：代码中已删除的条目会自动移除，新增条目则直接带有翻译，也不会留下需要清理的 fuzzy 条目。对于 Babel 和 Django 布局，仍需通过 `pybabel update` 或 `makemessages` 更新英文目录。

### 为什么已经翻译了 PO 文件，应用仍然显示英文？

通常是以下三种原因之一：该条目仍被标记为 `fuzzy`；`.mo` 文件没有重新编译或重新部署；或者 PO 文件中的 `msgid` 与代码中的字符串已不再匹配。运行 `msgfmt --statistics` 可以查看已翻译条目和 fuzzy 条目各有多少。

## 要点总结

* 只翻译 `msgstr`；`msgid`、`msgid_plural` 和 `msgctxt` 是查找键，应保持提取时的原样。
* 按照文件头中 `nplurals` 指定的数量编写 `msgstr[n]` 槽位：日语一个，德语两个，波兰语三个，阿拉伯语六个。
* `msgfmt` 会丢弃 fuzzy 条目，而且会照常编译空的复数槽位，导致运行时什么也不输出；`--check` 只能在带有格式标志的条目上发现这一问题，因此请对每个 PO 文件运行 `msgfmt --check --statistics`，并另行检查是否存在空译文。
* General Translation 的 CLI 通过 `"pot"`、`"transformationFormat": "PO"` 和 `transform`，将 POT 模板翻译为各区域设置对应的 PO 文件，并按目标语言填充所有复数槽位。

## Related questions

- [General Translation 与 GNU gettext：两者都需要吗？](https://generaltranslation.com/zh/kb/general-translation-vs-gettext.mdx): 继续用 GNU gettext 负责提取和渲染，再引入 General Translation 来生成和维护 PO 文件。General Translation 会把译者备注和消息上下文纳入生成过程，统一应用共享术语，并为审校人员提供 仪表板 编辑器。你现有的 gettext 调用和已编译的消息目录均保持原样。
- [General Translation 与 Django 翻译框架：两者都需要吗？](https://generaltranslation.com/zh/kb/general-translation-vs-django-i18n.mdx): 将 Django 的翻译框架与 General Translation 搭配使用。Django 负责消息提取、语言选择和渲染；General Translation 则基于你的产品术语和仪表板审阅，填充 Django 的 PO 文件（包括复数形式）。你的模板和 URL 模式无需改动，译文会随代码同步更新。
- [General Translation 与 Babel、Flask-Babel 对比：该选哪一个？](https://generaltranslation.com/zh/kb/general-translation-vs-flask-babel.mdx): Babel 可以为任意区域设置格式化日期、数字和货币，并管理 gettext 消息目录；Flask-Babel 则负责将其集成到 Flask 中，但两者都不会翻译你的字符串。General Translation 的 CLI 会为每种语言填充 Babel 的 PO 文件（包括复数形式），因此你可以继续使用 Flask-Babel，不必再手动翻译消息目录。
- [如何翻译 Flask 或 FastAPI 应用？](https://generaltranslation.com/zh/kb/translate-python-app.mdx): 标记面向用户的字符串，为每个请求确定区域设置（通常依据 Accept-Language），然后加载该区域设置的译文。Flask 应用通常使用 Flask-Babel 搭配 gettext 消息目录，FastAPI 应用则通过中间件接入 Babel。General Translation 的 CLI 可以填充这些消息目录，而其实验性的 gt-flask 和 gt-fastapi SDK 无需消息目录，即可翻译内联的 t() 字符串。
- [General Translation 与 Weblate：该选哪一个？](https://generaltranslation.com/zh/kb/general-translation-vs-weblate.mdx): Weblate 是一个可自托管、基于 Git 的本地化平台，专为社区或团队协作翻译代码仓库文件而设计。General Translation 则将译文生成融入开发与发布流程，依托已标记的 JSX 和共享的产品上下文完成翻译。你可以继续沿用现有的 gettext 文件，也可以改用内联编写，连源文案目录的维护工作也一并省去。

## Previous and next questions

- Previous question: [如何翻译 Lottie 动画？](https://generaltranslation.com/zh/kb/translate-lottie-animations.mdx)
- Next question: [如何翻译 SEO 标题和meta 描述？](https://generaltranslation.com/zh/kb/translate-seo-metadata.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/zh/kb/sitemap.md) for every article.
