Internationalization and localization
Framework-owned Translator, catalogues, locale negotiation, and ICU format helpers.
Apps keep entity / catalog multilang in their own persistence. Message catalogues are for UI chrome, emails, and flash-style strings - not a substitute for *_lang product tables.
serenade-translation (#132, intl deepen #280)
| Piece | Role |
|---|---|
Translator / TranslatorInterface |
trans / trans_choice, locale + fallbacks |
MessageCatalogue |
Domain → id → template for one locale |
CatalogueLoader |
Pluggable parsers (TomlCatalogueLoader, JsonCatalogueLoader) |
| File naming | {domain}.{locale}.{toml|json} (e.g. messages.fr.toml); +intl-icu domain suffix allowed |
LocaleNegotiator / LocaleMiddleware |
Query → cookie → Accept-Language → default; stores _locale |
format_number / format_number_f64 / format_currency / format_date |
ICU4X helpers (Cargo feature icu, default on) |
language_display_name / region_display_name |
Localized language and region labels (ICU) |
is_currency_code / normalize_currency_code |
ISO 4217 alphabetic code shape helpers |
TranslationBundle / TranslationExtension |
DI: service id translator (Arc<Translator>) |
There is no separate serenade-intl crate. Currency patterns, select formatting, and display names live in serenade-translation.
Catalogue format (TOML first)
Section titled “Catalogue format (TOML first)”tagline = "Open public wall"likes = "{count, plural, one {# like} other {# likes}}"greeting = "Hello {name}"welcome = "{gender, select, female {Welcome, madam} male {Welcome, sir} other {Welcome}}"JSON is supported the same way (messages.en.json). Implement CatalogueLoader for XLIFF, Fluent, Gettext, etc.
Message format
Section titled “Message format”- Placeholders:
{name} - ICU plural subset:
{count, plural, one {# item} other {# items}}(and=0 {…}exact arms) - ICU select subset (including gender):
{gender, select, female {She} male {He} other {They}} - Select keys match the argument value case-insensitively; unmatched values use
otherwhen present - Missing ids return the id string (Symfony habit)
Numbers and currency
Section titled “Numbers and currency”With feature icu (default):
format_number/format_number_f64use ICU4X decimal patterns (locale grouping and separators)format_currency(amount, "EUR", &locale)uses ICU4X currency patterns when the locale and ISO 4217 code are usable; otherwise falls back to{number} {CODE}is_currency_code/normalize_currency_codevalidate and uppercase three-letter alphabetic codes (shape only, not the ISO registry)language_display_name("fr", &en)/region_display_name("US", &en)return CLDR display names when data is present
Without icu, numbers and currency fall back to plain ASCII digits and {number} {CODE}; display-name helpers return None.
Locale negotiation
Section titled “Locale negotiation”Priority:
- Existing
_localerequest attribute - Query
_locale(requires adapter query string; Actix copies it) - Cookie
_locale Accept-Languageagainst the allowed list- Default locale
LocaleMiddleware applies this on sync HttpKernel stacks. Async apps (e.g. MyFeed) call LocaleNegotiator::apply at the start of the handler.
config/packages/translation.toml:
default_locale = "en"fallbacks = ["en"]paths = ["translations"]Register TranslationExtension with build_container. Resolve translator as Arc<Translator>.
MyFeed dogfood
Section titled “MyFeed dogfood”examples/demo-html loads translations/messages.{en,fr}.toml, exposes /locale/{code} cookie switch, and translates chrome/admin strings. The feed footer shows a locale-aware demo price via format_currency. Post bodies stay in the author language.
