# Django 多语言处理:"Polish" 被误译为 "波兰语" 的悲剧(上下文标记)
当你真正支持多语言(i18n)时,往往会遇到类似的情况。
* 按钮上写着 **“Polish”**(意为“润色”),但翻译结果却是 **“波兰语”**
* 日期选择器里的 **“May”** 在某些语言中被翻译成了人名
* 菜单里的 **“Book”** 只被翻译成 “书”,而没有被翻译成 “预订”
问题的根源很简单。
> 计算机不懂 **“上下文”**。
> 当字符串相同时,它们会被视为完全相同。
本文将介绍如何利用 Django 的 **上下文标记**,在保持源代码不变的前提下,为同一字符串提供不同的翻译。
---
## 为什么会出现这种情况?gettext 的基本行为 {#sec-aff43a618865}
Django 的翻译系统内部使用 **GNU gettext**。gettext 的工作原理非常简单:
* 原文字符串 → `msgid`
* 翻译字符串 → `msgstr`
* **如果 `msgid` 相同,则始终使用同一个 `msgstr`**
```po
# django.po
msgid "Polish"
msgstr "波兰语"
```
一旦在 `.po` 文件中出现了上述映射,
无论在何处使用 “Polish”,都会得到同样的翻译。
这导致以下两种 UI 无法区分:
* “Polish” = 动词(润色)
* “Polish” = 语言/国家名称
为避免这种冲突,有时会把源代码改成:
```html
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}
```
虽然翻译可以正常,但
**用户看到的字符串本身就会变得怪异**,或者在其他地方复用时会出现问题。
我们想要的是:
* 源字符串保持 `"Polish"`
* **单独说明当前使用的是哪种含义**
这正是 **上下文标记(Contextual Marker)** 的作用。
---
## 在模板中解决:`{% translate %}` + `context` {#sec-e28151952ba2}
在 Django 模板中,可以给 `{% translate %}`(或旧式 `{% trans %}`)标签添加 `context` 选项,以按上下文区分同一字符串。
### 1) 原始代码(冲突) {#sec-40afc992a96b}
```html
{% load i18n %}
{% translate "Polish" %}
```
在 `.po` 文件中,这两处都对应 `msgid "Polish"`,
只能使用同一种翻译。
### 2) 改进代码(使用 context) {#sec-4fa05f04b9d3}
```html
{% load i18n %}
{% translate "Polish" context "language name" %}
```
关键点:
* `context` 后的字符串 **不会显示给用户**。
* 仅为翻译系统和翻译者提供 **元信息**。
* 简短、清晰的描述更易于理解。
* `"verb: to refine UI"`
* `"language name"`
* `"menu label"`
* `"button text"` 等
### 3) `{% blocktranslate %}` 也可以使用 {#sec-c2a9f27dea01}
当句子较长时,可以使用 `{% blocktranslate %}`(或 `{% blocktrans %}`),同样可以添加 `context`。
```html
{% load i18n %}
{% blocktranslate context "greeting message" with username=user.username %}
Hello {{ username }}
{% endblocktranslate %}
```
这样,即使是同样的 `"Hello %(username)s"` 句子,也能在不同上下文中使用多次。
---
## 在 Python 代码中解决:`pgettext` {#sec-3a9a008d3f99}
在模板之外(如视图、模型、表单等)使用 `gettext` 的地方,需要改用 **`pgettext`** 系列函数。
常用的有:
* `pgettext(context, message)`
* `pgettext_lazy(context, message)` – 延迟求值(模型字段、模块级别等)
* `npgettext(context, singular, plural, number)` – 同时处理复数和上下文
### 1) 基本示例 {#sec-27f89a09cea2}
```python
from django.utils.translation import pgettext
def my_view(request):
# 1. 月份 "May"
month = pgettext("month name", "May")
# 2. 人名 "May"
person = pgettext("person name", "May")
# 3. 助动词 "may" (~可能)
verb = pgettext("auxiliary verb", "may")
```
这样,虽然 `msgid` 都是 `"May"` 或 `"may"`,
但不同的 `context` 让它们拥有不同的翻译。
### 2) 在模型中使用 `pgettext_lazy` {#sec-11832a96275e}
模型字段的 `verbose_name`、`help_text`,或 `choices` 中的标签也常会出现同音异义问题。
```python
from django.db import models
from django.utils.translation import pgettext_lazy
class Order(models.Model):
# "Order" = 订单
type = models.CharField(
verbose_name=pgettext_lazy("order model field", "Order type"),
max_length=20,
)
STATUS_CHOICES = [
# "Open" = 状态
("open", pgettext_lazy("order status", "Open")),
# "Open" = 动作(打开)
("opened", pgettext_lazy("log action", "Open")),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
)
```
如上所示:
* 同一 `"Open"` 也能被区分为 **状态** 与 **动作**。
### 3) 同时处理复数:`npgettext` {#sec-ed84f3cf8d15}
当同一词在复数形式时意义不同,可使用 `npgettext` 同时处理上下文和复数。
```python
from django.utils.translation import npgettext
def get_notification(count):
# 假设 "Message" 用于通知
return npgettext(
"user notification", # context
"You have %(count)d message", # singular
"You have %(count)d messages", # plural
count
) % {"count": count}
```
---
## `.po` 文件会是什么样子? {#sec-fb610023c81a}
使用 `context` 后,执行:
```bash
python manage.py makemessages -l ko
```
`.po` 文件会出现 `msgctxt` 字段。
```po
# django.po
# 1) "Polish" = UI 润色(动词)
msgctxt "verb: to refine UI"
msgid "Polish"
msgstr "润色"
# 2) "Polish" = 语言/国家名称
msgctxt "language name"
msgid "Polish"
msgstr "波兰语"
```
`msgid` 相同,但由于 `msgctxt` 不同,
翻译工具(Poedit、Weblate、Crowdin 等)会将它们视为**不同条目**。
翻译者可以更直观地了解每个字符串在 UI 中的用途。
关键点:
* **源代码中的字符串完全不变**(仍为 `"Polish"`)
* 仅 `.po` 文件结构更智能。
---
## 编写优质 Context 的技巧 {#sec-df37b35ab408}
Context 字符串对用户不可见,但对翻译者是**唯一的提示**。建议遵循以下准则。
### 1) 像描述“角色”一样写 {#sec-9dfe29490c86}
* `"button label"` – 按钮文本
* `"menu item"` – 导航菜单
* `"tooltip"` – 鼠标悬停文本
* `"error message"` – 错误信息
* `"form field label"` – 表单标签
描述 UI 角色能让几乎所有语言在自动翻译后保持意义。
### 2) 再写一次“概念” {#sec-135dce4b280b}
同一词在不同概念中使用时:
* `"File"`
* `"file menu item"`
* `"uploaded file object"`
* `"Order"`
* `"e-commerce order"`
* `"sorting order"`
加入**领域概念**可让多语言翻译更稳定。
### 3) 不要把翻译结果写进 Context {#sec-9197b1a38f27}
Context 仅为说明。若把翻译结果写进去:
* 随着语言增多,维护成本大幅上升
* 自动翻译系统可能会混淆
始终以**原文(通常是英文)**为基准,简短说明概念/角色。
---
## 何时使用上下文标记? {#sec-58e84d2a2b4c}
以下情况几乎一定要考虑使用 `context`/`pgettext`:
1. **短字符串(1–2 词)**
* 按钮文本
* 选项卡名称
* 菜单项
2. **在 UI 中以不同含义复用的字符串**
* `"Open"`、`"Close"`、`"Save"` 等通用动作
* `"Book"`、`"Order"`、`"Post"` 等既可作名词又可作动词
3. **翻译者无法直接查看 UI**
* 使用协作翻译平台
* 仅向外部翻译公司提供 `.po` 文件
---
## 小结 {#sec-8cc5759d0812}

* Django 默认将同一 `msgid` 只映射一次翻译。
* 因此,`"Polish"`、`"May"`、`"Book"` 等同音异义词经常导致翻译冲突。
* 解决方案:
* 模板中使用 `{% translate "…" context "…" %}`
* Python 代码中使用 `pgettext` / `pgettext_lazy` / `npgettext`
* 通过添加 `msgctxt`,`.po` 文件中同一原文可拥有多种翻译
这样既保持了代码可读性,又显著提升了翻译质量和维护性。
在多语言支持日益重要的时代,**上下文标记** 是 Django 开发者必备的 i18n 工具。
---
**值得一读的相关文章**
- [Django 中 gettext 与 gettext_lazy 的区别:从评估时点理解](/ko/whitedec/2026/1/5/django-gettext-vs-gettext-lazy/)
- [将 gettext_lazy 用于 JSON 键时出现的问题与解决方案](/ko/whitedec/2025/4/26/gettext-lazy-json-key-problem-solution/)