Localization¶
Per-tenant settings for locale, timezone, and currency. Server-side formatting for documents and communications.
Per-Tenant Settings¶
Each tenant configures its localization preferences:
| Field | Type | Default | Example |
|---|---|---|---|
locale |
SupportedLocale |
en |
es |
timezone |
TimeZoneField |
UTC |
America/New_York |
default_currency |
SupportedCurrency |
USD |
MXN |
TenantLocaleMiddleware¶
Placed after TenantStatusMiddleware, this middleware activates locale settings per-request.
Language Resolution Priority¶
Accept-Languageheader — parsed for quality scores (e.g.,es;q=0.9), matched againstLANGUAGES- Tenant locale — the tenant's configured default language
LANGUAGE_CODE— Django setting fallback (en-us)
The middleware:
- Attempts exact language match, then base language (e.g.,
es-mx→es) - Activates
translation.activate()andtimezone.activate() - Sets request attributes:
request.locale,request.tenant_timezone,request.tenant_currency - Sets the
Content-Languageresponse header - Deactivates locale and timezone after response
Timezone Handling¶
- Accepts both
zoneinfo.ZoneInfoobjects and IANA timezone strings - Converts strings to
ZoneInfoobjects internally - Defaults to
UTCon any error
Server-Side Formatting¶
Server-side formatting applies only to rendered documents — PDFs, emails, and compliance reports. API responses always return raw values.
When Formatting Applies¶
| Output | Formatted? | Technology |
|---|---|---|
| API JSON responses | No — raw Decimal, ISO dates | Frontend formats via Intl |
| PDF documents | Yes | Babel + Jinja2 filters |
| Email/SMS templates | Yes | Babel + Jinja2 filters |
| Compliance reports | Yes | Babel formatters |
Babel Formatters¶
common/formatting.py wraps Babel for locale-aware formatting:
from common.formatting import fmt_currency, fmt_date
# English
fmt_currency(Decimal("1234.56"), "USD", locale="en") # → "$1,234.56"
# Spanish
fmt_currency(Decimal("1234.56"), "USD", locale="es") # → "1.234,56 US$"
Communication Template Resolution¶
When sending a communication, the system resolves the template language:
async def resolve_template_for_language(
*,
template_type: str,
channel: str,
program_id: UUID | None,
language: str,
) -> CommunicationTemplate | None:
Resolution order:
- Find template matching
(template_type, channel, program_id, language) - If not found and
language != "en", fall back to English - Return
Noneif no template exists
Template Language Field¶
CommunicationTemplate has a language field with a unique constraint:
UniqueConstraint(
fields=["template_type", "channel", "program", "language"],
name="unique_template_per_language",
)
This ensures one template per type/channel/program/language combination.
Sending with Language Preference¶
send_communication() reads borrower.language_preference to select the template:
borrower_lang = getattr(borrower, "language_preference", "en") or "en"
rendered = await render_template(
template_id=template_id,
context={"borrower": borrower},
locale=borrower_lang,
)
PDF Template Resolution¶
render_pdf() tries language-specific template variants:
render_pdf("monthly_statement.html", context, language="es")
# Tries: monthly_statement_es.html → monthly_statement.html
Existing Template Variants¶
| Template | English | Spanish |
|---|---|---|
| Monthly statement | monthly_statement.html |
monthly_statement_es.html |
| TILA disclosure | tila_disclosure.html |
tila_disclosure_es.html |
| Adverse action notice | adverse_action_notice.html |
adverse_action_notice_es.html |
Template variants follow the naming convention {stem}_{language_code}{suffix}.
Language Source¶
PDF generation reads from borrower.language_preference:
language = getattr(borrower, "language_preference", "en") or "en"
pdf_bytes = render_pdf("monthly_statement.html", context, language=language)
See Also¶
- Overview --- i18n architecture
- Backend i18n --- Translation markers, Babel formatters
- Communications --- Communication template system
- Documents --- PDF generation