Skip to main content

Flask i18n:使用 Flask-Babel 构建多语言应用

从 gettext 基础到生产部署:使用 Flask-Babel、PO 文件和自动化 AI 翻译实现 Flask 应用国际化。

1

安装 Flask-Babel

Flask-Babel 是 Flask 的标准国际化扩展。它将 GNU gettext 与 Flask 和 Jinja2 模板集成,开箱即用地提供翻译函数、区域设置选择和时区支持。

Flask-Babel 封装了 Babel(Python i18n 库),并将其与 Flask 的请求生命周期集成。它提供 gettext()、ngettext() 和 lazy_gettext() 函数,还可根据浏览器标头自动检测区域设置。
Terminal
pip install Flask-Babel
2

配置 Babel

创建 babel.cfg 文件,告诉 pybabel 在何处扫描可翻译字符串,然后使用区域设置选择器函数初始化 Flask-Babel,以确定每个请求应提供哪种语言。

babel.cfg
# babel.cfg — tells pybabel where to find translatable strings
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
app.py
from flask import Flask, request
from flask_babel import Babel

app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
# Directory where translations live (default: "translations")
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'

def get_locale():
    # 1. Check URL parameter or session
    # 2. Fall back to browser Accept-Language header
    return request.accept_languages.best_match(['en', 'de', 'ja', 'es', 'fr'])

babel = Babel(app, locale_selector=get_locale)
每次请求都会调用 locale_selector 函数。如果它返回的区域设置没有已编译的 .mo 文件,Flask-Babel 会静默回退到默认区域设置,不会引发错误,字符串只会显示为未翻译状态。
3

标记待翻译字符串

在 Python 代码中使用 gettext(),在 Jinja2 模板中使用 _() 包装每个面向用户的字符串。对于模块加载时定义、但需要在之后的请求中翻译的字符串(例如表单标签和配置),请使用 lazy_gettext()。

app.py
from flask_babel import gettext, ngettext, lazy_gettext

# In views — gettext() for immediate translation
@app.route('/')
def index():
    flash(gettext('Your profile has been updated.'))
    return render_template('index.html',
        title=gettext('Home'))

# In forms/config — lazy_gettext() for deferred translation
class LoginForm(FlaskForm):
    username = StringField(lazy_gettext('Username'))
    password = PasswordField(lazy_gettext('Password'))
    submit = SubmitField(lazy_gettext('Sign In'))
