Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

MethodSignatureNotes
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:

  1. Requested Locale: Looks up the translation in the specified catalog (e.g. fr-FR).
  2. Default Locale: If the translation is missing in the requested catalog, it falls back to the configured default locale catalog (e.g. en-US).
  3. Raw message ID: If the key is not found in either catalog, it returns the raw message_id string 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:

  1. HTTP Accept-Language Header: Parses the first valid BCP-47 tag (using first_locale_tag). E.g., accept-language: fr-FR, en;q=0.8 resolves to fr-FR.
  2. Session override: Looks up the key _locale in the user’s active session.
  3. 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 a chrono::NaiveDate.
  • localized_datetime(datetime, locale): Formats a chrono::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>