翻訳対象の文字列をマークし、.po ファイルを生成し、compilemessages を実行したのに、アプリはまだ英語を表示しています。あなただけではありません。Django の i18n フレームワークは強力ですが、経験豊富な開発者でも引っかかる落とし穴があります。
このガイドでは、Django の翻訳が静かに失敗する最も一般的な 10 の原因を、それぞれの正確な症状と修正方法とともに解説します。
1. .po ファイルを編集した後に compilemessages を実行し忘れる
.po ファイルを編集した(手動またはツールで)のに、翻訳されたテキストが表示されません。アプリは元の英語文字列を表示し続けます。
Django は実行時に .po ファイルを読みません。代わりにコンパイルされた .mo(マシンオブジェクト)バイナリファイルを読みます。.po ファイルを再コンパイルせずに編集した場合、Django は何も変わったことを知りません。
.po ファイルを変更するたびに compilemessages を実行してください:
python manage.py compilemessages
TranslateBot で翻訳を自動化している場合は、ワークフローの最後のステップとして compilemessages を追加してください:
python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages
2. テンプレートに {% load i18n %} がない
テンプレートで {% trans "Hello" %} を使用していますが、Django が TemplateSyntaxError を発生させます。あるいは、テンプレートエンジンの設定が誤っている場合、タグが静かに何もしないことがあります。
{% trans %} と {% blocktrans %} タグは Django の i18n テンプレートタグライブラリにあります。ロードしないと、テンプレートエンジンはそれらを認識しません。
翻訳タグを使用するすべてのテンプレートの先頭に {% load i18n %} を追加してください:
{% load i18n %}
<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>
これはテンプレートごとの要件です。親テンプレートが i18n をロードしていても、翻訳タグを使用する子テンプレートには独自の {% load i18n %} 宣言が必要です。
3. LocaleMiddleware が MIDDLEWARE にないか、間違った位置にある
ブラウザの Accept-Language ヘッダー、URL プレフィックス、セッション設定に関係なく、Django は常にデフォルト言語でコンテンツを提供します。
LocaleMiddleware は各リクエストのアクティブ言語を決定します。これがないと、Django は LANGUAGE_CODE をデフォルトとして使用し、すべての言語選択メカニズムを無視します。ミドルウェアスタック内の位置も重要です。セッションデータと URL 解決へのアクセスが必要だからです。
MIDDLEWARE 設定に LocaleMiddleware を追加してください。SessionMiddleware と CommonMiddleware の後に配置します:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.locale.LocaleMiddleware", # Must be after SessionMiddleware
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
]
URL ベースの言語切り替えを使用する場合は、URL 設定に django.conf.urls.i18n が含まれていることも確認してください:
from django.conf.urls.i18n import i18n_patterns
urlpatterns = i18n_patterns(
path("", include("myapp.urls")),
)
4. 言語コードの不一致(例:pt-br vs pt_BR)
翻訳は .po ファイルに存在し、compilemessages は成功しますが、Django は特定のロケールの翻訳を無視します。
Django はロケールディレクトリが <language>_<COUNTRY> 形式(アンダースコア区切りで大文字の国コード)に従うことを期待しています。例えば、ブラジルポルトガル語は pt_BR です。ディレクトリ名が pt-br、pt-BR、または ptBR の場合、Django はそれを見つけられません。LANGUAGES 設定にも同じことが当てはまります:そこのコードはハイフン(pt-br)を使用しますが、ファイルシステムはアンダースコア(pt_BR)を使用します。
ディレクトリ構造が Django の期待に一致していることを確認してください:
locale/
pt_BR/
LC_MESSAGES/
django.po
django.mo
設定ではハイフン形式を使用してください:
LANGUAGES = [
("en", "English"),
("pt-br", "Brazilian Portuguese"),
("zh-hans", "Simplified Chinese"),
]
makemessages を実行するときは、ロケールフラグにアンダースコア形式を使用してください:
python manage.py makemessages -l pt_BR
5. Fuzzy エントリがコンパイル時に静かにスキップされる
翻訳は .po ファイルに存在しますが、Django はその特定のエントリに対して実行時に元の英語文字列を表示します。翻訳がファイルにあるのに表示されないため、特にイライラします。
Django の makemessages がソース文字列がわずかに変更されたことを検出すると、既存の翻訳を「fuzzy」(人間のレビューが必要な推測であることを意味する)とマークします。compilemessages コマンドはすべての fuzzy エントリをスキップし、未翻訳として扱います。そのため、エントリは .po ファイルでは翻訳済みに見えますが、.mo ファイルはそれを完全に除外します。
fuzzy エントリはこのように見えます:
#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
翻訳をレビューし、必要に応じて msgstr を更新してから、#, fuzzy フラグを削除してください:
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
その後、再コンパイルしてください:
python manage.py compilemessages
大規模なプロジェクトでは、fuzzy エントリが蓄積し、見逃しやすくなります。TranslateBot の check_translations コマンドはこれらを自動的に検出します:
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
CI パイプラインに check_translations --makemessages として追加すれば、二度と fuzzy エントリをリリースすることはなくなります。
6. LOCALE_PATHS が設定されていないか、間違ったディレクトリを指している
makemessages はある場所に .po ファイルを作成しますが、Django は別の場所でそれを探します。翻訳はディスク上に存在しますが、ロードされません。
Django は特定の順序で翻訳ファイルを検索します:まず LOCALE_PATHS ディレクトリ、次に各アプリの locale/ ディレクトリ、最後にプロジェクトの locale/ ディレクトリです。LOCALE_PATHS が設定されていないか、間違ったパスを指している場合、Django は .po ファイルを見つけられないかもしれません。
設定で LOCALE_PATHS を絶対パスに設定してください:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [
BASE_DIR / "locale",
]
ディレクトリが存在し、期待される構造を持っていることを確認してください:
locale/
de/
LC_MESSAGES/
django.po
django.mo
nl/
LC_MESSAGES/
django.po
django.mo
よくある間違いは、LOCALE_PATHS を絶対パスではなく locale/(相対パス)に設定することです。Django はプロジェクトルートからの相対パスを解決しません。プロセスの作業ディレクトリに依存しますが、それは期待するものとは異なることが多いです。
7. キャッシュが古い翻訳を提供している
翻訳を更新してコンパイルしましたが、古いテキストが表示され続けます。サーバーを再起動すると修正されます。
Django の翻訳カタログはプロセスごとに 1 回ロードされます。本番環境では、Gunicorn や Uvicorn のような WSGI/ASGI サーバーがワーカープロセスを長期間維持します。.mo ファイルはディスク上で変更されたかもしれませんが、実行中のプロセスはまだメモリに古い翻訳を保持しています。さらに、Django のキャッシュフレームワークや Nginx、Cloudflare のようなリバースプロキシを使用している場合、キャッシュされたレスポンスは有効期限が切れるまで古いコンテンツを提供します。
新しい翻訳をデプロイした後、アプリケーションサーバーを再起動してください:
# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)
# Systemd
sudo systemctl restart myapp
# Docker
docker compose restart web
Django のキャッシュフレームワークの場合、翻訳を更新した後にキャッシュをクリアしてください:
from django.core.cache import cache
cache.clear()
開発環境では、runserver は Python ファイルが変更されると自動的にリロードしますが、.mo ファイルは監視しません。compilemessages を実行した後、手動で再起動する必要があります。
8. 文字列が gettext(_() または {% trans %})でラップされていない
makemessages が特定の文字列を抽出しないため、.po ファイルに表示されず、翻訳されません。これは最も基本的な問題であり、大規模なコードベースで最も見落としやすい問題でもあります。
Django の makemessages コマンドは xgettext を使用してソースコードの翻訳マーカーをスキャンします。文字列が gettext()(通常 _() としてエイリアス)、gettext_lazy()、{% trans %}、または {% blocktrans %} でラップされていない場合、抽出プロセスには見えません。
すべてのユーザー向け文字列をラップしてください:
# Python code
from django.utils.translation import gettext_lazy as _
class Article(models.Model):
class Meta:
verbose_name = _("article")
verbose_name_plural = _("articles")
# Views
from django.utils.translation import gettext as _
def my_view(request):
message = _("Your changes have been saved.")
return HttpResponse(message)
<!-- Templates -->
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>
<p>{% blocktrans with count=items|length %}You have {{ count }} items.{% endblocktrans %}</p>
TranslateBot の --dry-run フラグを使用して、現在未翻訳の文字列をプレビューし、抽出時に見逃された文字列をキャッチしてください:
python manage.py translate --target-lang de --dry-run
これは API 呼び出しや変更を行わずに、.po ファイル内のすべての未翻訳エントリを表示します。
9. f-string は翻訳できない(Django の制限)
f-string を _() でラップすると、構文エラーが発生するか、makemessages が翻訳できない壊れた/部分的な文字列を抽出します。
Python の f-string は実行時に評価されます。xgettext 抽出ツールはソースコードを静的に解析するため、{} 中括弧内の Python 式を評価できません。つまり、_(f"Hello, {name}") はリテラルの {name} 式を含む文字列として抽出される(または完全に抽出に失敗する)ため、結果の .po エントリは実行時の文字列と一致しません。
代わりに Django の % フォーマットまたは名前付きプレースホルダーを持つ .format() を使用してください:
# Wrong -- f-string cannot be extracted
message = _(f"Hello, {user.name}! You have {count} new messages.")
# Correct -- named placeholders
message = _("Hello, %(name)s! You have %(count)d new messages.") % {
"name": user.name,
"count": count,
}
# Also correct -- .format() with positional args
message = _("Hello, {0}! You have {1} new messages.").format(user.name, count)
これは TranslateBot やツールの制限ではありません。gettext の動作原理に根本的なものです。ソース文字列は静的リテラルでなければならず、実行時に抽出および検索できるようにする必要があります。
TranslateBot は翻訳中にこれらすべてのプレースホルダー形式(%(name)s、{0}、%s、HTML タグ)を保持するため、翻訳された文字列は完全に機能し続けます。
10. プレースホルダーフォーマット文字列エラーが .po コンパイルを壊す
compilemessages がエラーで失敗するか、.po ファイルに #, python-format フラグの不一致があり、エントリが静かに削除されます。
ソース文字列に %(name)s のような Python フォーマットプレースホルダーが含まれている場合、Django は .po エントリを #, python-format でマークします。翻訳に異なるプレースホルダーがある場合(%(nome)s のようなタイプミス、欠落したプレースホルダー、または余分なプレースホルダー)、gettext ツールがエントリを拒否するか、compilemessages が失敗する可能性があります。これは手動翻訳やプレースホルダーのセマンティクスを理解しない AI 翻訳ツールでよく発生します。
壊れたエントリはこのように見えます:
#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."
ここで %(naam)s は %(name)s であるべきです。プレースホルダーはソースと正確に一致する必要があります。
翻訳された文字列がソースとまったく同じプレースホルダーを含んでいることを確認してください。タイプミス、欠落したプレースホルダー、余分なプレースホルダーをチェックしてください。
これは TranslateBot が真の優位性を提供する分野です。プレースホルダー保持ロジックにより、翻訳出力内のすべてのフォーマット文字列(%(name)s、{0}、%s)がソースと正確に一致することが保証されます。プレースホルダー処理は 100% のテストカバレッジでカバーされているため、翻訳からのフォーマット文字列エラーはコンパイル時にキャッチされるのではなく、ツールレベルで排除されます。
手動で翻訳する場合やプレースホルダーを処理しないツールを使用する場合は、以下で .po ファイルを検証してください:
msgfmt --check-format locale/de/LC_MESSAGES/django.po
これは gettext のフォーマット文字列検証を実行し、不一致を報告します。
すべてをまとめる:防御的なワークフロー
これらの問題のほとんどは共通の根本原因を持っています:忘れやすい手動ステップです。以下は 10 の問題すべてを防ぐワークフローです:
# 1. Extract strings (catches #8 -- any new gettext-wrapped strings)
python manage.py makemessages -a --no-obsolete
# 2. Translate (catches #1, #5, #8, #9, #10 -- handles untranslated,
# fuzzy, and placeholder issues automatically)
python manage.py translate
# 3. Compile (catches #1 -- generates .mo files)
python manage.py compilemessages
# 4. Verify in CI (catches everything that slipped through)
python manage.py check_translations --makemessages
ステップ 4 を CI パイプラインに追加すれば、未翻訳の文字列、fuzzy エントリ、フォーマットエラーが本番環境に到達する前にビルドを失敗させます。
クイックリファレンステーブル
| 原因 | 症状 | ワンライン修正 |
|---|---|---|
compilemessages なし |
翻訳は存在するが表示されない | python manage.py compilemessages |
{% load i18n %} がない |
{% trans %} で TemplateSyntaxError |
テンプレートに {% load i18n %} を追加 |
| LocaleMiddleware がない | 言語が常に英語のデフォルト | MIDDLEWARE に django.middleware.locale.LocaleMiddleware を追加 |
| 言語コードの不一致 | ロケールディレクトリが見つからない | ディレクトリには pt_BR(アンダースコア)、設定には pt-br(ハイフン)を使用 |
| Fuzzy エントリがスキップ | .po に翻訳はあるがアプリにない |
レビュー後に #, fuzzy フラグを削除 |
| 間違った LOCALE_PATHS | .po ファイルは存在するが Django が無視 |
LOCALE_PATHS を絶対パスに設定 |
| キャッシュされた翻訳 | 更新後も古いテキストが表示される | アプリケーションサーバーを再起動 |
| 文字列が gettext にない | 文字列が .po ファイルにない |
_() または {% trans %} でラップ |
| gettext 内の f-string | 壊れた抽出または実行時の不一致 | % または .format() プレースホルダーに置換 |
| プレースホルダーの不一致 | compilemessages 失敗またはエントリ削除 |
ソースと翻訳間でプレースホルダーを正確に一致させる |
翻訳ステップを自動化し、CI でチェックを強制すると、これらの問題のほとんどは解消されます。TranslateBot の translate コマンドはプレースホルダーの保持とインクリメンタル翻訳を処理し、check_translations は本番環境に到達する前にすり抜けたもの(未翻訳のエントリ、fuzzy フラグ、フォーマット文字列の問題)をすべてキャッチします。