Aller au contenu

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:

STATUS_CHOICES = [
    (STATUS_PENDING, _('En attente')),  # DON'T DO THIS!
]

Correct:

STATUS_CHOICES = [
    (STATUS_PENDING, _('Pending')),  # English in code
]

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:

msg = _("User") + " " + username + " " + _("created")

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:

docker compose exec web python manage.py makemessages -l fr --no-location

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:

msgid "Draft"
msgstr "Brouillon"

msgid "Sent"
msgstr "Envoyé"

msgid "Paid"
msgstr "Payé"

Step 4: Compile Messages

After adding translations, compile them:

docker compose exec web python manage.py compilemessages

This creates binary .mo files that Django uses at runtime.

Step 5: Restart Services

After compiling messages, restart the application:

docker compose restart web celery

Common Mistakes to Avoid

1. ❌ Translating in Python Code

# WRONG!
STATUS_CHOICES = [
    ('pending', _('En attente')),
]

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

<!-- WRONG! -->
<h1>Bienvenue</h1>
<!-- CORRECT! -->
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>

5. ❌ Not Wrapping Error Messages

# WRONG!
raise ValueError("Invalid date format")
# 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:

  1. Make sure LANGUAGE_CODE = 'fr' in settings
  2. Restart the server
  3. Check the UI - it should display French text

Resources

Checklist for Developers

Before committing code, verify:

  • [ ] All user-facing strings use _() or gettext_lazy()
  • [ ] All strings are in English in the code
  • [ ] Ran makemessages -l fr to update translation files
  • [ ] Added French translations to django.po
  • [ ] Ran compilemessages to 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!