目次
Python i18n:gettextからモダンな翻訳ワークフローまで
Pythonは、あらゆるプログラミング言語の中でも最も豊富なi18nエコシステムを持っています。これは、大規模な多言語アプリケーション——特にグローバルに展開されているDjangoウェブフレームワーク、そして国際的なコミュニティに対応しなければならない科学計算やデータツールにおけるPythonの広範な利用——からの数十年にわたる投資によるものです。
このガイドでは、Pythonのi18nの全体像を解説します。標準ライブラリに組み込まれたクラシックなgettextアプローチから、Bavelの包括的なロケールデータライブラリ、DjangoとFlask向けのフレームワーク固有のソリューション、そしてFluentを使ったモダンなアプローチまでをカバーします。
Python i18nオプション早見表
| ライブラリ / アプローチ | 最適な用途 |
|---|---|
| gettext (stdlib) | シンプルなスクリプトやアプリケーション |
| Babel | 数値・日付・通貨のフォーマット;POファイル管理 |
| Django i18n | DjangoウェブアプリケーションDjango |
| Flask-Babel | FlaskウェブアプリケーションFlask |
| fluent.runtime | MozillaのFluentフォーマットを使うアプリケーション |
| Babel + gettext | ほとんどの本番Pythonウェブサービス |
gettext:PythonのビルトインI18n
Pythonの標準ライブラリにはgettextが含まれており、GNU gettext国際化APIを実装しています。これは多くの上位ライブラリが構築されている基盤です。
gettextの仕組み
gettextは翻訳の保存に.po(Portable Object)ファイルを使用し、実行時の読み込みのためにバイナリの.mo(Machine Object)ファイルにコンパイルします。
ワークフロー:
- ソースコード内の文字列を
_()またはgettext()でマークする - マークされた文字列を
xgettextまたはpybabelで.potテンプレートに抽出する - テンプレートからロケール固有の
.poファイルを作成する - 翻訳者が翻訳を入力する
.poファイルを.moファイルにコンパイルする- ユーザーのロケールに基づいて実行時に読み込む
gettextの基本的な使い方
import gettext
import locale
def setup_i18n(lang: str) -> gettext.GNUTranslations:
"""指定された言語の翻訳を読み込みます。"""
translation = gettext.translation(
domain='messages',
localedir='locales',
languages=[lang],
fallback=True # 見つからない場合はmsgid(ソース文字列)にフォールバック
)
return translation
# アプリケーションのセットアップ
trans = setup_i18n('fr')
_ = trans.gettext
ngettext = trans.ngettext # 複数形対応の翻訳
# 使用例
print(_("Hello, world!"))
print(_("Welcome, %(name)s!") % {"name": "Alice"})
# 複数形
count = 3
print(ngettext(
"%(count)d item", # 単数形
"%(count)d items", # 複数形
count # 形式を決定する数値
) % {"count": count})
抽出用の文字列マーキング
# _()で直接マーキング
title = _("Dashboard")
error = _("An error occurred: %(message)s") % {"message": str(e)}
# i18nがセットアップされる前に定義する必要がある文字列には、
# 遅延翻訳パターンを使います:
def _(s):
return s # 定義時は何もしない
# これらの文字列は抽出されますが、実行時まで翻訳されません
ERROR_MESSAGES = {
"not_found": _("Resource not found"),
"unauthorized": _("You are not authorized to perform this action"),
}
# 実行時に実際の_関数で翻訳します:
def get_error_message(key: str, translation_func) -> str:
return translation_func(ERROR_MESSAGES[key])
Babel:Pythonの包括的なi18nライブラリ
Babelはgettextを拡張し、数値・通貨・日付などのCLDRベースの完全なロケールデータを提供します。Pythonで最も包括的なi18nライブラリです。
pip install Babel
数値と通貨のフォーマット
from babel.numbers import format_number, format_currency, format_percent
from babel import Locale
# Localeオブジェクト
en_us = Locale('en', 'US')
de_de = Locale('de', 'DE')
ja_jp = Locale('ja', 'JP')
# 数値フォーマット
amount = 1234567.89
print(format_number(amount, locale='en_US')) # 1,234,567.89
print(format_number(amount, locale='de_DE')) # 1.234.567,89
print(format_number(amount, locale='en_IN')) # 12,34,567.89
# 通貨フォーマット
print(format_currency(1234.56, 'USD', locale='en_US')) # $1,234.56
print(format_currency(1234.56, 'EUR', locale='de_DE')) # 1.234,56 €
print(format_currency(1234.56, 'JPY', locale='ja_JP')) # ¥1,235(小数なし)
# パーセンテージ
print(format_percent(0.8527, locale='en_US')) # 85%
print(format_percent(0.8527, '#.##%', locale='de_DE')) # 85,27%
日付と時刻のフォーマット
from babel.dates import format_date, format_datetime, format_time, get_timezone
from datetime import datetime, date
dt = datetime(2024, 3, 15, 14, 30, 0)
d = date(2024, 3, 15)
# 日付フォーマット
print(format_date(d, locale='en_US')) # Mar 15, 2024
print(format_date(d, locale='de_DE')) # 15.03.2024
print(format_date(d, locale='ja_JP')) # 2024/03/15
print(format_date(d, format='full', locale='fr_FR')) # vendredi 15 mars 2024
# タイムゾーン付きの日時
tz = get_timezone('Europe/Berlin')
print(format_datetime(dt, locale='de_DE', tzinfo=tz))
# 相対時間(例:「3時間前」)
from babel.dates import format_timedelta
from datetime import timedelta
delta = timedelta(hours=-3)
print(format_timedelta(delta, locale='en_US', add_direction=True)) # 3 hours ago
print(format_timedelta(delta, locale='fr_FR', add_direction=True)) # il y a 3 heures
BabelによるPOファイルの管理
Babelは翻訳ファイルを管理するコマンドラインツールpybabelを提供しています:
# 1. PythonソースコードからI18n可能な文字列を抽出する pybabel extract -F babel.cfg -o messages.pot . # babel.cfgは抽出対象のファイルを設定します: # [python: **.py] # [jinja2: **/templates/**.html] # 2. 新しい言語を初期化する(locales/fr/LC_MESSAGES/messages.poを作成) pybabel init -i messages.pot -d locales -l fr # 3. ソース変更後、既存の.poファイルを更新する pybabel update -i messages.pot -d locales # 4. 実行時に使用するために.poファイルを.moファイルにコンパイルする pybabel compile -d locales
Babel/gettextの複数形
# .poファイル内(フランス語):
# msgid "%(count)d item"
# msgid_plural "%(count)d items"
# msgstr[0] "%(count)d élément"
# msgstr[1] "%(count)d éléments"
# Pythonでは:
from babel.support import Translations
translations = Translations.load('locales', ['fr'])
ngettext = translations.ngettext
for count in [0, 1, 2, 10]:
msg = ngettext("%(count)d item", "%(count)d items", count) % {"count": count}
print(msg)
Django i18n
Djangoは、テンプレート・モデル・ORMに深く統合された包括的なビルトインi18nサポートを持っています。
Django i18nのセットアップ
# settings.py
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en', 'English'),
('fr', 'Français'),
('de', 'Deutsch'),
('ja', '日本語'),
('ar', 'العربية'),
]
USE_I18N = True
USE_L10N = True # ロケール対応の数値/日付フォーマット
USE_TZ = True
LOCALE_PATHS = [BASE_DIR / 'locale']
MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware', # リクエストから言語を設定
# ... 他のミドルウェア
]
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
'django.template.context_processors.i18n',
# ...
],
},
}]
PythonコードでのDjango翻訳
from django.utils.translation import gettext as _, ngettext, gettext_lazy as _lazy
# 即時翻訳(すぐに評価される)
message = _("Welcome!")
# 遅延翻訳(文字列アクセス時に評価される - モデルフィールドやクラス属性に使用)
class Article(models.Model):
title = models.CharField(max_length=200)
class Meta:
verbose_name = _lazy("article")
verbose_name_plural = _lazy("articles")
# 複数形
def item_message(count: int) -> str:
return ngettext(
"You have %(count)d item",
"You have %(count)d items",
count
) % {"count": count}
# 文字列フォーマット - 翻訳可能にするために名前付きパラメータを使用
def welcome(name: str) -> str:
return _("Welcome, %(name)s!") % {"name": name}
# コンテキスト依存の翻訳(同じ文字列、異なる意味)
from django.utils.translation import pgettext
month = pgettext("month name", "May") # 動詞としての"May"と区別
Djangoテンプレートでの翻訳
{% load i18n %}
{# シンプルな翻訳 #}
<h1>{% trans "Dashboard" %}</h1>
{# 変数付きの翻訳 #}
{% blocktrans with name=user.first_name %}
Welcome, {{ name }}!
{% endblocktrans %}
{# テンプレートの複数形 #}
{% blocktrans count count=items|length %}
You have {{ count }} item.
{% plural %}
You have {{ count }} items.
{% endblocktrans %}
{# 言語スイッチャー #}
{% get_available_languages as LANGUAGES %}
<ul>
{% for lang_code, lang_name in LANGUAGES %}
<li>
<a href="/{{ lang_code }}/">{{ lang_name }}</a>
</li>
{% endfor %}
</ul>
Django URL i18nパターン
# urls.py
from django.conf.urls.i18n import i18n_patterns
from django.urls import path, include
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), # 言語スイッチャーエンドポイント
]
urlpatterns += i18n_patterns(
# これらのURLは言語プレフィックスを取得:/en/about/、/fr/about/
path('about/', views.about, name='about'),
path('products/', include('products.urls')),
prefix_default_language=False, # /about/は/en/about/にリダイレクト
)
Flask-BabelによるFlask i18n
Flask-BabelはBabelのパワーをFlaskアプリケーションにもたらします:
pip install Flask-Babel
# app.py
from flask import Flask, g, request
from flask_babel import Babel, _, ngettext, format_currency, format_datetime
app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
def get_locale():
# 1. URLパラメータを確認
lang = request.args.get('lang')
if lang:
return lang
# 2. セッション/データベースのユーザー設定を確認
if hasattr(g, 'current_user') and g.current_user.language:
return g.current_user.language
# 3. Accept-Languageヘッダーを使用
return request.accept_languages.best_match(['en', 'fr', 'de', 'ja'])
babel = Babel(app, locale_selector=get_locale)
@app.route('/products/<int:product_id>')
def product_detail(product_id):
product = Product.query.get_or_404(product_id)
# これらは現在のロケールを自動的に使用します
price = format_currency(product.price, 'USD')
created = format_datetime(product.created_at, format='medium')
return render_template('product.html',
product=product,
price=price,
created=created,
title=_("Product: %(name)s") % {"name": product.name}
)
モダンなアプローチ:PythonのFluent
MozillaのFluentはfluent.runtime経由でPythonから利用できます:
pip install fluent.runtime
from fluent.runtime import FluentBundle, FluentResource
# Fluentファイルの読み込みと使用
def create_bundle(locale: str, ftl_content: str) -> FluentBundle:
bundle = FluentBundle([locale])
resource = FluentResource(ftl_content)
errors = bundle.add_resource(resource)
if errors:
raise ValueError(f"FTL errors: {errors}")
return bundle
# FTLファイルの内容
en_ftl = """
welcome = Welcome to our app!
greeting = Hello, { $name }!
items =
{ $count ->
[0] No items
[one] { $count } item
*[other] { $count } items
}
"""
bundle = create_bundle("en-US", en_ftl)
def translate(bundle: FluentBundle, message_id: str, **kwargs) -> str:
msg = bundle.get_message(message_id)
if not msg or not msg.value:
return message_id
value, errors = bundle.format_pattern(msg.value, kwargs)
return value
print(translate(bundle, "welcome"))
print(translate(bundle, "greeting", name="Alice"))
print(translate(bundle, "items", count=0))
print(translate(bundle, "items", count=1))
print(translate(bundle, "items", count=5))
CI/CDとの統合
Pythonのi18nワークフローは、継続的なローカライゼーションパイプラインと自然に統合できます:
# .github/workflows/i18n.yml
name: i18n
on:
push:
paths:
- '**.py'
- '**/templates/**.html'
jobs:
extract-and-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Babel
run: pip install Babel
- name: Extract strings
run: pybabel extract -F babel.cfg -o messages.pot .
- name: Update translation files
run: pybabel update -i messages.pot -d locales
- name: Push to translation platform
run: |
# messages.potをTMSにアップロード
curl -X POST https://api.better-i18n.com/upload \
-F "file=@messages.pot" \
-H "Authorization: Bearer ${{ secrets.BETTER_I18N_API_KEY }}"
包括的なCI/CD i18nパターンについては、i18n CI/CDパイプライン自動化をご覧ください。
ロケール検出のベストプラクティス
from babel import Locale, UnknownLocaleError
from typing import Optional
SUPPORTED_LOCALES = ['en', 'fr', 'de', 'ja', 'ar', 'pt-BR']
def parse_accept_language(header: str) -> list[str]:
"""Accept-Languageヘッダーを言語コードの順序リストに解析します。"""
locales = []
for part in header.split(','):
parts = part.strip().split(';')
lang = parts[0].strip()
# 品質(q=0.9)を抽出するか、デフォルトの1.0を使用
q = 1.0
for param in parts[1:]:
if param.strip().startswith('q='):
try:
q = float(param.strip()[2:])
except ValueError:
pass
locales.append((lang, q))
# 品質でソート、最高値を先頭に
locales.sort(key=lambda x: x[1], reverse=True)
return [lang for lang, _ in locales]
def negotiate_locale(accept_language: str, supported: list[str] = SUPPORTED_LOCALES) -> str:
"""与えられたAccept-Languageヘッダーに対して最適なサポートロケールを見つけます。"""
requested = parse_accept_language(accept_language)
for requested_lang in requested:
# 完全一致を最初に確認
if requested_lang in supported:
return requested_lang
# 言語のみの一致(en-US → en)
base = requested_lang.split('-')[0]
if base in supported:
return base
# 同じベースを持つサポートロケールを検索
for supported_locale in supported:
if supported_locale.startswith(base + '-'):
return supported_locale
return 'en' # デフォルトのフォールバック
言語間の複数形ルールの詳細については、言語間の複数形ルールをご覧ください。翻訳管理エコシステムの概要については、翻訳管理システムをご覧ください。
better-i18nでアプリをグローバルに展開しましょう
better-i18nは、AIを活用した翻訳、gitネイティブなワークフロー、グローバルCDN配信を1つの開発者ファーストなプラットフォームに統合しています。スプレッドシートの管理をやめて、すべての言語でリリースを始めましょう。