templates/index.html
{# In Jinja2 templates, use _() shorthand for gettext #}
<h1>{{ _('Welcome to our app') }}</h1>

<p>{{ _('Hello, %(name)s!', name=user.name) }}</p>

<footer>
  {{ _('Copyright %(year)s Example Corp.', year=2026) }}
</footer>
在 Jinja2 模板中使用 _() 而非 gettext()——这是 gettext 的标准简写,可让模板保持简洁。Flask-Babel 会自动将 _() 注册为 Jinja2 全局函数。
4

提取消息

运行 pybabel extract,扫描源代码和模板中的可翻译字符串。这会创建一个 .pot(Portable Object Template)文件。然后为每种目标语言初始化目录,或在源字符串更改时更新现有目录。

Terminal
# Extract translatable strings from source code
pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .

# Initialize a new language (first time only)
pybabel init -i messages.pot -d translations -l de
pybabel init -i messages.pot -d translations -l ja
pybabel init -i messages.pot -d translations -l es

# Update existing catalogs when source strings change
pybabel update -i messages.pot -d translations
首次提取后,始终运行 pybabel update(而不是 init)。对现有语言目录运行 init 会覆盖所有现有翻译。update 命令会合并新字符串,同时保留现有翻译。
5

翻译 PO 文件

打开生成的 .po 文件,为每个 msgid 填写 msgstr 值。PO 文件是纯文本,您可直接编辑、使用 Poedit 等 PO 编辑器,或借助 AI 工具自动翻译。

translations/de/LC_MESSAGES/messages.po
# translations/de/LC_MESSAGES/messages.po
msgid "Welcome to our app"
msgstr "Willkommen in unserer App"

msgid "Hello, %(name)s!"
msgstr "Hallo, %(name)s!"

msgid "Your profile has been updated."
msgstr "Ihr Profil wurde aktualisiert."

msgid "Username"
msgstr "Benutzername"

msgid "Password"
msgstr "Passwort"

msgid "Sign In"
msgstr "Anmelden"
PO 文件包含上下文注释(以 #: 开头的行),显示每个字符串在源代码中的使用位置。译者据此了解上下文。请保留这些注释,它们由 pybabel extract 自动生成。
6

编译翻译

使用 pybabel compile 将 .po 文件编译为二进制 .mo 文件。Flask-Babel 在运行时读取 .mo 文件,无法直接读取 .po 文件。每次更新翻译后都必须重新编译。

Terminal
# Compile .po files to binary .mo files (required at runtime)
pybabel compile -d translations

# Flask-Babel reads .mo files, not .po files.
# You MUST compile after every translation update.
如果编辑 .po 文件后翻译仍未显示,几乎可以确定是忘记运行 pybabel compile。这是最常见的 Flask-Babel 问题。将编译步骤添加到部署脚本中,以避免生产环境出现此问题。
7

处理复数和变量

对涉及复数的字符串使用 ngettext()。它接收单数形式、复数形式和数量。Babel 会自动为每种语言采用正确的复数规则——英语有 2 种形式,俄语有 3 种,阿拉伯语有 6 种,日语有 1 种。

app.py
from flask_babel import ngettext

@app.route('/cart')
def cart():
    count = len(session.get('cart_items', []))
    message = ngettext(
        '%(num)d item in your cart',     # singular
        '%(num)d items in your cart',    # plural
        count                             # determines which form
    )
    return render_template('cart.html', message=message)
Plural forms in .po files
# English: 2 forms (nplurals=2)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d item in your cart"
msgstr[1] "%(num)d items in your cart"

# German: 2 forms (nplurals=2)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d Artikel in Ihrem Warenkorb"
msgstr[1] "%(num)d Artikel in Ihrem Warenkorb"

# Japanese: 1 form (nplurals=1)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "カートに%(num)d個の商品があります"

# Russian: 3 forms (nplurals=3)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d товар в вашей корзине"
msgstr[1] "%(num)d товара в вашей корзине"
msgstr[2] "%(num)d товаров в вашей корзине"
切勿使用 if count == 1 处理复数逻辑。法语等语言将 0 视为单数。俄语和阿拉伯语还存在英语没有的形式。请让 ngettext() 和 Babel 的 CLDR 复数规则处理选择。
8

添加区域设置切换功能

构建一个语言选择器,将用户的选择存储在 Flask 会话中。更新 locale_selector 函数,使其先检查会话,再回退到浏览器检测。

app.py
from flask import session, redirect, url_for, request
from flask_babel import refresh

@app.route('/set-language/<lang>')
def set_language(lang):
    session['lang'] = lang
    refresh()  # Force Flask-Babel to re-read the locale
    return redirect(request.referrer or url_for('index'))

# Update get_locale to check session first
def get_locale():
    # 1. Explicit user choice (stored in session)
    if 'lang' in session:
        return session['lang']
    # 2. Browser Accept-Language header
    return request.accept_languages.best_match(
        ['en', 'de', 'ja', 'es', 'fr']
    )
templates/components/language_switcher.html
{# Language switcher component #}
<nav class="language-switcher">
  {% for lang, name in [('en','English'),('de','Deutsch'),
                         ('ja','日本語'),('es','Español'),
                         ('fr','Français')] %}
    <a href="{{ url_for('set_language', lang=lang) }}"
       class="{{ 'active' if get_locale() == lang }}">
      {{ name }}
    </a>
  {% endfor %}
</nav>
更改会话区域设置后,调用 flask_babel.refresh(),强制 Flask-Babel 为当前请求重新读取区域设置。若不调用 refresh(),旧区域设置会一直保留到下一个请求。
9

自动翻译

完成 Flask-Babel 设置后,使用 AI 翻译 PO 文件。在 CI/CD 管线中自动执行提取、翻译和编译循环,使翻译与源代码保持同步。

Terminal
# Translate your PO files with AI directly from your IDE
# or use the CLI in CI/CD:
npx i18n-agent translate translations/de/LC_MESSAGES/messages.po \
  --source-lang en --target-lang de

# Bulk translate all languages at once:
npx i18n-agent translate messages.pot --lang de,ja,es,fr

# Then compile:
pybabel compile -d translations
采用渐进式翻译——添加新字符串并运行 pybabel update 后,只翻译尚未翻译的新条目(msgstr 为空的值),不要重新生成全部内容。这样可保留经人工审核的翻译。

额外功能:使用 flask-babel-locale-chain 实现智能区域设置回退

默认情况下,当用户偏好的区域设置不可用时,Flask-Babel 会直接回退到默认区域设置。如果只有 pt-PT 翻译,pt-BR 用户看到的会是英语而非葡萄牙语。flask-babel-locale-chain 可添加可配置的回退链,让相关区域设置自然逐级回退。

app.py
# pip install flask-babel-locale-chain
from flask_babel_locale_chain import LocaleChain

# Define fallback chains: pt-BR falls back to pt before en
locale_chain = LocaleChain({
    'pt-BR': ['pt-BR', 'pt', 'en'],
    'pt-PT': ['pt-PT', 'pt', 'en'],
    'zh-Hant': ['zh-Hant', 'zh-Hans', 'en'],
    'en-GB': ['en-GB', 'en', 'en-US'],
})

def get_locale():
    requested = request.accept_languages.best_match(
        ['en', 'pt-BR', 'pt', 'zh-Hant', 'zh-Hans']
    )
    # Returns the best available locale from the chain
    return locale_chain.resolve(requested)
flask-babel-locale-chain 是一个开源 Python 软件包。可在 GitHub 的 github.com/i18n-agent/flask-babel-locale-chain 查看。

自动保证翻译质量

使用 i18n-validate 在发布前发现缺失键和损坏的占位符。真实译文完成前,可使用 i18n-pseudo 生成伪译文来测试 UI。

常见问题

忘记将 .po 编译为 .mo

Flask-Babel 读取已编译的 .mo 文件,而非 .po 文件。如果编辑 .po 文件后翻译未显示,请运行 pybabel compile -d translations。将此步骤添加到部署脚本中。

在模块级使用 gettext()

gettext() 需要请求上下文。如果在模块级定义翻译字符串(类属性、常量),请改用 lazy_gettext()。它会将翻译推迟到字符串实际在请求中呈现时。

提取时遗漏字符串

pybabel extract 仅扫描与 babel.cfg 中模式匹配的文件。如果未提取字符串,请检查 babel.cfg 模式是否与文件结构匹配,添加 -k lazy_gettext 以提取惰性调用,并确保 Jinja2 模板使用正确的扩展名(.html、.jinja2)。

PO 文件编码错误

PO 文件必须采用 UTF-8 编码。如果看到 UnicodeDecodeError,请检查 .po 文件中的 Content-Type 标头,它应包含 charset=UTF-8。某些编辑器会使用其他编码保存,请务必在编辑后验证。

推荐的文件结构

Project Structure
my-flask-app/
├── app.py                          # Flask app with Babel config
├── babel.cfg                       # Extraction config
├── messages.pot                    # Template (extracted strings)
├── translations/
│   ├── de/
│   │   └── LC_MESSAGES/
│   │       ├── messages.po         # German translations (editable)
│   │       └── messages.mo         # Compiled binary (generated)
│   ├── ja/
│   │   └── LC_MESSAGES/
│   │       ├── messages.po
│   │       └── messages.mo
│   └── es/
│       └── LC_MESSAGES/
│           ├── messages.po
│           └── messages.mo
├── templates/
│   ├── base.html
│   ├── index.html
│   └── components/
│       └── language_switcher.html
├── requirements.txt
└── venv/

立即试用 i18n Agent

将翻译文件拖放到此处

JSON, YAML, PO, XML, CSV, Markdown, Properties

或点击选择文件

目标语言

无需注册即时估价

Flask i18n 常见问题