当前位置: 代码网 > it编程>前端脚本>Python > Python Web项目国际化(i18n)的完全指南

Python Web项目国际化(i18n)的完全指南

2026年08月05日 Python 我要评论
从 django 到 flask 再到 fastapi,一篇讲透 python web 国际化的正确姿势。前言上周接到一个需求:公司内部的工单系统要支持英文和日文。我打开代码一看,好家伙—

从 django 到 flask 再到 fastapi,一篇讲透 python web 国际化的正确姿势。

前言

上周接到一个需求:公司内部的工单系统要支持英文和日文。

我打开代码一看,好家伙——

return {"message": "操作成功", "data": result}

全项目 300 多处 硬编码中文字符串。

那一刻我悟了:国际化不是"加个翻译",而是一种架构思维。

今天这篇文章,我会从底层原理到三大主流框架的实战,把 python web 项目的国际化讲透。建议收藏 🌟,早晚用得上。

一、先搞懂:i18n 到底是什么?

i18n = internationalization(首尾字母 + 中间 18 个字母)

核心思想就一句话:代码里不写死任何自然语言文本,所有用户可见的字符串都通过"翻译系统"动态获取。

没有 i18n 的痛

# ❌ 硬编码
def create_order(user):
    if not user.is_verified:
        raise bizerror("请先完成实名认证")
    return {"msg": "下单成功"}

想加英文?改代码。想加日文?再改。想改一个措辞?全局搜索替换,祈祷别改错。

有 i18n 的爽

# ✅ 国际化
def create_order(user):
    if not user.is_verified:
        raise bizerror(_("error.need_verification"))
    return {"msg": _("order.create_success")}

翻译文件单独维护,代码永远不用动。这才是正确的打开方式。

二、python 国际化的基石:gettext

不管你用什么框架,底层几乎都是 gettext。它是 python 标准库自带的模块,也是 gnu 翻译工具链的核心。

2.1 核心概念

概念说明
.pot 文件模板文件,从代码中提取的所有待翻译字符串
.po 文件翻译文件,翻译人员编辑的就是它
.mo 文件编译后的二进制文件,程序运行时读取的是它
msgid原始字符串(通常是英文)
msgstr翻译后的字符串

2.2 一个最小示例

目录结构:

project/
├── app.py
└── locale/
    ├── en/
    │   └── lc_messages/
    │       ├── messages.po
    │       └── messages.mo
    └── zh_cn/
        └── lc_messages/
            ├── messages.po
            └── messages.mo

locale/zh_cn/lc_messages/messages.po

msgid "hello, {name}!"
msgstr "你好,{name}!"
msgid "you have {count} new messages."
msgstr "你有 {count} 条新消息。"

编译 .po.mo

msgfmt locale/zh_cn/lc_messages/messages.po \
       -o locale/zh_cn/lc_messages/messages.mo

代码中使用:

import gettext

# 加载翻译
zh = gettext.translation('messages', localedir='locale', languages=['zh_cn'])
zh.install()
_ = zh.gettext

print(_("hello, {name}!").format(name="张三"))
# 输出: 你好,张三!

划重点gettext 是标准库,零依赖。但实际 web 项目中,我们通常用框架封装好的方案。

三、django:开箱即用的 i18n 全家桶

django 的国际化是最完善的,没有之一。从中间件、模板标签到 orm 错误信息,全链路支持。

3.1 配置(3 步搞定)

settings.py

# 1. 开启国际化
use_i18n = true
use_l10n = true

# 2. 支持的语言
languages = [
    ('zh-hans', '简体中文'),
    ('en', 'english'),
    ('ja', '日本語'),
]

# 3. 翻译文件目录
locale_paths = [base_dir / 'locale']

# 4. 添加中间件(放在 commonmiddleware 之后)
middleware = [
    ...
    'django.middleware.locale.localemiddleware',
    ...
]

3.2 代码中标记翻译

from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _lazy

# 视图函数中
def order_view(request):
    return jsonresponse({
        "message": _("order created successfully"),
        "total": 99.9
    })

# 模型中(必须用 lazy 版本!)
class product(models.model):
    name = models.charfield(
        max_length=100,
        help_text=_lazy("the display name of the product")
    )

坑点提醒:在模型定义、表单类等模块级别的代码中,必须用 gettext_lazy 而不是 gettext,否则翻译会在服务启动时就固化,切换语言无效。

3.3 模板中使用

{% load i18n %}
<h1>{% trans "welcome to our store" %}</h1>
<p>{% blocktrans count counter=items|length %}
    you have {{ counter }} item.
{% plural %}
    you have {{ counter }} items.
{% endblocktrans %}

3.4 提取 & 编译翻译

# 提取所有待翻译字符串 → 生成 .po 文件
python manage.py makemessages -l zh_hans
python manage.py makemessages -l ja

# 翻译完成后,编译
python manage.py compilemessages

