Powrót do bloga

Dlaczego tłumaczenia Django się psują (i jak naprawić 10 najczęstszych przyczyn)

2026-02-04 9 min czytania
Dlaczego tłumaczenia Django się psują (i jak naprawić 10 najczęstszych przyczyn)

Oznaczyłeś ciągi znaków do tłumaczenia, wygenerowałeś pliki .po, uruchomiłeś compilemessages, a Twoja aplikacja nadal wyświetla angielski. Nie jesteś sam. Framework i18n Django jest potężny, ale ma ostre krawędzie, które łapią nawet doświadczonych programistów.

Ten przewodnik obejmuje 10 najczęstszych powodów, dla których tłumaczenia Django cicho zawodzą, z dokładnymi objawami i poprawkami dla każdego z nich.

1. Zapomnienie o uruchomieniu compilemessages po edycji plików .po

Edytowałeś plik .po (ręcznie lub za pomocą narzędzia), ale przetłumaczony tekst nigdy się nie pojawia. Aplikacja nadal wyświetla oryginalne ciągi w języku angielskim.

Django nie czyta plików .po w czasie wykonywania. Zamiast tego czyta skompilowane pliki binarne .mo (machine object). Jeśli edytujesz plik .po bez ponownej kompilacji, Django nie wie, że coś się zmieniło.

Uruchom compilemessages po każdej zmianie pliku .po:

python manage.py compilemessages

Jeśli automatyzujesz tłumaczenia za pomocą TranslateBot, dodaj compilemessages jako ostatni krok w swoim przepływie pracy:

python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages

2. Brak {% load i18n %} w szablonach

Używasz {% trans "Hello" %} w szablonie, ale Django zgłasza TemplateSyntaxError. Albo gorzej, tag cicho nic nie robi, jeśli masz źle skonfigurowany silnik szablonów.

Tagi {% trans %} i {% blocktrans %} znajdują się w bibliotece tagów szablonów i18n Django. Bez załadowania jej silnik szablonów ich nie rozpoznaje.

Dodaj {% load i18n %} na początku każdego szablonu, który używa tagów tłumaczenia:

{% load i18n %}

<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>

To jest wymóg per-szablonowy. Nawet jeśli szablon nadrzędny ładuje i18n, szablony potomne używające tagów tłumaczenia potrzebują własnej deklaracji {% load i18n %}.

3. LocaleMiddleware nie jest w MIDDLEWARE lub jest w złej pozycji

Django zawsze serwuje zawartość w domyślnym języku niezależnie od nagłówka Accept-Language przeglądarki, prefiksu URL czy ustawień sesji.

LocaleMiddleware określa aktywny język dla każdego żądania. Bez niego Django domyślnie używa LANGUAGE_CODE i ignoruje wszystkie mechanizmy wyboru języka. Jego pozycja w stosie middleware również ma znaczenie, ponieważ potrzebuje dostępu do danych sesji i rozwiązywania URL.

Dodaj LocaleMiddleware do ustawienia MIDDLEWARE, po SessionMiddleware i 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",
]

Upewnij się również, że django.conf.urls.i18n jest uwzględniony w konfiguracji URL, jeśli używasz przełączania języka opartego na URL:

from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path("", include("myapp.urls")),
)

4. Niezgodności kodów języków (np. pt-br vs pt_BR)

Tłumaczenia istnieją w plikach .po, compilemessages kończy się sukcesem, ale Django ignoruje tłumaczenie dla pewnych locale.

Django oczekuje, że katalogi locale będą w formacie <language>_<COUNTRY> z podkreślnikiem jako separatorem i wielką literą kodu kraju. Na przykład pt_BR dla portugalskiego brazylijskiego. Jeśli Twój katalog nazywa się pt-br, pt-BR lub ptBR, Django go nie znajdzie. To samo dotyczy ustawienia LANGUAGES: kody tam używają myślników (pt-br), ale system plików używa podkreślników (pt_BR).

Upewnij się, że struktura katalogów odpowiada oczekiwaniom Django:

locale/
    pt_BR/
        LC_MESSAGES/
            django.po
            django.mo

A w ustawieniach używaj formy z myślnikiem:

LANGUAGES = [
    ("en", "English"),
    ("pt-br", "Brazilian Portuguese"),
    ("zh-hans", "Simplified Chinese"),
]

Uruchamiając makemessages, użyj formy z podkreślnikiem dla flagi locale:

python manage.py makemessages -l pt_BR

5. Wpisy fuzzy cicho pomijane podczas kompilacji

Tłumaczenie istnieje w pliku .po, ale Django wyświetla oryginalny ciąg angielski w czasie wykonywania dla tego konkretnego wpisu. Jest to szczególnie frustrujące, ponieważ tłumaczenie jest tuż obok w pliku.

Gdy makemessages Django wykryje, że ciąg źródłowy nieco się zmienił, oznacza istniejące tłumaczenie jako "fuzzy" (co oznacza, że to zgadywanie wymagające ludzkiej weryfikacji). Polecenie compilemessages pomija wszystkie wpisy fuzzy, traktując je jako nieprzetłumaczone. Więc wpis wygląda na przetłumaczony w pliku .po, ale plik .mo całkowicie go wyklucza.

