返回博客

Django 翻译失效的原因(以及如何修复 10 个最常见问题)

2026-02-04 5 分钟阅读
Django 翻译失效的原因(以及如何修复 10 个最常见问题)

你标记了需要翻译的字符串,生成了 .po 文件,运行了 compilemessages,但你的应用仍然显示英文。你并不孤单。Django 的 i18n 框架功能强大,但即使是经验丰富的开发者也会踩坑。

本指南涵盖了 Django 翻译静默失败的 10 个最常见原因,以及每个问题的确切症状和修复方法。

1. 编辑 .po 文件后忘记运行 compilemessages

你编辑了一个 .po 文件(手动或使用工具),但翻译后的文本始终不显示。应用一直显示原始的英文字符串。

Django 在运行时不读取 .po 文件,而是读取编译后的 .mo(机器对象)二进制文件。如果你编辑了 .po 文件但没有重新编译,Django 根本不知道有任何变化。

每次修改 .po 文件后都要运行 compilemessages

python manage.py compilemessages

如果你使用 TranslateBot 自动化翻译,请将 compilemessages 作为工作流的最后一步:

python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages

2. 模板中缺少 {% load i18n %}

你在模板中使用了 {% trans "Hello" %},但 Django 抛出了 TemplateSyntaxError。或者更糟的是,如果模板引擎配置不当,标签会静默地不起作用。

{% trans %}{% blocktrans %} 标签位于 Django 的 i18n 模板标签库中。如果不加载它,模板引擎将无法识别这些标签。

在每个使用翻译标签的模板顶部添加 {% load i18n %}

{% load i18n %}

<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>

这是一个逐模板的要求。即使父模板加载了 i18n,使用翻译标签的子模板也需要有自己的 {% load i18n %} 声明。

3. LocaleMiddleware 未添加到 MIDDLEWARE 或位置不正确

无论浏览器的 Accept-Language 头、URL 前缀或会话设置如何,Django 始终以默认语言提供内容。

LocaleMiddleware 决定每个请求的活动语言。如果没有它,Django 将默认使用 LANGUAGE_CODE 并忽略所有语言选择机制。它在中间件栈中的位置也很重要,因为它需要访问会话数据和 URL 解析。

LocaleMiddleware 添加到你的 MIDDLEWARE 设置中,放在 SessionMiddlewareCommonMiddleware 之后:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.locale.LocaleMiddleware",  # Must be after SessionMiddleware
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
]

如果你使用基于 URL 的语言切换,还要确保在 URL 配置中包含 django.conf.urls.i18n

from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path("", include("myapp.urls")),
)

