Ai marcat string-urile pentru traducere, ai generat fișiere .po, ai rulat compilemessages, iar aplicația ta încă afișează în engleză. Nu ești singur. Framework-ul i18n al Django este puternic, dar are colțuri ascuțite care prind chiar și dezvoltatorii experimentați.
Acest ghid acoperă cele mai frecvente 10 motive pentru care traducerile Django eșuează silențios, cu simptome exacte și remedieri pentru fiecare.
1. Uitarea de a rula compilemessages după editarea fișierelor .po
Ai editat un fișier .po (manual sau cu un instrument), dar textul tradus nu apare niciodată. Aplicația continuă să afișeze string-urile originale în engleză.
Django nu citește fișierele .po la runtime. Citește în schimb fișierele binare .mo (machine object) compilate. Dacă editezi un fișier .po fără a recompila, Django nu știe că s-a schimbat ceva.
Rulează compilemessages după fiecare modificare a fișierului .po:
python manage.py compilemessages
Dacă îți automatizezi traducerile cu TranslateBot, adaugă compilemessages ca ultimul pas în fluxul tău de lucru:
python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages
2. Lipsă {% load i18n %} în template-uri
Folosești {% trans "Hello" %} într-un template, dar Django aruncă o eroare TemplateSyntaxError. Sau mai rău, tag-ul nu face nimic în liniște dacă ai un motor de template configurat greșit.
Tag-urile {% trans %} și {% blocktrans %} se află în biblioteca de tag-uri template i18n a Django. Fără a o încărca, motorul de template nu le recunoaște.
Adaugă {% load i18n %} în partea de sus a fiecărui template care folosește tag-uri de traducere:
{% load i18n %}
<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>
Aceasta este o cerință per template. Chiar dacă un template părinte încarcă i18n, template-urile copil care folosesc tag-uri de traducere au nevoie de propria lor declarație {% load i18n %}.
3. LocaleMiddleware nu este în MIDDLEWARE sau este în poziția greșită
Django servește întotdeauna conținut în limba implicită, indiferent de header-ul Accept-Language al browser-ului, prefixul URL sau setările de sesiune.
LocaleMiddleware determină limba activă pentru fiecare cerere. Fără el, Django folosește implicit LANGUAGE_CODE și ignoră toate mecanismele de selectare a limbii. Poziția sa în stiva de middleware contează de asemenea, deoarece are nevoie de acces la datele de sesiune și rezoluția URL.
Adaugă LocaleMiddleware la setarea MIDDLEWARE, după 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",
]
Asigură-te de asemenea că django.conf.urls.i18n este inclus în configurația URL dacă folosești schimbarea limbii bazată pe URL:
from django.conf.urls.i18n import i18n_patterns
urlpatterns = i18n_patterns(
path("", include("myapp.urls")),
)
4. Nepotriviri ale codurilor de limbă (de ex. pt-br vs pt_BR)
Traducerile există în fișierele .po, compilemessages reușește, dar Django ignoră traducerea pentru anumite locale-uri.
Django se așteaptă ca directoarele locale să urmeze formatul <language>_<COUNTRY> cu separator underscore și cod de țară cu litere mari. De exemplu, pt_BR pentru portugheza braziliană. Dacă directorul tău se numește pt-br, pt-BR sau ptBR, Django nu îl va găsi. Același lucru se aplică și setării LANGUAGES: codurile de acolo folosesc cratimă (pt-br), dar sistemul de fișiere folosește underscore (pt_BR).
Asigură-te că structura directorului corespunde așteptărilor Django:
locale/
pt_BR/
LC_MESSAGES/
django.po
django.mo
Și în setări, folosește forma cu cratimă:
LANGUAGES = [
("en", "English"),
("pt-br", "Brazilian Portuguese"),
("zh-hans", "Simplified Chinese"),
]
Când rulezi makemessages, folosește forma cu underscore pentru flag-ul locale:
python manage.py makemessages -l pt_BR
5. Intrări fuzzy ignorate silențios în timpul compilării
O traducere există în fișierul .po, dar Django afișează string-ul original în engleză la runtime pentru acea intrare specifică. Aceasta este deosebit de frustrantă deoarece traducerea este chiar acolo în fișier.
Când makemessages al Django detectează că un string sursă s-a schimbat ușor, marchează traducerea existentă ca "fuzzy" (adică o estimare care necesită revizuire umană). Comanda compilemessages sare peste toate intrările fuzzy, tratându-le ca netraduse. Deci intrarea arată tradusă în fișierul .po, dar fișierul .mo o exclude complet.
O intrare fuzzy arată așa:
#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
Revizuiește traducerea, actualizează msgstr dacă este necesar, apoi elimină flag-ul #, fuzzy:
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
Apoi recompilează:
python manage.py compilemessages
Într-un proiect mai mare, intrările fuzzy se acumulează și sunt ușor de ratat. Comanda check_translations a TranslateBot le detectează automat:
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
Adaugă check_translations --makemessages la pipeline-ul CI și nu vei mai livra niciodată o intrare fuzzy.
6. LOCALE_PATHS neconfigurat sau care indică spre directorul greșit
makemessages creează fișiere .po într-o locație, dar Django le caută în altă parte. Traducerile există pe disc dar nu sunt niciodată încărcate.
Django caută fișierele de traducere într-o ordine specifică: directoarele LOCALE_PATHS mai întâi, apoi directorul locale/ al fiecărei aplicații, și în final directorul locale/ al proiectului. Dacă LOCALE_PATHS nu este setat sau indică spre o cale greșită, Django s-ar putea să nu găsească niciodată fișierele .po.
Setează LOCALE_PATHS în setările tale la o cale absolută:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [
BASE_DIR / "locale",
]
Verifică dacă directorul există și conține structura așteptată:
locale/
de/
LC_MESSAGES/
django.po
django.mo
nl/
LC_MESSAGES/
django.po
django.mo
O greșeală frecventă este setarea LOCALE_PATHS la locale/ (relativ) în loc de o cale absolută. Django nu rezolvă căile relative din rădăcina proiectului. Depinde de directorul de lucru al procesului, care de multe ori nu este ceea ce te aștepți.
7. Cache-ul servește traduceri vechi
Ai actualizat și compilat traducerile, dar textul vechi continuă să apară. Repornirea serverului rezolvă problema.
Catalogul de traduceri al Django este încărcat o singură dată per proces. În producție, serverele WSGI/ASGI precum Gunicorn sau Uvicorn mențin procesele worker active pentru perioade lungi. Fișierul .mo s-ar fi putut schimba pe disc, dar procesul în execuție are încă traducerile vechi în memorie. Pe deasupra, dacă folosești framework-ul cache al Django sau un reverse proxy precum Nginx sau Cloudflare, răspunsurile din cache vor servi conținut vechi până expiră.
Repornește serverul aplicației după deploy-ul noilor traduceri:
# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)
# Systemd
sudo systemctl restart myapp
# Docker
docker compose restart web
Pentru framework-ul cache al Django, golește cache-ul după actualizarea traducerilor:
from django.core.cache import cache
cache.clear()
În dezvoltare, runserver se reîncarcă automat când se schimbă fișierele Python dar nu monitorizează fișierele .mo. Trebuie să-l repornești manual după rularea compilemessages.
8. String-uri neîmpachetate în gettext (_() sau {% trans %})
makemessages nu extrage anumite string-uri, deci nu apar niciodată în fișierele .po și nu sunt niciodată traduse. Aceasta este cea mai de bază problemă și totodată cea mai ușor de trecut cu vederea într-o bază de cod mare.
Comanda makemessages a Django folosește xgettext pentru a scana codul sursă în căutarea marcajelor de traducere. Dacă un string nu este împachetat în gettext() (aliasul comun _()), gettext_lazy(), {% trans %} sau {% blocktrans %}, este invizibil pentru procesul de extracție.
Împachetează fiecare string vizibil utilizatorului:
# 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>
Folosește flag-ul --dry-run al TranslateBot pentru a previzualiza ce string-uri sunt în prezent netraduse, astfel încât să poți identifica string-urile ratate în timpul extracției:
python manage.py translate --target-lang de --dry-run
Aceasta arată toate intrările netraduse din fișierele .po fără a face apeluri API sau modificări.
9. f-string-urile nu pot fi traduse (Limitare Django)
Împachetezi un f-string în _() și fie primești o eroare de sintaxă, fie makemessages extrage un string deteriorat/parțial care nu poate fi tradus.
f-string-urile Python sunt evaluate la runtime. Instrumentul de extracție xgettext parsează codul sursă static, deci nu poate evalua expresii Python din interiorul acoladelor {}. Aceasta înseamnă că _(f"Hello, {name}") este extras ca un string conținând o expresie literală {name} (sau eșuează complet la extracție), iar intrarea .po rezultată nu va corespunde niciodată cu string-ul de runtime.
Folosește în schimb formatarea % a Django sau .format() cu substituenți numiți:
# 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)
Aceasta nu este o limitare a TranslateBot sau a instrumentelor. Este fundamentală pentru modul în care funcționează gettext. String-ul sursă trebuie să fie un literal static pentru a putea fi extras și căutat la runtime.
TranslateBot păstrează toate aceste formate de substituenți (%(name)s, {0}, %s, tag-uri HTML) în timpul traducerii, astfel încât string-urile traduse rămân complet funcționale.
10. Erori de format string ale substituenților care strică compilarea .po
compilemessages eșuează cu o eroare, sau fișierul .po are o nepotrivire a flag-ului #, python-format, iar intrarea este eliminată silențios.
Când un string sursă conține substituenți de format Python precum %(name)s, Django marchează intrarea .po cu #, python-format. Dacă traducerea are substituenți diferiți (o greșeală de tastare precum %(nome)s, un substituent lipsă sau unul în plus), instrumentele gettext pot respinge intrarea sau compilemessages poate eșua. Aceasta se întâmplă frecvent cu traducerile manuale sau cu instrumentele de traducere AI care nu înțeleg semantica substituenților.
O intrare deteriorată arată așa:
#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."
Aici %(naam)s ar trebui să fie %(name)s. Substituenții trebuie să corespundă exact cu sursa.
Asigură-te că string-urile traduse conțin exact aceiași substituenți ca sursa. Verifică greșelile de tastare, substituenții lipsă și substituenții în plus.
Aceasta este o zonă în care TranslateBot oferă un avantaj real. Logica sa de păstrare a substituenților asigură că toate string-urile de format (%(name)s, {0}, %s) din output-ul tradus corespund exact cu sursa. Gestionarea substituenților este acoperită de 100% test coverage, astfel încât erorile de format string din traducere sunt eliminate la nivel de instrument, nu detectate la momentul compilării.
Dacă traduci manual sau cu un instrument care nu gestionează substituenții, validează fișierele .po cu:
msgfmt --check-format locale/de/LC_MESSAGES/django.po
Aceasta rulează validarea format string a gettext și raportează orice nepotriviri.
Punerea tuturor la un loc: un flux de lucru defensiv
Majoritatea acestor probleme au o cauză rădăcină comună: pași manuali care sunt ușor de uitat. Iată un flux de lucru care previne toate cele 10 probleme:
# 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
Adaugă pasul 4 la pipeline-ul CI și string-urile netraduse, intrările fuzzy și erorile de format vor face build-ul să eșueze înainte de a ajunge în producție.
Tabel de referință rapidă
| Cauza | Simptom | Remediere într-o linie |
|---|---|---|
Fără compilemessages |
Traducerile există dar nu apar | python manage.py compilemessages |
Lipsă {% load i18n %} |
TemplateSyntaxError pe {% trans %} |
Adaugă {% load i18n %} la template |
| Lipsă LocaleMiddleware | Limba este mereu implicit engleză | Adaugă django.middleware.locale.LocaleMiddleware la MIDDLEWARE |
| Nepotrivire cod limbă | Director locale negăsit | Folosește pt_BR (underscore) pentru directoare, pt-br (cratimă) pentru setări |
| Intrări fuzzy ignorate | Traducere în .po dar nu în aplicație |
Elimină flag-ul #, fuzzy după revizuire |
| LOCALE_PATHS greșit | Fișierele .po există dar Django le ignoră |
Setează LOCALE_PATHS la o cale absolută |
| Traduceri în cache | Text vechi apare după actualizare | Repornește serverul aplicației |
| String nu în gettext | String lipsește din fișierele .po |
Împachetează în _() sau {% trans %} |
| f-string în gettext | Extracție deteriorată sau nepotrivire runtime | Înlocuiește cu substituenți % sau .format() |
| Nepotrivire substituenți | compilemessages eșuează sau intrarea eliminată |
Potrivește substituenții exact între sursă și traducere |
Majoritatea acestor probleme dispar când automatizezi pasul de traducere și aplici verificări în CI. Comanda translate a TranslateBot gestionează păstrarea substituenților și traducerea incrementală, în timp ce check_translations prinde orice scapă (intrări netraduse, flag-uri fuzzy și probleme de format string) înainte de a ajunge în producție.