在 Web 开发中配置多语言支持,不少开发者都遇到过这个场景:代码里明明写了 _('Hello'),页面死活不翻译,也不知道是哪里断了。其实这不是函数写错了,而是整个翻译的链条在某个环节悄悄断了。今天就来聊聊,gettext 在 Python 框架里不生效的那些坑,以及怎么优雅地绕过去。
gettext 为什么在 Flask/Django 里总不生效
说穿了,问题不在于 gettext 本身,而在于翻译上下文没被正确激活,或者 .mo 文件的路径、语言环境没对上。翻译链路上任何一环脱节,结果都一样——页面不翻译。
gettext和_()本质上就是个占位符,真正触发翻译的,是运行时的 locale 设置和已加载的.mo文件- Django 默认用
django.utils.translation.gettext,Flask 通常要手动绑定gettext到当前请求的语言,比如通过flask_babel的get_locale() - 常见的诡异现象:
_("Hello")始终返回英文,控制台没报错,但LANG=zh_CN.UTF-8 python app.py也无效——说明环境变量没传进 Web 进程,或者没调用translation.install() - 检查
LC_MESSAGES是否在运行时可用:import locale; print(locale.getlocale()),如果输出(None, None),gettext直接退化为恒等函数,等于没翻译
提取字符串时 xgettext 找不到 _() 或 ngettext()
这个问题其实挺常见的,默认 xgettext 只识别 C 风格的 gettext("..."),而 Python 项目里普遍用短别名 _(),不显式声明就提取不到。
- 必须加参数:
xgettext --language=Python --keyword=_ --keyword=ngettext:1,2 -o messages.pot *.py - 如果用了
lazy_gettext(比如 Flask-WTF 里的表单),得额外加--keyword=lazy_gettext - Django 项目请直接用
django-admin makemessages -l zh_Hans,它内置了对_()和gettext_lazy的识别,比裸用xgettext省心得多 - 需要注意:Jinja2 模板里的
{% trans %}...不会被xgettext扫到,Django 要加-e html,Flask-Babel 要配babel.cfg并指定jinja2解析器
多语言切换后日期/数字格式还是英文
这是个容易混淆的点。gettext 只管字符串翻译,不管区域格式。比如 2024-06-12 不会自动变成 2024年6月12日。这是 locale 模块的职责,但和 gettext 的 locale 压根不是一套机制。
- Python 的
locale.setlocale(locale.LC_TIME, "zh_CN.UTF-8")必须在每次请求中显式调用(线程安全起见,不能全局设一次),而且系统得装了对应 locale(locale -a | grep zh_CN查一下) - 更稳的做法是绕过
locale模块:用babel.dates.format_date(..., locale="zh"),它不依赖系统 locale,纯 Python 实现,不容易出问题 - 数字千分位、小数点也同理:
babel.numbers.format_number(1234567.89, locale="de")输出1.234.567,89,而locale.format_string("%.2f", x, grouping=True)容易因系统缺失 locale 直接崩溃
部署时 .mo 文件找不到或加载失败
本地跑通不等于线上能用。部署时的问题集中在路径、权限、编码这三件事上。
gettext.translation(domain, localedir, languages=["zh"])中的localedir必须是绝对路径,相对路径在 WSGI/Gunicorn 下极易失效;推荐用os.path.join(os.path.dirname(__file__), "locales")- 确认
.mo文件放在localedir/zh/LC_MESSAGES/messages.mo(不是zh_CN或zh_Hans),语言码要和languages参数完全一致 .mo文件权限需为 644,且 Web 进程用户(如www-data)有读取localedir目录的权限;用strace -e trace=openat python app.py 2>&1 | grep locales可以查看实际打开路径- 如果用 Docker,确保构建时 COPY 了
locales/,且未被.dockerignore忽略——这是最容易被漏掉的一环
最麻烦的其实是语言码匹配逻辑。浏览器发 Accept-Language: zh-CN,zh;q=0.9,你的代码却只查 zh_Hans,中间差了标准化这一步。Babel 的 Locale.parse() 和 match_languages() 是绕不开的胶水。