Skip to content

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.

translations/messages.en.toml
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.

  • 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 other when present
  • Missing ids return the id string (Symfony habit)

With feature icu (default):

  • format_number / format_number_f64 use 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_code validate 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.

Priority:

  1. Existing _locale request attribute
  2. Query _locale (requires adapter query string; Actix copies it)
  3. Cookie _locale
  4. Accept-Language against the allowed list
  5. 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>.

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.