4. 语言代码不匹配(例如 pt-brpt_BR

翻译存在于你的 .po 文件中,compilemessages 成功运行,但 Django 对某些区域设置忽略了翻译。

Django 期望区域目录遵循 <language>_<COUNTRY> 格式,使用下划线分隔符和大写国家代码。例如,巴西葡萄牙语使用 pt_BR。如果你的目录名为 pt-brpt-BRptBR,Django 将找不到它。LANGUAGES 设置也同样适用:其中的代码使用连字符(pt-br),但文件系统使用下划线(pt_BR)。

确保你的目录结构符合 Django 的期望:

locale/
    pt_BR/
        LC_MESSAGES/
            django.po
            django.mo

在你的设置中,使用连字符形式:

LANGUAGES = [
    ("en", "English"),
    ("pt-br", "Brazilian Portuguese"),
    ("zh-hans", "Simplified Chinese"),
]

运行 makemessages 时,使用下划线形式的区域标志:

python manage.py makemessages -l pt_BR

5. 模糊条目在编译时被静默跳过

翻译存在于 .po 文件中,但 Django 在运行时对该特定条目显示原始英文字符串。这个问题特别令人沮丧,因为翻译明明就在文件中。

当 Django 的 makemessages 检测到源字符串发生了轻微变化时,它会将现有翻译标记为"fuzzy"(意味着这是一个需要人工审核的猜测)。compilemessages 命令会跳过所有模糊条目,将它们视为未翻译。因此条目在 .po 文件中看起来已翻译,但 .mo 文件完全排除了它。

一个模糊条目看起来像这样:

#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

审核翻译,如需更新 msgstr,然后删除 #, fuzzy 标志:

msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

然后重新编译:

python manage.py compilemessages

在大型项目中,模糊条目会不断积累且容易被忽略。TranslateBot 的 check_translations 命令可以自动捕获这些问题:

python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed

将它添加到你的 CI 流水线中,使用 check_translations --makemessages,你就再也不会发布模糊条目了。

6. LOCALE_PATHS 未配置或指向错误的目录

makemessages 在一个位置创建 .po 文件,但 Django 在另一个位置查找它们。翻译存在于磁盘上但从未加载。

Django 按特定顺序搜索翻译文件:首先是 LOCALE_PATHS 目录,然后是每个应用的 locale/ 目录,最后是项目的 locale/ 目录。如果 LOCALE_PATHS 未设置或指向错误的路径,Django 可能永远找不到你的 .po 文件。

在你的设置中将 LOCALE_PATHS 设为绝对路径:

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

LOCALE_PATHS = [
    BASE_DIR / "locale",
]

验证目录是否存在并包含预期的结构:

locale/
    de/
        LC_MESSAGES/
            django.po
            django.mo
    nl/
        LC_MESSAGES/
            django.po
            django.mo

一个常见的错误是将 LOCALE_PATHS 设置为 locale/(相对路径)而不是绝对路径。Django 不会从你的项目根目录解析相对路径。它取决于进程的工作目录,这通常不是你所期望的。

7. 缓存提供过期的翻译

你更新并编译了翻译,但旧文本仍然出现。重启服务器可以修复它。

Django 的翻译目录在每个进程中只加载一次。在生产环境中,像 Gunicorn 或 Uvicorn 这样的 WSGI/ASGI 服务器会让工作进程长时间保持活跃。.mo 文件可能已经在磁盘上更改了,但运行中的进程仍然在内存中保存着旧的翻译。此外,如果你使用 Django 的缓存框架或像 Nginx 或 Cloudflare 这样的反向代理,缓存的响应将在过期之前一直提供旧内容。

部署新翻译后重启应用服务器:

# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)

# Systemd
sudo systemctl restart myapp

# Docker
docker compose restart web

对于 Django 的缓存框架,在更新翻译后清除缓存:

from django.core.cache import cache
cache.clear()

在开发环境中,runserver 会在 Python 文件更改时自动重新加载,但不会监视 .mo 文件。运行 compilemessages 后你需要手动重启它。

8. 字符串未用 gettext(_(){% trans %})包裹

makemessages 没有提取某些字符串,因此它们从不出现在你的 .po 文件中,也永远不会被翻译。这是最基本的问题,在大型代码库中也是最容易被忽视的。

Django 的 makemessages 命令使用 xgettext 扫描你的源代码中的翻译标记。如果一个字符串没有被 gettext()(通常别名为 _())、gettext_lazy(){% trans %}{% blocktrans %} 包裹,它对提取过程是不可见的。

包裹每个面向用户的字符串:

# Python code
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
    class Meta:
        verbose_name = _("article")
        verbose_name_plural = _("articles")

# Views
from django.utils.translation import gettext as _

def my_view(request):
    message = _("Your changes have been saved.")
    return HttpResponse(message)
<!-- Templates -->
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>
<p>{% blocktrans with count=items|length %}You have {{ count }} items.{% endblocktrans %}</p>

使用 TranslateBot 的 --dry-run 标志预览当前未翻译的字符串,以便你可以捕获在提取过程中遗漏的字符串:

python manage.py translate --target-lang de --dry-run

这会显示你的 .po 文件中所有未翻译的条目,而不会进行任何 API 调用或更改。

9. f-string 无法被翻译(Django 限制)

你用 _() 包裹了一个 f-string,结果要么得到语法错误,要么 makemessages 提取了一个无法翻译的损坏/不完整的字符串。

Python f-string 在运行时求值。xgettext 提取工具静态解析源代码,因此它无法求值 {} 大括号内的 Python 表达式。这意味着 _(f"Hello, {name}") 被提取为包含字面 {name} 表达式的字符串(或完全无法提取),生成的 .po 条目将永远不会匹配运行时字符串。

使用 Django 的 % 格式化或带命名占位符的 .format() 代替:

# Wrong -- f-string cannot be extracted
message = _(f"Hello, {user.name}! You have {count} new messages.")

# Correct -- named placeholders
message = _("Hello, %(name)s! You have %(count)d new messages.") % {
    "name": user.name,
    "count": count,
}

# Also correct -- .format() with positional args
message = _("Hello, {0}! You have {1} new messages.").format(user.name, count)

这不是 TranslateBot 或工具的限制。这是 gettext 工作原理的根本问题。源字符串必须是静态字面量,以便可以在运行时提取和查找。

TranslateBot 在翻译过程中保留所有这些占位符格式(%(name)s{0}%s、HTML 标签),因此翻译后的字符串保持完全可用。

10. 占位符格式字符串错误导致 .po 编译失败

compilemessages 失败并报错,或者 .po 文件的 #, python-format 标志不匹配,条目被静默丢弃。

当源字符串包含 Python 格式占位符(如 %(name)s)时,Django 会用 #, python-format 标记 .po 条目。如果翻译中有不同的占位符(拼写错误如 %(nome)s、缺少占位符或多余的占位符),gettext 工具可能会拒绝该条目或 compilemessages 可能会失败。这种情况通常发生在手动翻译或不理解占位符语义的 AI 翻译工具中。

一个损坏的条目看起来像这样:

#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."

这里 %(naam)s 应该是 %(name)s。占位符必须与源完全匹配。

确保翻译后的字符串包含与源完全相同的占位符。检查拼写错误、缺少的占位符和多余的占位符。

这是 TranslateBot 提供真正优势的一个领域。它的占位符保留逻辑确保翻译输出中的所有格式字符串(%(name)s{0}%s)与源完全匹配。占位符处理由 100% 的测试覆盖率保障,因此格式字符串错误在工具层面就被消除了,而不是在编译时才被发现。

如果你手动翻译或使用不处理占位符的工具,请用以下命令验证你的 .po 文件:

msgfmt --check-format locale/de/LC_MESSAGES/django.po

这将运行 gettext 的格式字符串验证并报告任何不匹配。

整合所有内容:防御性工作流

这些问题中的大多数有一个共同的根本原因:容易忘记的手动步骤。以下是一个可以预防所有 10 个问题的工作流:

# 1. Extract strings (catches #8 -- any new gettext-wrapped strings)
python manage.py makemessages -a --no-obsolete

# 2. Translate (catches #1, #5, #8, #9, #10 -- handles untranslated,
#    fuzzy, and placeholder issues automatically)
python manage.py translate

# 3. Compile (catches #1 -- generates .mo files)
python manage.py compilemessages

# 4. Verify in CI (catches everything that slipped through)
python manage.py check_translations --makemessages

将第 4 步添加到你的 CI 流水线中,未翻译的字符串、模糊条目和格式错误将在到达生产环境之前使构建失败。

快速参考表

原因 症状 一行修复
未运行 compilemessages 翻译存在但不显示 python manage.py compilemessages
缺少 {% load i18n %} {% trans %}TemplateSyntaxError 在模板中添加 {% load i18n %}
缺少 LocaleMiddleware 语言始终默认为英文 django.middleware.locale.LocaleMiddleware 添加到 MIDDLEWARE
语言代码不匹配 找不到区域目录 目录使用 pt_BR(下划线),设置使用 pt-br(连字符)
模糊条目被跳过 翻译在 .po 中但不在应用中 审核后删除 #, fuzzy 标志
LOCALE_PATHS 错误 .po 文件存在但 Django 忽略它们 LOCALE_PATHS 设为绝对路径
缓存的翻译 更新后仍显示旧文本 重启应用服务器
字符串未用 gettext 包裹 字符串未出现在 .po 文件中 _(){% trans %} 包裹
gettext 中使用 f-string 提取损坏或运行时不匹配 替换为 %.format() 占位符
占位符不匹配 compilemessages 失败或条目被丢弃 在源和翻译之间精确匹配占位符

当你自动化翻译步骤并在 CI 中强制执行检查时,这些问题大多会消失。TranslateBot 的 translate 命令处理占位符保留和增量翻译,而 check_translations 在到达生产环境之前捕获任何漏网之鱼(未翻译的条目、模糊标志和格式字符串问题)。

停止手动编辑 .po 文件

TranslateBot 使用 AI 自动化 Django 翻译。一条命令,覆盖所有语言,每次翻译仅需几分钱。