Terug naar blog

Waarom Django-vertalingen mislukken (en hoe de 10 meest voorkomende oorzaken op te lossen)

2026-02-04 9 min lezen
Waarom Django-vertalingen mislukken (en hoe de 10 meest voorkomende oorzaken op te lossen)

Je hebt strings gemarkeerd voor vertaling, .po-bestanden gegenereerd, compilemessages uitgevoerd, en je app toont nog steeds Engels. Je bent niet de enige. Django's i18n-framework is krachtig, maar heeft scherpe randjes die zelfs ervaren ontwikkelaars verrassen.

Deze gids behandelt de 10 meest voorkomende redenen waarom Django-vertalingen stilletjes falen, met exacte symptomen en oplossingen voor elk probleem.

1. Vergeten compilemessages uit te voeren na het bewerken van .po-bestanden

Je hebt een .po-bestand bewerkt (handmatig of met een tool), maar de vertaalde tekst verschijnt nooit. De app blijft de originele Engelse strings tonen.

Django leest geen .po-bestanden tijdens runtime. Het leest in plaats daarvan de gecompileerde .mo (machine object) binaire bestanden. Als je een .po-bestand bewerkt zonder opnieuw te compileren, weet Django niet dat er iets is veranderd.

Voer compilemessages uit na elke wijziging aan een .po-bestand:

python manage.py compilemessages

Als je je vertalingen automatiseert met TranslateBot, voeg dan compilemessages toe als laatste stap in je workflow:

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

2. Ontbrekende {% load i18n %} in templates

Je gebruikt {% trans "Hello" %} in een template, maar Django geeft een TemplateSyntaxError. Of erger nog, de tag doet stilletjes niets als je een verkeerd geconfigureerde template-engine hebt.

De {% trans %}- en {% blocktrans %}-tags zitten in Django's i18n template-tagbibliotheek. Zonder deze te laden herkent de template-engine ze niet.

Voeg {% load i18n %} toe bovenaan elke template die vertaaltags gebruikt:

{% load i18n %}

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

Dit is een vereiste per template. Zelfs als een bovenliggende template i18n laadt, hebben onderliggende templates die vertaaltags gebruiken hun eigen {% load i18n %}-declaratie nodig.

3. LocaleMiddleware ontbreekt in MIDDLEWARE of staat op de verkeerde positie

Django serveert altijd content in de standaardtaal, ongeacht de Accept-Language-header van de browser, het URL-voorvoegsel of sessie-instellingen.

LocaleMiddleware bepaalt de actieve taal voor elk verzoek. Zonder deze middleware gebruikt Django standaard LANGUAGE_CODE en negeert alle taalkeuzemechanismen. De positie in de middleware-stack is ook belangrijk, omdat het toegang nodig heeft tot sessiegegevens en URL-resolutie.

Voeg LocaleMiddleware toe aan je MIDDLEWARE-instelling, na SessionMiddleware en 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",
]

Zorg er ook voor dat django.conf.urls.i18n is opgenomen in je URL-configuratie als je URL-gebaseerde taalwisseling gebruikt:

from django.conf.urls.i18n import i18n_patterns

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

4. Taalcode-mismatches (bijv. pt-br vs pt_BR)

Vertalingen bestaan in je .po-bestanden, compilemessages slaagt, maar Django negeert de vertaling voor bepaalde locales.

Django verwacht dat locale-directories het formaat <language>_<COUNTRY> volgen met een underscore als scheidingsteken en een landcode in hoofdletters. Bijvoorbeeld pt_BR voor Braziliaans Portugees. Als je directory pt-br, pt-BR of ptBR heet, vindt Django hem niet. Hetzelfde geldt voor de LANGUAGES-instelling: de codes daar gebruiken koppeltekens (pt-br), maar het bestandssysteem gebruikt underscores (pt_BR).

Zorg dat je directorystructuur overeenkomt met wat Django verwacht:

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

En gebruik in je instellingen de vorm met koppelteken:

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

Bij het uitvoeren van makemessages gebruik je de underscore-vorm voor de locale-vlag:

python manage.py makemessages -l pt_BR

5. Fuzzy-vermeldingen worden stilletjes overgeslagen tijdens compilatie

Er staat een vertaling in het .po-bestand, maar Django toont de originele Engelse string tijdens runtime voor die specifieke vermelding. Dit is bijzonder frustrerend omdat de vertaling er gewoon staat in het bestand.

