# 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} ![开发者因同音异义翻译而困惑的样子](/media/editor_temp/6/14112b82-d049-4b32-9275-02429e9305c0.png) * 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/)