你标记了需要翻译的字符串,生成了 .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 设置中,放在 SessionMiddleware 和 CommonMiddleware 之后:
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-br 与 pt_BR)
翻译存在于你的 .po 文件中,compilemessages 成功运行,但 Django 对某些区域设置忽略了翻译。
Django 期望区域目录遵循 <language>_<COUNTRY> 格式,使用下划线分隔符和大写国家代码。例如,巴西葡萄牙语使用 pt_BR。如果你的目录名为 pt-br、pt-BR 或 ptBR,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 在到达生产环境之前捕获任何漏网之鱼(未翻译的条目、模糊标志和格式字符串问题)。