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 常見問題