Wanneer Django's makemessages detecteert dat een bronstring licht is gewijzigd, markeert het de bestaande vertaling als "fuzzy" (wat betekent dat het een schatting is die menselijke beoordeling nodig heeft). Het compilemessages-commando slaat alle fuzzy-vermeldingen over en behandelt ze als onvertaald. De vermelding ziet er dus vertaald uit in het .po-bestand, maar het .mo-bestand sluit het volledig uit.

Een fuzzy-vermelding ziet er zo uit:

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

Controleer de vertaling, werk msgstr bij indien nodig, en verwijder dan de #, fuzzy-vlag:

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

Compileer daarna opnieuw:

python manage.py compilemessages

In een groter project stapelen fuzzy-vermeldingen zich op en zijn ze makkelijk te missen. Het check_translations-commando van TranslateBot vangt deze automatisch op:

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

Voeg check_translations --makemessages toe aan je CI-pipeline en je zult nooit meer een fuzzy-vermelding uitrollen.

6. LOCALE_PATHS niet geconfigureerd of wijst naar de verkeerde directory

makemessages maakt .po-bestanden aan op één locatie, maar Django zoekt ze ergens anders. Vertalingen bestaan op schijf maar worden nooit geladen.

Django zoekt vertaalbestanden in een specifieke volgorde: eerst LOCALE_PATHS-directories, dan de locale/-directory van elke app, en ten slotte de locale/-directory van het project. Als LOCALE_PATHS niet is ingesteld of naar het verkeerde pad wijst, vindt Django mogelijk nooit je .po-bestanden.

Stel LOCALE_PATHS in je instellingen in op een absoluut pad:

from pathlib import Path

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

LOCALE_PATHS = [
    BASE_DIR / "locale",
]

Controleer of de directory bestaat en de verwachte structuur bevat:

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

Een veelgemaakte fout is LOCALE_PATHS instellen op locale/ (relatief) in plaats van een absoluut pad. Django lost relatieve paden niet op vanuit je projectroot. Het hangt af van de werkdirectory van het proces, wat vaak niet is wat je verwacht.

7. Cache serveert verouderde vertalingen

Je hebt vertalingen bijgewerkt en gecompileerd, maar de oude tekst blijft verschijnen. De server herstarten lost het op.

Django's vertaalcatalogus wordt eenmaal per proces geladen. In productie houden WSGI/ASGI-servers zoals Gunicorn of Uvicorn werkprocessen langdurig in leven. Het .mo-bestand is mogelijk gewijzigd op schijf, maar het lopende proces heeft nog steeds de oude vertalingen in het geheugen. Bovendien, als je Django's cacheframework of een reverse proxy zoals Nginx of Cloudflare gebruikt, zullen gecachete responses oude content serveren totdat ze verlopen.

Herstart je applicatieserver na het deployen van nieuwe vertalingen:

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

# Systemd
sudo systemctl restart myapp

# Docker
docker compose restart web

Voor Django's cacheframework, leeg de cache na het bijwerken van vertalingen:

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

Tijdens ontwikkeling herlaadt runserver automatisch wanneer Python-bestanden veranderen, maar het bewaakt geen .mo-bestanden. Je moet het handmatig herstarten na het uitvoeren van compilemessages.

8. Strings niet gewrapt in gettext (_() of {% trans %})

makemessages extraheert bepaalde strings niet, waardoor ze nooit in je .po-bestanden verschijnen en nooit worden vertaald. Dit is het meest basale probleem en ook het makkelijkst te missen in een grote codebase.

Django's makemessages-commando gebruikt xgettext om je broncode te scannen op vertaalmarkers. Als een string niet is gewrapt in gettext() (gewoonlijk als alias _()), gettext_lazy(), {% trans %} of {% blocktrans %}, is het onzichtbaar voor het extractieproces.

Wrap elke gebruikersgerichte string:

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

Gebruik de --dry-run-vlag van TranslateBot om te bekijken welke strings momenteel onvertaald zijn, zodat je strings kunt opvangen die gemist zijn tijdens extractie:

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

Dit toont alle onvertaalde vermeldingen in je .po-bestanden zonder API-aanroepen of wijzigingen te maken.

9. f-strings kunnen niet worden vertaald (Django-beperking)

Je wrapt een f-string in _() en krijgt ofwel een syntaxfout, of makemessages extraheert een kapotte/gedeeltelijke string die niet kan worden vertaald.

Python f-strings worden geEvalueerd tijdens runtime. De xgettext-extractietool parst broncode statisch, dus het kan geen Python-expressies evalueren binnen {}-accolades. Dit betekent dat _(f"Hello, {name}") wordt geExtraheerd als een string met een letterlijke {name}-expressie (of helemaal niet wordt geExtraheerd), en de resulterende .po-vermelding zal nooit overeenkomen met de runtime-string.

