Вы пометили строки для перевода, сгенерировали файлы .po, запустили compilemessages, а ваше приложение по-прежнему показывает английский. Вы не одиноки. Фреймворк i18n Django мощный, но у него есть подводные камни, которые ловят даже опытных разработчиков.
Это руководство охватывает 10 самых распространённых причин, по которым переводы Django молча ломаются, с точными симптомами и исправлениями для каждого случая.
1. Забыли запустить compilemessages после редактирования файлов .po
Вы отредактировали файл .po (вручную или с помощью инструмента), но переведённый текст так и не появляется. Приложение продолжает показывать оригинальные английские строки.
Django не читает файлы .po во время выполнения. Он читает скомпилированные бинарные файлы .mo (machine object). Если вы отредактировали файл .po без перекомпиляции, Django не знает, что что-то изменилось.
Запускайте compilemessages после каждого изменения файла .po:
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 %} находятся в библиотеке тегов шаблонов i18n Django. Без её загрузки движок шаблонов не распознаёт их.
Добавьте {% 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 или находится в неправильной позиции
Django всегда отдаёт контент на языке по умолчанию, независимо от заголовка Accept-Language браузера, префикса URL или настроек сессии.
LocaleMiddleware определяет активный язык для каждого запроса. Без него Django использует LANGUAGE_CODE по умолчанию и игнорирует все механизмы выбора языка. Его позиция в стеке middleware тоже важна, так как ему нужен доступ к данным сессии и разрешению URL.
Добавьте LocaleMiddleware в настройку MIDDLEWARE, после 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",
]
Также убедитесь, что django.conf.urls.i18n включён в конфигурацию URL, если вы используете переключение языка на основе URL:
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 показывает оригинальную английскую строку во время выполнения для этой конкретной записи. Это особенно раздражает, потому что перевод прямо там, в файле.
Когда makemessages Django обнаруживает, что исходная строка немного изменилась, он помечает существующий перевод как "fuzzy" (то есть это догадка, требующая проверки человеком). Команда compilemessages пропускает все нечёткие записи, считая их непереведёнными. Поэтому запись выглядит переведённой в файле .po, но файл .mo полностью её исключает.
Нечёткая запись выглядит так:
#, 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
В больших проектах нечёткие записи накапливаются и их легко пропустить. Команда check_translations TranslateBot обнаруживает их автоматически:
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
Добавьте её в ваш CI-пайплайн с check_translations --makemessages, и вы больше никогда не отправите нечёткую запись в продакшн.
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 загружается один раз на процесс. В продакшне серверы WSGI/ASGI, такие как Gunicorn или Uvicorn, поддерживают рабочие процессы живыми в течение длительного времени. Файл .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 и никогда не переводятся. Это самая базовая проблема и одновременно самая легко упускаемая в большой кодовой базе.
Команда makemessages Django использует 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>
Используйте флаг --dry-run TranslateBot для предварительного просмотра непереведённых строк, чтобы обнаружить строки, пропущенные при извлечении:
python manage.py translate --target-lang de --dry-run
Это показывает все непереведённые записи в ваших файлах .po без каких-либо API-вызовов или изменений.
9. f-строки не могут быть переведены (ограничение Django)
Вы оборачиваете f-строку в _() и получаете либо синтаксическую ошибку, либо makemessages извлекает сломанную/частичную строку, которую невозможно перевести.
F-строки Python вычисляются во время выполнения. Инструмент извлечения 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, и запись молча отбрасывается.
Когда исходная строка содержит заполнители формата Python, такие как %(name)s, Django помечает запись .po флагом #, python-format. Если перевод содержит другие заполнители (опечатка вроде %(nome)s, отсутствующий заполнитель или лишний), инструменты gettext могут отклонить запись или compilemessages может завершиться с ошибкой. Это часто происходит при ручных переводах или с инструментами перевода на основе ИИ, которые не понимают семантику заполнителей.
Сломанная запись выглядит так:
#, 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-пайплайн, и непереведённые строки, нечёткие записи и ошибки форматирования приведут к сбою сборки до попадания в продакшн.
Таблица быстрого справочника
| Причина | Симптом | Исправление одной строкой |
|---|---|---|
Нет compilemessages |
Переводы есть, но не отображаются | python manage.py compilemessages |
Отсутствует {% load i18n %} |
TemplateSyntaxError при {% trans %} |
Добавить {% load i18n %} в шаблон |
| Отсутствует LocaleMiddleware | Язык всегда английский по умолчанию | Добавить django.middleware.locale.LocaleMiddleware в MIDDLEWARE |
| Несоответствие кода языка | Директория локали не найдена | Использовать pt_BR (подчёркивание) для директорий, pt-br (дефис) для настроек |
| Fuzzy-записи пропущены | Перевод в .po, но не в приложении |
Удалить флаг #, fuzzy после проверки |
| Неправильный LOCALE_PATHS | Файлы .po есть, но Django их игнорирует |
Установить LOCALE_PATHS как абсолютный путь |
| Кешированные переводы | Старый текст после обновления | Перезапустить сервер приложения |
| Строка не в gettext | Строка отсутствует в файлах .po |
Обернуть в _() или {% trans %} |
| f-строка в gettext | Сломанное извлечение или несоответствие при выполнении | Заменить на заполнители % или .format() |
| Несоответствие заполнителей | compilemessages падает или запись отброшена |
Точно совместить заполнители между исходной строкой и переводом |
Большинство этих проблем исчезают, когда вы автоматизируете шаг перевода и применяете проверки в CI. Команда translate TranslateBot обрабатывает сохранение заполнителей и инкрементальный перевод, а check_translations ловит всё, что проскользнуло (непереведённые записи, флаги fuzzy и проблемы с форматными строками), до попадания в продакшн.