Назад до блогу

Чому переклади Django ламаються (і як виправити 10 найпоширеніших причин)

2026-02-04 9 хв читання
Чому переклади Django ламаються (і як виправити 10 найпоширеніших причин)

Ви позначили рядки для перекладу, згенерували файли .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 пропускає всі 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 записи накопичуються і їх легко пропустити. Команда check_translations TranslateBot автоматично їх знаходить:

python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed

Додайте check_translations --makemessages до вашого CI pipeline і ви більше ніколи не відправите 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 завантажується один раз на процес. У продакшені 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 pipeline, і неперекладені рядки, fuzzy записи та помилки форматування зупинять збірку до того, як вони потраплять у продакшен.

Таблиця швидкої довідки

Причина Симптом Однорядкове виправлення
Відсутній 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 та проблеми з рядками форматування), до того як це потрапить у продакшен.

Припиніть редагувати .po файли вручну

TranslateBot автоматизує переклади Django за допомогою ШІ. Одна команда, всі ваші мови, копійки за переклад.