3.5 前端切换语言

# urls.py
from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path('orders/', order_view),
    # url 会变成: /zh-hans/orders/ 或 /en/orders/
)

也可以通过 accept-language 请求头或 cookie 自动识别。

四、flask:轻量但够用

flask 本身不带 i18n,但 flask-babel 插件做得非常成熟。

4.1 安装 & 初始化

pip install flask-babel
from flask import flask, g, request
from flask_babel import babel, gettext as _, lazy_gettext as _lazy

app = flask(__name__)
app.config['babel_default_locale'] = 'zh_hans_cn'
app.config['babel_translation_directories'] = 'translations'

babel = babel(app)

@babel.localeselector
def get_locale():
    """决定当前请求使用哪种语言"""
    # 优先级:url参数 > 用户设置 > 浏览器偏好
    lang = request.args.get('lang')
    if lang in ['zh_hans_cn', 'en', 'ja']:
        return lang
    return request.accept_languages.best_match(['zh_hans_cn', 'en', 'ja'])

4.2 使用翻译

@app.route('/api/order', methods=['post'])
def create_order():
    user = g.current_user
    if not user.is_verified:
        return jsonify({
            "code": 403,
            "message": _("please complete identity verification first.")
        }), 403

    return jsonify({
        "code": 200,
        "message": _("order created successfully.")
    })

4.3 翻译工作流

创建 babel.cfg(告诉 babel 去哪里找字符串):