Gebruik in plaats daarvan Django's %-opmaak of .format() met benoemde placeholders:

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

Dit is geen beperking van TranslateBot of tooling. Het is fundamenteel aan hoe gettext werkt. De bronstring moet een statische letterlijke waarde zijn zodat deze kan worden geExtraheerd en opgezocht tijdens runtime.

TranslateBot behoudt al deze placeholder-formaten (%(name)s, {0}, %s, HTML-tags) tijdens vertaling, zodat de vertaalde strings volledig functioneel blijven.

10. Placeholder-formaatstringsfouten breken .po-compilatie

compilemessages faalt met een fout, of het .po-bestand heeft een #, python-format-vlagmismatch, en de vermelding wordt stilletjes verwijderd.

Wanneer een bronstring Python-formaatplaceholders bevat zoals %(name)s, markeert Django de .po-vermelding met #, python-format. Als de vertaling andere placeholders heeft (een typfout zoals %(nome)s, een ontbrekende placeholder, of een extra) kunnen de gettext-tools de vermelding afwijzen of kan compilemessages falen. Dit gebeurt vaak bij handmatige vertalingen of bij AI-vertaaltools die placeholder-semantiek niet begrijpen.

Een kapotte vermelding ziet er zo uit:

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

Hier zou %(naam)s eigenlijk %(name)s moeten zijn. Placeholders moeten exact overeenkomen met de bron.

Zorg ervoor dat vertaalde strings exact dezelfde placeholders bevatten als de bron. Controleer op typfouten, ontbrekende placeholders en extra placeholders.

Dit is een gebied waar TranslateBot een echt voordeel biedt. De placeholder-behoudlogica zorgt ervoor dat alle formaatstrings (%(name)s, {0}, %s) in de vertaalde output exact overeenkomen met de bron. De placeholder-afhandeling wordt gedekt door 100% testdekking, zodat formaatstringsfouten uit vertaling worden geElimineerd op toolniveau in plaats van te worden gevangen tijdens compilatie.

Als je handmatig vertaalt of met een tool die placeholders niet afhandelt, valideer dan je .po-bestanden met:

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

Dit voert de formaatstring-validatie van gettext uit en rapporteert eventuele mismatches.

Alles samenbrengen: een defensieve workflow

De meeste van deze problemen delen een gemeenschappelijke oorzaak: handmatige stappen die makkelijk te vergeten zijn. Hier is een workflow die alle 10 problemen voorkomt:

# 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

Voeg stap 4 toe aan je CI-pipeline en onvertaalde strings, fuzzy-vermeldingen en formaatfouten zullen de build laten falen voordat ze productie bereiken.

Snelreferentietabel

Oorzaak Symptoom Oplossing in een regel
Geen compilemessages Vertalingen bestaan maar verschijnen niet python manage.py compilemessages
Ontbrekende {% load i18n %} TemplateSyntaxError op {% trans %} Voeg {% load i18n %} toe aan de template
Ontbrekende LocaleMiddleware Taal is altijd standaard Engels Voeg django.middleware.locale.LocaleMiddleware toe aan MIDDLEWARE
Taalcode-mismatch Locale-directory niet gevonden Gebruik pt_BR (underscore) voor directories, pt-br (koppelteken) voor instellingen
Fuzzy-vermeldingen overgeslagen Vertaling in .po maar niet in de app Verwijder #, fuzzy-vlag na beoordeling
Verkeerde LOCALE_PATHS .po-bestanden bestaan maar Django negeert ze Stel LOCALE_PATHS in op een absoluut pad
Gecachete vertalingen Oude tekst verschijnt na update Herstart de applicatieserver
String niet in gettext String ontbreekt in .po-bestanden Wrap in _() of {% trans %}
f-string in gettext Kapotte extractie of runtime-mismatch Vervang door %- of .format()-placeholders
Placeholder-mismatch compilemessages faalt of vermelding verwijderd Stem placeholders exact af tussen bron en vertaling

De meeste van deze problemen verdwijnen wanneer je de vertaalstap automatiseert en controles afdwingt in CI. Het translate-commando van TranslateBot handelt placeholder-behoud en incrementele vertaling af, terwijl check_translations alles opvangt wat erdoorheen glipt (onvertaalde vermeldingen, fuzzy-vlaggen en formaatstring-problemen) voordat ze productie bereiken.

Stop met het handmatig bewerken van .po-bestanden

TranslateBot automatiseert Django-vertalingen met AI. Één commando, al uw talen, centen per vertaling.