Skip to content

Backend i18n

Django gettext for translation markers, Babel for locale-aware formatting, and Jinja2 filters for templates.

Translation Markers

gettext_lazy — Models, Choices, Forms

Use deferred evaluation for strings that are evaluated at import time:

from django.utils.translation import gettext_lazy as _

class LoanStatus(models.TextChoices):
    ACTIVE = "active", _("Active")
    CLOSED = "closed", _("Closed")

class Loan(BaseModel):
    class Meta:
        verbose_name = _("loan")
        verbose_name_plural = _("loans")

gettext — Services, Tasks, Runtime Errors

Use immediate evaluation for strings evaluated at runtime:

from django.utils.translation import gettext as _

raise DoNotContactError(_("Borrower has do_not_contact flag set."))

raise DoNotContactError(
    _("Borrower has active do-not-interact rule for channel '%(channel)s'.")
    % {"channel": channel}
)

Never use f-strings inside _()

The message extractor cannot parse f-strings. Always use % named substitution:

# WRONG — extractor can't parse this
raise ValueError(_(f"Loan {loan.loan_number} is invalid"))

# CORRECT
raise ValueError(
    _("Loan %(loan_number)s is invalid") % {"loan_number": loan.loan_number}
)

What NOT to Mark

Category Reason
GL entry descriptions Audit trail — must stay in English
Log messages (logger.info/warning/error) For developers, not users
Webhook payloads and external API data Consumed by machines
Internal identifiers and code-level strings Not user-facing

Model Conventions

  • verbose_name should be lowercase (Django capitalizes automatically)
  • Abstract base models (mixins) do not get verbose_name
  • Use gettext_lazy for all help_text on model fields

Message Catalogs

File Locations

backend/locale/
├── en/
│   └── LC_MESSAGES/
│       ├── django.po    # Source catalog (English)
│       └── django.mo    # Compiled catalog
└── es/
    └── LC_MESSAGES/
        ├── django.po    # Source catalog (Spanish)
        └── django.mo    # Compiled catalog

Workflow

# Extract translatable strings from source code
uv run python manage.py makemessages -l en -l es --no-wrap

# After translating .po files, compile to .mo
uv run python manage.py compilemessages

Both .po and .mo files are committed to the repository.

Babel Formatters

common/formatting.py provides locale-aware formatting for server-rendered content (PDFs, emails, compliance documents):

Function Input Output (locale=en) Output (locale=es)
fmt_currency(1234.56, "USD") Decimal $1,234.56 1.234,56 US$
fmt_date(date(2025,1,15)) date Jan 15, 2025 15 ene 2025
fmt_datetime(dt) datetime Jan 15, 2025, 2:30:00 PM 15 ene 2025, 14:30:00
fmt_number(1234.56) Decimal 1,234.56 1.234,56
fmt_percent(0.065) Decimal 6.5% 6,5%

All functions accept a locale: str = "en" parameter. They are pure functions with no Django dependency.

Jinja2 Template Filters

Formatting functions are registered as Jinja2 filters on two template environments:

PDF Templates (common/pdf.py)

_env.filters["currency"] = lambda v, locale="en", currency="USD": fmt_currency(v, currency=currency, locale=locale)
_env.filters["date"] = lambda v, locale="en": fmt_date(v, locale=locale)
_env.filters["number"] = lambda v, locale="en": fmt_number(v, locale=locale)
_env.filters["percent"] = lambda v, locale="en": fmt_percent(v, locale=locale)

Communication Templates (apps/communications/services.py)

def _jinja2_env(*, locale: str = "en", currency: str = "USD") -> jinja2.Environment:
    env = jinja2.Environment(autoescape=True)
    env.filters["currency"] = lambda v, loc=locale: fmt_currency(v, currency=currency, locale=loc)
    env.filters["date"] = lambda v, loc=locale: fmt_date(v, locale=loc)
    env.filters["number"] = lambda v, loc=locale: fmt_number(v, locale=loc)
    env.filters["percent"] = lambda v, loc=locale: fmt_percent(v, locale=loc)
    return env

Usage in Templates

Dear {{ borrower.first_name }},

Your payment of {{ amount | currency }} is due on {{ due_date | date }}.
Your current interest rate is {{ rate | percent }}.

The locale parameter is passed through from the borrower's language preference.

See Also