[python: app/**.py]
[jinja2: templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

提取 → 初始化 → 翻译 → 编译:

# 1. 提取
pybabel extract -f babel.cfg -o messages.pot .

# 2. 初始化语言(首次)
pybabel init -i messages.pot -d translations -l zh_hans_cn
pybabel init -i messages.pot -d translations -l en
pybabel init -i messages.pot -d translations -l ja

# 3. 翻译人员编辑 translations/zh_hans_cn/lc_messages/messages.po

# 4. 编译
pybabel compile -d translations

# 5. 后续更新(新增字符串后)
pybabel update -i messages.pot -d translations

4.4 jinja2 模板

<h1>{{ _('welcome back') }}, {{ user.name }}</h1>
<p>{{ ngettext(
    'you have %(count)d notification.',
    'you have %(count)d notifications.',
    count=notifications|length
) }}</p>

五、fastapi:异步时代的 i18n 方案

fastapi 没有官方 i18n 插件,但这不代表不能优雅地做。社区主流方案是 gettext + 自定义中间件

5.1 项目结构

fastapi_app/
├── main.py
├── i18n.py              # 国际化核心模块
├── locale/
│   ├── en/lc_messages/messages.mo
│   ├── zh_cn/lc_messages/messages.mo
│   └── ja/lc_messages/messages.mo
└── routers/
    └── order.py

5.2 核心:i18n 模块

# i18n.py
import gettext
from contextvars import contextvar
from pathlib import path

# 用 contextvar 保证异步安全(每个请求独立的语言上下文)
_current_locale: contextvar[str] = contextvar('locale', default='zh_cn')

locale_dir = path(__file__).parent / 'locale'
supported_locales = {'zh_cn', 'en', 'ja'}

# 预加载所有翻译对象,避免每次请求都读磁盘
_translations: dict[str, gettext.gnutranslations] = {}

def _load_translations():
    for lang in supported_locales:
        try:
            _translations[lang] = gettext.translation(
                'messages', localedir=str(locale_dir), languages=[lang]
            )
        except filenotfounderror:
            _translations[lang] = gettext.nulltranslations()

_load_translations()

def set_locale(locale: str):
    if locale in supported_locales:
        _current_locale.set(locale)

def get_locale() -> str:
    return _current_locale.get()

def _(msgid: str) -> str:
    """翻译函数,根据当前请求的语言返回对应文本"""
    locale = _current_locale.get()
    return _translations[locale].gettext(msgid)

def ngettext(singular: str, plural: str, n: int) -> str:
    locale = _current_locale.get()
    return _translations[locale].ngettext(singular, plural, n)

5.3 中间件:自动识别语言

# main.py
from fastapi import fastapi, request
from starlette.middleware.base import basehttpmiddleware
from i18n import set_locale, supported_locales

app = fastapi()

class localemiddleware(basehttpmiddleware):
    async def dispatch(self, request: request, call_next):
        # 优先级:查询参数 > 请求头 > 默认值
        locale = (
            request.query_params.get('lang')
            or request.headers.get('accept-language', 'zh_cn')[:5]
        )
        # 标准化:zh-cn → zh_cn
        locale = locale.replace('-', '_')
        if locale not in supported_locales:
            locale = 'zh_cn'

        set_locale(locale)
        response = await call_next(request)
        return response

app.add_middleware(localemiddleware)

5.4 在路由中使用

# routers/order.py
from fastapi import apirouter, depends
from i18n import _, ngettext

router = apirouter()

@router.post("/api/orders")
async def create_order():
    # 业务逻辑...
    return {
        "code": 200,
        "message": _("order created successfully."),
    }

@router.get("/api/notifications")
async def get_notifications(count: int = 3):
    msg = ngettext(
        "you have {n} new notification.",
        "you have {n} new notifications.",
        count
    ).format(n=count)
    return {"message": msg}

5.5 翻译文件管理

和前面一样,用 pybabel 工具链:

# 提取
pybabel extract -f babel.cfg -o messages.pot .

# 初始化
pybabel init -i messages.pot -d locale -l zh_cn
pybabel init -i messages.pot -d locale -l en

# 编译(改完 .po 后必须执行!)
pybabel compile -d locale

fastapi 特别注意:因为用了 contextvar,在 async 环境下每个请求的语言设置是隔离的,不会串。但如果你用了 run_in_threadpool 跑同步代码,contextvar 也能正确传播,放心用。

六、进阶:数字、日期、货币的本地化

翻译了文字还不够。1,234,567.89 在德国是 1.234.567,89,日期 07/31/2026 在英国是 31/07/2026

这时候需要 babel 库:

pip install babel
from babel.numbers import format_currency, format_decimal
from babel.dates import format_datetime
from datetime import datetime

# 货币
format_currency(99.9, 'cny', locale='zh_cn')   # '¥99.90'
format_currency(99.9, 'usd', locale='en_us')    # '$99.90'
format_currency(99.9, 'jpy', locale='ja_jp')    # '¥99'

# 数字
format_decimal(1234567.89, locale='de_de')      # '1.234.567,89'

# 日期
now = datetime(2026, 7, 31, 14, 30)
format_datetime(now, format='long', locale='zh_cn')  # '2026年7月31日 14:30:00'
format_datetime(now, format='long', locale='en_us')  # 'jul 31, 2026, 2:30:00 pm'

在 fastapi 中可以封装一个工具函数:

from babel.numbers import format_currency
from i18n import get_locale

def localize_price(amount: float, currency: str = 'cny') -> str:
    return format_currency(amount, currency, locale=get_locale())

七、踩坑清单 & 最佳实践 

写了这么多,把我踩过的坑总结成清单,建议截图保存

常见错误

说明
gettext 而不是 gettext_lazy在类定义、模块级别使用会导致翻译固化
忘记编译 .mo 文件改了 .po 不编译,线上完全不生效
msgid 用中文一旦要改措辞,所有翻译文件都要跟着改
字符串拼接_("hello") + name ❌ → _("hello, {name}").format(name=name)
忽略复数规则英文 1 条 vs 2 条,俄语有 3 种复数形式!用 ngettext
翻译文件没加 .gitignore.mo 是编译产物,不应入库(或入库,看团队规范)

最佳实践

1. msgid 统一用英文,作为"唯一标识符"

2. 所有用户可见文本都过 _(),包括错误码对应的消息

3. 翻译文件交给专业翻译 / 翻译平台(如 crowdin、lokalise)

4. ci/cd 中加一步 pybabel compile,防止忘编译

5. 写单元测试验证每种语言的翻译完整性

6. 数字、日期、货币用 babel,别自己 format

翻译完整性检查脚本

# scripts/check_translations.py
import polib
from pathlib import path

def check(locale_dir: str):
    pot = polib.pofile(f'{locale_dir}/messages.pot')
    source_ids = {e.msgid for e in pot}

    for po_file in path(locale_dir).rglob('*.po'):
        po = polib.pofile(str(po_file))
        translated = {e.msgid for e in po.translated_entries()}
        missing = source_ids - translated
        if missing:
            print(f"⚠️  {po_file.parent.parent.name} 缺少 {len(missing)} 条翻译:")
            for m in list(missing)[:5]:
                print(f"   - {m}")
        else:
            print(f"✅ {po_file.parent.parent.name} 翻译完整")

check('locale')

八、三大框架对比总结

维度djangoflaskfastapi
i18n 支持⭐⭐⭐⭐⭐ 内置全家桶⭐⭐⭐⭐ flask-babel 插件⭐⭐⭐ 需自行封装
上手难度低(配置即用)中(需装插件+配置)中高(需理解 contextvar)
模板翻译{% trans %} 标签{{ _() }}通常前后端分离,不涉及
复数支持blocktrans + pluralngettext()ngettext()
异步安全n/a(同步框架)n/a(同步为主)✅ contextvar 天然支持
适用场景全栈项目、cms中小型 api / 传统 web高性能 api、微服务

九、写在最后

国际化这件事,越早做成本越低

项目初期加一个 _() 包裹,成本几乎为零。但等到上线后再回头改,那就是几百个文件的"考古工程"。

记住这个原则:任何用户能看到的文字,都不应该出现在代码里。

以上就是python web项目国际化(i18n)的完全指南的详细内容,更多关于python国际化i18n的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com