Internationalization (i18n)
Djangors provides runtime internationalization features including catalog loading, fallback resolution, request locale parsing middleware, locale-aware date/time formatting, and template translation integration.
Fluent Catalogs
Translations in Djangors are written using Mozilla Fluent (.ftl) file syntax. Fluent provides a highly expressive syntax that handles gender, plurals, and variables naturally.
Example messages.ftl (English):
welcome = Welcome to our site!
login-failed = Incorrect username or password.
In Rust, you load catalogs using the Locales container:
#![allow(unused)]
fn main() {
use djangors::i18n::Locales;
let mut locales = Locales::new("en-US"); // Define the default fallback locale
// Add language catalogs
locales.add_locale("en-US", include_str!("locales/en-US.ftl"))?;
locales.add_locale("fr-FR", include_str!("locales/fr-FR.ftl"))?;
}
The Catalog Building Block
Underneath Locales sits the lower-level Catalog
type: a single parsed Fluent bundle for one locale. Locales::add_locale is just
Catalog::from_ftl + insertion into a map; the whole fallback chain (requested locale → default
locale → raw message ID) lives in Locales::translate, not in Catalog.
| Method | Signature | Notes |
|---|---|---|
Catalog::from_ftl(locale, src) | fn(&str, &str) -> Result<Self, I18nError> | Parses an .ftl source for locale (InvalidLocale/Fluent/Resource errors) |
catalog.get(id, args) | fn(&self, &str, Option<&FluentArgs<'_>>) -> Option<String> | Formats one message; returns None when the ID is missing — no fallback |
Use Catalog directly when you need a single pre-parsed locale without any fallback behaviour:
#![allow(unused)]
fn main() {
use djangors::i18n::Catalog;
use fluent_bundle::FluentArgs;
let catalog = Catalog::from_ftl("en-US", "welcome = Hello { $name }, welcome!").unwrap();
let mut args = FluentArgs::new();
args.set("name", "Ada");
assert_eq!(
catalog.get("welcome", Some(&args)),
Some("Hello Ada, welcome!".to_string())
);
// Missing IDs are None here — the fallback chain is Locales' job.
assert_eq!(catalog.get("no-such-message", None), None);
}
When you need the fallback chain, wrap catalogs in Locales as shown above and call
locales.translate(...) instead.
Translation Fallback Chain
When translating a message ID using locales.translate("locale_tag", "message_id", args), Djangors evaluates a strict fallback chain to prevent rendering errors:
- Requested Locale: Looks up the translation in the specified catalog (e.g.
fr-FR). - Default Locale: If the translation is missing in the requested catalog, it falls back to the configured default locale catalog (e.g.
en-US). - Raw message ID: If the key is not found in either catalog, it returns the raw
message_idstring itself (e.g."welcome") rather than panicking or failing.
Request Locale Resolution
To automatically determine the locale per HTTP request, Djangors provides LocaleLayer middleware.
#![allow(unused)]
fn main() {
use djangors::i18n::LocaleLayer;
// Add this to your application's Tower middleware stack:
let app = app.layer(LocaleLayer::new("en-US"));
}
Parsing Order
LocaleLayer resolves the locale tag for each request in the following order:
- HTTP
Accept-LanguageHeader: Parses the first valid BCP-47 tag (usingfirst_locale_tag). E.g.,accept-language: fr-FR, en;q=0.8resolves tofr-FR. - Session override: Looks up the key
_localein the user’s active session. - Default: Falls back to the configured default locale tag (e.g.
en-US).
Once resolved, the middleware inserts a ResolvedLocale struct containing the locale string into the request’s extension map.
Locale-Aware Formatting
Djangors provides helpers to format dates and times based on the active locale:
localized_date(date, locale): Formats achrono::NaiveDate.localized_datetime(datetime, locale): Formats achrono::DateTime<Utc>.
#![allow(unused)]
fn main() {
use djangors::i18n::localized_date;
use chrono::NaiveDate;
let date = NaiveDate::from_ymd_opt(2026, 7, 30).unwrap();
// US formats: Month/Day/Year
assert_eq!(localized_date(date, "en-US"), "07/30/2026");
// UK/Europe formats: Day/Month/Year
assert_eq!(localized_date(date, "en-GB"), "30/07/2026");
// Fallback format: Year-Month-Day
assert_eq!(localized_date(date, "zh-CN"), "2026-07-30");
}
Template Integration
Template filters in Minijinja do not have direct access to the HTTP request context. To support translations in templates, you must pass the resolved locale and an opaque LocalesValue object wrapper in the template rendering context.
Wiring it up in Rust
#![allow(unused)]
fn main() {
use djangors::i18n::{locales_value, trans};
use minijinja::Environment;
use std::sync::Arc;
// 1. Register the "trans" filter in your Minijinja environment
let mut env = Environment::new();
env.add_filter("trans", trans);
// 2. Add the catalogs wrapper and the active locale to the context
let ctx = minijinja::context! {
locales => locales_value(Arc::new(locales_store)),
locale => resolved_locale_string,
};
}
Rendering in Templates
Use the trans filter to resolve translated strings inside your HTML templates:
<!-- Translates the "welcome" message ID -->
<h1>{{ "welcome" | trans(locales, locale) }}</h1>