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 の設定

翻訳対象文字列を走査する場所を pybabel に伝える babel.cfg ファイルを作成し、リクエストごとに提供する言語を決定するロケールセレクター関数を使用して 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 init ではなく pybabel update を実行してください。既存の言語ディレクトリに対して 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 が読み込むのは、.po ファイルではなくコンパイル済みの .mo ファイルです。.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 に関するよくある質問