Wpis fuzzy wygląda tak:

#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

Przejrzyj tłumaczenie, zaktualizuj msgstr jeśli to konieczne, a następnie usuń flagę #, fuzzy:

msgid "Welcome to our website!"
msgstr "Welkom op onze website!"

Następnie ponownie skompiluj:

python manage.py compilemessages

W większym projekcie wpisy fuzzy się kumulują i łatwo je przeoczyć. Polecenie check_translations TranslateBot wykrywa je automatycznie:

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

Dodaj check_translations --makemessages do swojego pipeline CI i nigdy więcej nie wyślesz wpisu fuzzy.

6. LOCALE_PATHS nie skonfigurowany lub wskazujący na zły katalog

makemessages tworzy pliki .po w jednej lokalizacji, ale Django szuka ich gdzie indziej. Tłumaczenia istnieją na dysku, ale nigdy się nie ładują.

Django szuka plików tłumaczeń w określonej kolejności: najpierw katalogi LOCALE_PATHS, następnie katalog locale/ każdej aplikacji, a na końcu katalog locale/ projektu. Jeśli LOCALE_PATHS nie jest ustawiony lub wskazuje na złą ścieżkę, Django może nigdy nie znaleźć Twoich plików .po.

Ustaw LOCALE_PATHS w ustawieniach na ścieżkę bezwzględną:

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

LOCALE_PATHS = [
    BASE_DIR / "locale",
]

Sprawdź, czy katalog istnieje i zawiera oczekiwaną strukturę:

locale/
    de/
        LC_MESSAGES/
            django.po
            django.mo
    nl/
        LC_MESSAGES/
            django.po
            django.mo

Częstym błędem jest ustawienie LOCALE_PATHS na locale/ (względną) zamiast ścieżki bezwzględnej. Django nie rozwiązuje ścieżek względnych z katalogu głównego projektu. Zależy to od katalogu roboczego procesu, który często nie jest tym, czego się spodziewasz.

7. Cache serwuje stare tłumaczenia

Zaktualizowałeś i skompilowałeś tłumaczenia, ale stary tekst nadal się pojawia. Restart serwera rozwiązuje problem.

Katalog tłumaczeń Django jest ładowany raz na proces. W produkcji serwery WSGI/ASGI takie jak Gunicorn lub Uvicorn utrzymują procesy workerów przez dłuższy czas. Plik .mo mógł się zmienić na dysku, ale działający proces nadal ma stare tłumaczenia w pamięci. Ponadto, jeśli używasz frameworka cache Django lub odwrotnego proxy takiego jak Nginx lub Cloudflare, buforowane odpowiedzi będą serwować starą zawartość do momentu wygaśnięcia.

Zrestartuj serwer aplikacji po wdrożeniu nowych tłumaczeń:

# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)

# Systemd
sudo systemctl restart myapp

# Docker
docker compose restart web

Dla frameworka cache Django wyczyść cache po aktualizacji tłumaczeń:

from django.core.cache import cache
cache.clear()

Podczas programowania runserver automatycznie się przeładowuje, gdy zmieniają się pliki Python, ale nie monitoruje plików .mo. Musisz go ręcznie zrestartować po uruchomieniu compilemessages.

8. Ciągi nie opakowane w gettext (_() lub {% trans %})

makemessages nie wyodrębnia pewnych ciągów, więc nigdy nie pojawiają się w plikach .po i nigdy nie są tłumaczone. To jest najbardziej podstawowy problem i jednocześnie najłatwiejszy do przeoczenia w dużej bazie kodu.

Polecenie makemessages Django używa xgettext do skanowania kodu źródłowego w poszukiwaniu znaczników tłumaczenia. Jeśli ciąg nie jest opakowany w gettext() (zwykle aliasowany jako _()), gettext_lazy(), {% trans %} lub {% blocktrans %}, jest niewidoczny dla procesu wyodrębniania.

Opakuj każdy ciąg widoczny dla użytkownika:

# 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>

Użyj flagi --dry-run TranslateBot, aby zobaczyć podgląd aktualnie nieprzetłumaczonych ciągów, dzięki czemu złapiesz ciągi pominięte podczas wyodrębniania:

python manage.py translate --target-lang de --dry-run

To pokazuje wszystkie nieprzetłumaczone wpisy w plikach .po bez wykonywania jakichkolwiek wywołań API czy zmian.

9. f-stringi nie mogą być tłumaczone (ograniczenie Django)

Opakowujesz f-string w _() i albo dostajesz błąd składni, albo makemessages wyodrębnia uszkodzony/częściowy ciąg, którego nie da się przetłumaczyć.

F-stringi Pythona są ewaluowane w czasie wykonywania. Narzędzie ekstrakcji xgettext parsuje kod źródłowy statycznie, więc nie może ewaluować wyrażeń Pythona wewnątrz nawiasów {}. Oznacza to, że _(f"Hello, {name}") jest wyodrębniany jako ciąg zawierający literalne wyrażenie {name} (lub w ogóle nie jest wyodrębniany), a wynikowy wpis .po nigdy nie będzie pasował do ciągu w czasie wykonywania.

