Django Internationalization (i18n) Guidelines¶
Overview¶
This project uses Django's built-in internationalization system to support multiple languages (primarily French). All user-facing text must be translatable.
Current Languages¶
- Source Language: English (en)
- Target Language: French (fr)
Core Principles¶
1. NEVER Hardcode Text in French¶
❌ Wrong:
✅ Correct:
The French translation will be in the .po file, NOT in the Python code.
2. Always Use Translation Functions¶
For all user-facing strings, use Django's translation functions:
In Python Code¶
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _lazy
# For strings that are evaluated immediately
message = _("User created successfully")
# For strings that are evaluated later (model fields, form labels)
class MyModel(models.Model):
name = models.CharField(_("Name"), max_length=100)
# For model choices
STATUS_CHOICES = [
('pending', _('Pending')),
('completed', _('Completed')),
]
In Templates¶
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>
<p>{% blocktrans %}Hello {{ username }}{% endblocktrans %}</p>
3. String Guidelines¶
- Write all source strings in English
- Use complete sentences when possible
- Be descriptive - "Failed" is better than "Error"
- Keep strings short but meaningful
- Avoid concatenation - use format strings instead
❌ Wrong:
✅ Correct:
from django.utils.translation import gettext as _
msg = _("User %(username)s created") % {'username': username}
# Or using format strings
msg = _("User {username} created").format(username=username)
Workflow for Adding Translatable Strings¶
Step 1: Write Code in English¶
# apps/myapp/models.py
from django.utils.translation import gettext_lazy as _
class Invoice(models.Model):
STATUS_DRAFT = 'draft'
STATUS_SENT = 'sent'
STATUS_PAID = 'paid'
STATUS_CHOICES = [
(STATUS_DRAFT, _('Draft')),
(STATUS_SENT, _('Sent')),
(STATUS_PAID, _('Paid')),
]
status = models.CharField(
_('status'),
max_length=20,
choices=STATUS_CHOICES,
default=STATUS_DRAFT
)
Step 2: Generate Translation Files¶
After adding new translatable strings, run:
This command:
- Scans all Python and template files
- Finds all strings wrapped in _(), gettext(), {% trans %}, etc.
- Updates locale/fr/LC_MESSAGES/django.po
Step 3: Add French Translations¶
Edit locale/fr/LC_MESSAGES/django.po and add translations:
Step 4: Compile Messages¶
After adding translations, compile them:
This creates binary .mo files that Django uses at runtime.
Step 5: Restart Services¶
After compiling messages, restart the application:
Common Mistakes to Avoid¶
1. ❌ Translating in Python Code¶
2. ❌ Forgetting to Import Translation Functions¶
# WRONG!
class MyModel(models.Model):
name = models.CharField("Name", max_length=100) # Not translatable
# CORRECT!
from django.utils.translation import gettext_lazy as _
class MyModel(models.Model):
name = models.CharField(_("Name"), max_length=100)
3. ❌ Using gettext() Instead of gettext_lazy() in Models¶
# WRONG! Will be evaluated at import time
from django.utils.translation import gettext as _
class MyModel(models.Model):
STATUS_CHOICES = [
('active', _('Active')), # Evaluated too early!
]
# CORRECT! Will be evaluated when needed
from django.utils.translation import gettext_lazy as _
class MyModel(models.Model):
STATUS_CHOICES = [
('active', _('Active')), # Evaluated lazily
]
4. ❌ Hardcoding Text in Templates¶
5. ❌ Not Wrapping Error Messages¶
# CORRECT!
from django.utils.translation import gettext as _
raise ValueError(_("Invalid date format"))
File Locations¶
- Translation files:
locale/fr/LC_MESSAGES/django.po - Compiled files:
locale/fr/LC_MESSAGES/django.mo - App-specific translations:
apps/myapp/locale/fr/LC_MESSAGES/django.po
Quick Reference¶
| Use Case | Import | Usage |
|---|---|---|
| Model fields, choices | from django.utils.translation import gettext_lazy as _ |
_("Text") |
| View messages | from django.utils.translation import gettext as _ |
_("Text") |
| Templates | {% load i18n %} |
{% trans "Text" %} |
| Templates with variables | {% load i18n %} |
{% blocktrans %}...{% endblocktrans %} |
Testing Translations¶
To test if your translations are working:
- Make sure
LANGUAGE_CODE = 'fr'in settings - Restart the server
- Check the UI - it should display French text
Resources¶
Checklist for Developers¶
Before committing code, verify:
- [ ] All user-facing strings use
_()orgettext_lazy() - [ ] All strings are in English in the code
- [ ] Ran
makemessages -l frto update translation files - [ ] Added French translations to
django.po - [ ] Ran
compilemessagesto compile translations - [ ] Restarted services (
docker compose restart web celery) - [ ] Tested the UI in French
Pre-commit Hook (Optional)¶
To automatically check for hardcoded French strings, you can add a pre-commit hook:
#!/bin/bash
# .git/hooks/pre-commit
# Check for common French words in Python files (excluding locale/)
if git diff --cached --name-only | grep -E '\.py$' | xargs grep -l "_(\".*[àâäéèêëïîôùûüÿæœç].*\")" | grep -v locale/; then
echo "Error: Found hardcoded French strings in Python files!"
echo "Please use English strings in code and add translations to locale/fr/LC_MESSAGES/django.po"
exit 1
fi
Remember: Code in English, translate in .po files!