Skip to main content

Module i18n

Module i18n 

Source
Expand description

I18n capability trait. See plan/ecosystem/02-capabilities.md §I18n.

An I18nPlugin turns a translation key plus a locale plus a bag of variables into a localized string. The trait is shaped so that the same surface covers three very different backends: static JSON files (@bext/i18n-static), Mozilla Fluent (@bext/i18n-fluent), and ICU MessageFormat (@bext/i18n-icu). A project swaps between them in bext.config.toml without touching code.

§Design notes

  • Sync, object-safe. Like every other capability in this crate, I18nPlugin is Send + Sync, has no generics on the trait, and is callable from WASM, QuickJS, and native host code alike.

  • Owned String return, not Cow<'_, str>. The plan sketch uses Cow<'_, str>, which would let a backend return a borrowed slice of a statically-interned key when no interpolation happened. That borrow would have to live as long as the backend, which forces either a lifetime on the trait or an internal self-referential mess across the FFI boundary. Owning the string costs one allocation per call and makes every other concern (WASM guests, host-fn marshalling, async bridging, handle storage) trivial.

  • Missing keys never error. translate returns Result<String, I18nError>, but the Ok value is a best-effort rendering — if the key is unknown, the backend returns Ok("{key}") (or whatever its configured missing-key policy is). A missing translation should never take down a UI. Errors are reserved for genuine backend failures: a locale that was promised in supported_locales() but failed to load, a Fluent syntax error in a bundle, an ICU formatter that could not format a variable. The caller branches on Err to decide whether to retry, fall back to a hardcoded English string, or surface a 500.

  • TransVars is a flat HashMap<String, I18nValue>. Passing &HashMap<String, String> would be ABI-flat but would force backends to re-parse numeric and date arguments from strings, which is exactly the kind of bug that ICU MessageFormat exists to prevent. Instead we carry a tagged enum of the small set of variable types every major i18n library supports: string, i64, f64, bool. Plurals are driven by i64 and f64 naturally; bool covers Fluent’s male/female/other selector shape.

  • supported_locales returns owned strings. A borrowed &[String] would be fine today but would bind the slice to the lifetime of the plugin, which crosses badly through dyn-trait host-fn dispatch. Owned Vec<String> is one allocation on what is an infrequently-called, boot-time query.

  • fallback_locale is a single-locale answer, not a chain. BCP 47 negotiation (asking for fr-CA and getting fr) is the caller’s job — specifically, it belongs in the I18nPlugin::negotiate helper, which takes a requested locale and walks backward to the broadest tag the plugin supports, finally falling back to fallback_locale. Backends do not need to implement negotiation themselves; they only declare what they support.

Enums§

I18nError
Why a translate call failed. Ok(rendered) is the overwhelming majority path — every method here is reserved for a real backend failure the caller needs to decide about.
I18nValue
A variable passed to translate for interpolation.

Traits§

I18nPlugin
A plugin that translates keys into localized strings.

Type Aliases§

TransVars
A bag of variables passed to translate.