Django 多语言处理:"Polish" 被误译为 "波兰语" 的悲剧(上下文标记)

当你真正支持多语言(i18n)时,往往会遇到类似的情况。

  • 按钮上写着 “Polish”(意为“润色”),但翻译结果却是 “波兰语”
  • 日期选择器里的 “May” 在某些语言中被翻译成了人名
  • 菜单里的 “Book” 只被翻译成 “书”,而没有被翻译成 “预订”

问题的根源很简单。

计算机不懂 “上下文”。 当字符串相同时,它们会被视为完全相同。

本文将介绍如何利用 Django 的 上下文标记,在保持源代码不变的前提下,为同一字符串提供不同的翻译。


为什么会出现这种情况?gettext 的基本行为



Django 的翻译系统内部使用 GNU gettext。gettext 的工作原理非常简单:

  • 原文字符串 → msgid
  • 翻译字符串 → msgstr
  • 如果 msgid 相同,则始终使用同一个 msgstr
# django.po

msgid "Polish"
msgstr "波兰语"

一旦在 .po 文件中出现了上述映射, 无论在何处使用 “Polish”,都会得到同样的翻译。

这导致以下两种 UI 无法区分:

  • “Polish” = 动词(润色)
  • “Polish” = 语言/国家名称

为避免这种冲突,有时会把源代码改成:

<!-- 不推荐的写法 -->
{% trans "Polish (verb)" %}
{% trans "Polish (language)" %}

虽然翻译可以正常,但 用户看到的字符串本身就会变得怪异,或者在其他地方复用时会出现问题。

我们想要的是:

  • 源字符串保持 "Polish"
  • 单独说明当前使用的是哪种含义

这正是 上下文标记(Contextual Marker) 的作用。


在模板中解决:{% translate %} + context

在 Django 模板中,可以给 {% translate %}(或旧式 {% trans %})标签添加 context 选项,以按上下文区分同一字符串。

1) 原始代码(冲突)

{% load i18n %}

<button>{% translate "Polish" %}</button>
<span>{% translate "Polish" %}</span>

.po 文件中,这两处都对应 msgid "Polish", 只能使用同一种翻译。

2) 改进代码(使用 context)

{% load i18n %}

<button>
  {% translate "Polish" context "verb: to refine UI" %}
</button>

<span>
  {% translate "Polish" context "language name" %}
</span>

关键点:

  • context 后的字符串 不会显示给用户
  • 仅为翻译系统和翻译者提供 元信息
  • 简短、清晰的描述更易于理解。

  • "verb: to refine UI"

  • "language name"
  • "menu label"
  • "button text"

3) {% blocktranslate %} 也可以使用

当句子较长时,可以使用 {% blocktranslate %}(或 {% blocktrans %}),同样可以添加 context

{% load i18n %}

{% blocktranslate context "greeting message" with username=user.username %}
  Hello {{ username }}
{% endblocktranslate %}

这样,即使是同样的 "Hello %(username)s" 句子,也能在不同上下文中使用多次。


在 Python 代码中解决:pgettext



在模板之外(如视图、模型、表单等)使用 gettext 的地方,需要改用 pgettext 系列函数。

常用的有:

  • pgettext(context, message)
  • pgettext_lazy(context, message) – 延迟求值(模型字段、模块级别等)
  • npgettext(context, singular, plural, number) – 同时处理复数和上下文

1) 基本示例

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

模型字段的 verbose_namehelp_text,或 choices 中的标签也常会出现同音异义问题。

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

当同一词在复数形式时意义不同,可使用 npgettext 同时处理上下文和复数。

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 文件会是什么样子?

使用 context 后,执行:

python manage.py makemessages -l ko

.po 文件会出现 msgctxt 字段。

# 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 的技巧

Context 字符串对用户不可见,但对翻译者是唯一的提示。建议遵循以下准则。

1) 像描述“角色”一样写

  • "button label" – 按钮文本
  • "menu item" – 导航菜单
  • "tooltip" – 鼠标悬停文本
  • "error message" – 错误信息
  • "form field label" – 表单标签

描述 UI 角色能让几乎所有语言在自动翻译后保持意义。

2) 再写一次“概念”

同一词在不同概念中使用时:

  • "File"
  • "file menu item"
  • "uploaded file object"

  • "Order"

  • "e-commerce order"
  • "sorting order"

加入领域概念可让多语言翻译更稳定。

3) 不要把翻译结果写进 Context

Context 仅为说明。若把翻译结果写进去:

  • 随着语言增多,维护成本大幅上升
  • 自动翻译系统可能会混淆

始终以原文(通常是英文)为基准,简短说明概念/角色。


何时使用上下文标记?

以下情况几乎一定要考虑使用 context/pgettext

  1. 短字符串(1–2 词) * 按钮文本 * 选项卡名称 * 菜单项
  2. 在 UI 中以不同含义复用的字符串 * "Open""Close""Save" 等通用动作 * "Book""Order""Post" 等既可作名词又可作动词
  3. 翻译者无法直接查看 UI * 使用协作翻译平台 * 仅向外部翻译公司提供 .po 文件

小结

开发者因同音异义翻译而困惑的样子

  • Django 默认将同一 msgid 只映射一次翻译。
  • 因此,"Polish""May""Book" 等同音异义词经常导致翻译冲突。
  • 解决方案:
  • 模板中使用 {% translate "…" context "…" %}
  • Python 代码中使用 pgettext / pgettext_lazy / npgettext
  • 通过添加 msgctxt.po 文件中同一原文可拥有多种翻译

这样既保持了代码可读性,又显著提升了翻译质量和维护性。

在多语言支持日益重要的时代,上下文标记 是 Django 开发者必备的 i18n 工具。


值得一读的相关文章