Użyj formatowania % Django lub .format() z nazwanymi symbolami zastępczymi:

# 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)

To nie jest ograniczenie TranslateBot ani narzędzia. Jest to fundamentalne dla sposobu działania gettext. Ciąg źródłowy musi być statycznym literałem, aby mógł zostać wyodrębniony i wyszukany w czasie wykonywania.

TranslateBot zachowuje wszystkie te formaty symboli zastępczych (%(name)s, {0}, %s, tagi HTML) podczas tłumaczenia, więc przetłumaczone ciągi pozostają w pełni funkcjonalne.

10. Błędy formatowania symboli zastępczych psują kompilację .po

compilemessages zawodzi z błędem lub plik .po ma niezgodność flagi #, python-format, a wpis jest cicho usuwany.

Gdy ciąg źródłowy zawiera symbole zastępcze formatu Pythona jak %(name)s, Django oznacza wpis .po jako #, python-format. Jeśli tłumaczenie ma inne symbole zastępcze (literówka jak %(nome)s, brakujący symbol zastępczy lub dodatkowy), narzędzia gettext mogą odrzucić wpis lub compilemessages może zawieść. To zwykle zdarza się przy ręcznych tłumaczeniach lub przy narzędziach tłumaczenia AI, które nie rozumieją semantyki symboli zastępczych.

Uszkodzony wpis wygląda tak:

#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."

Tutaj %(naam)s powinno być %(name)s. Symbole zastępcze muszą dokładnie odpowiadać źródłu.

Upewnij się, że przetłumaczone ciągi zawierają dokładnie te same symbole zastępcze co źródło. Sprawdź literówki, brakujące symbole zastępcze i dodatkowe symbole zastępcze.

To jest jeden z obszarów, w którym TranslateBot zapewnia realną przewagę. Jego logika zachowywania symboli zastępczych gwarantuje, że wszystkie ciągi formatujące (%(name)s, {0}, %s) w przetłumaczonym wyniku dokładnie odpowiadają źródłu. Obsługa symboli zastępczych jest pokryta 100% testami, więc błędy ciągów formatujących z tłumaczenia są eliminowane na poziomie narzędzia, a nie wyłapywane w czasie kompilacji.

Jeśli tłumaczysz ręcznie lub używasz narzędzia, które nie obsługuje symboli zastępczych, zwaliduj pliki .po za pomocą:

msgfmt --check-format locale/de/LC_MESSAGES/django.po

To uruchamia walidację ciągów formatujących gettext i raportuje wszelkie niezgodności.

Wszystko razem: defensywny przepływ pracy

Większość tych problemów ma wspólną przyczynę: ręczne kroki, o których łatwo zapomnieć. Oto przepływ pracy, który zapobiega wszystkim 10 problemom:

# 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

Dodaj krok 4 do swojego pipeline CI, a nieprzetłumaczone ciągi, wpisy fuzzy i błędy formatowania spowodują niepowodzenie builda zanim dotrą na produkcję.

Tabela szybkiej referencji

Przyczyna Objaw Poprawka jednolinijkowa
Brak compilemessages Tłumaczenia istnieją, ale się nie pojawiają python manage.py compilemessages
Brak {% load i18n %} TemplateSyntaxError na {% trans %} Dodaj {% load i18n %} do szablonu
Brak LocaleMiddleware Język zawsze domyślnie angielski Dodaj django.middleware.locale.LocaleMiddleware do MIDDLEWARE
Niezgodność kodu języka Katalog locale nie znaleziony Używaj pt_BR (podkreślnik) dla katalogów, pt-br (myślnik) dla ustawień
Wpisy fuzzy pominięte Tłumaczenie w .po, ale nie w aplikacji Usuń flagę #, fuzzy po weryfikacji
Zły LOCALE_PATHS Pliki .po istnieją, ale Django je ignoruje Ustaw LOCALE_PATHS na ścieżkę bezwzględną
Zbuforowane tłumaczenia Stary tekst pojawia się po aktualizacji Zrestartuj serwer aplikacji
Ciąg nie w gettext Ciąg brakuje w plikach .po Opakuj w _() lub {% trans %}
f-string w gettext Uszkodzone wyodrębnianie lub niezgodność runtime Zastąp symbolami zastępczymi % lub .format()
Niezgodność symboli zastępczych compilemessages zawodzi lub wpis usunięty Dopasuj symbole zastępcze dokładnie między źródłem a tłumaczeniem

Większość tych problemów znika, gdy zautomatyzujesz krok tłumaczenia i wymusisz kontrole w CI. Polecenie translate TranslateBot obsługuje zachowywanie symboli zastępczych i tłumaczenie przyrostowe, podczas gdy check_translations łapie wszystko, co się wymknęło (nieprzetłumaczone wpisy, flagi fuzzy i problemy z ciągami formatującymi) zanim dotrą na produkcję.

Przestań ręcznie edytować pliki .po

TranslateBot automatyzuje tłumaczenia Django za pomocą AI. Jedno polecenie, wszystkie języki, grosze za tłumaczenie.