LANGUAGES & LOCALIZATION
Internationalization with glory-i18n.
Keep translations in Fluent resources and select a language explicitly for each application. glory-i18n connects immutable catalogs to static values, Cage sources, and Bond computations, while reactive messages follow their Owner's lifetime.
Enable the optional integration
Enable the facade's i18n feature and import glory::i18n. This guide pins the semantic implementation to a Git revision that includes the feature; an older crates.io release may not expose it. The combined server/browser example also selects ssr and hydrate; hydrate includes web. Use the locked wasm-bindgen CLI version when generating /app.js. Put the example modules below under src/examples/, declare them in mod.rs, and expose that module from your library. Compile i18n_server only for native targets and i18n_browser only for wasm32.
Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
glory = { git = "https://github.com/glory-rs/glory", rev = "0a46e2bd2f301807571b67f4a91776402eda8aeb", default-features = false, features = ["i18n", "ssr", "hydrate"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "=0.2.129"
web-sys = { version = "=0.3.106", features = ["Window", "Document", "Element", "Location"] }src/lib.rs
pub mod examples;src/examples/mod.rs
#[cfg(target_arch = "wasm32")]
pub mod i18n_browser;
pub mod i18n_catalog;
#[cfg(not(target_arch = "wasm32"))]
pub mod i18n_server;
pub mod i18n_shared;
pub mod sapid_action;
Write parameters and plural forms in Fluent
Store these two files in locales/ at your crate root. Message IDs and variable names stay the same across languages, while sentence order and plural branches may differ. These demo resources are separate from the website's own navigation and article translations.
locales/demo.en.ftl
greeting = Hello, { $name }!
inbox = { $count ->
[one] You have one message.
*[other] You have { $count } messages.
}
add = Add a message
locales/demo.zh-CN.ftl
greeting = 你好,{ $name }!
inbox = 你有 { $count } 条消息。
add = 增加一条消息
Pass count as a number rather than a preformatted String. Fluent selects English's one or other category using the resource locale; Simplified Chinese can use one pattern for every count. The starred other branch is the default. Test representative values such as 0, 1 and 2 instead of assuming every language has English's plural categories.
Parse a catalog and format parameters
Catalog::new parses owned resources once and rejects syntax errors and duplicate entries. Share the resulting Rc<Catalog> within your application instead of reparsing it during each reactive evaluation. The setup below deliberately fails early for invalid embedded resources. greeting demonstrates an ordinary strict lookup with a borrowed name parameter; it returns formatting errors to its caller.
src/examples/i18n_catalog.rs
use glory::i18n::{Args, Catalog, Error, Locale};
use std::rc::Rc;
pub fn catalog() -> Rc<Catalog> {
let catalog = Catalog::new(
"en",
[
("en", include_str!("../../locales/demo.en.ftl")),
("zh-CN", include_str!("../../locales/demo.zh-CN.ftl")),
],
)
.expect("valid embedded Fluent resources");
assert!(catalog.validate_complete().is_empty());
Rc::new(catalog)
}
pub fn greeting(catalog: &Catalog, locale: &Locale, name: &str) -> Result<String, Error> {
let mut args = Args::new();
args.set("name", name);
catalog.format_exact(locale, "greeting", Some(&args))
}
Create the catalog once, parse "en" or "zh-CN" as a glory::i18n::Locale, then call greeting(&catalog, &locale, "Ada"). Args::set accepts text and numeric values. Catalog::format_exact never falls back. I18n::format is an immediate read of its language source: a bare read while constructing a widget does not install a subscription.
Switch languages with an owned Cage
The shared widget below owns a language Cage and a message count. I18n::new also accepts a static Locale or a Bond<Locale> derived from application state. message explicitly creates a Bond<Result<String, Error>> under the supplied Owner and tracks reads from both the language and the argument closure. The label Bond below applies this demo's tested-resource error policy. Revise language in an event handler; the runtime schedules the affected bindings at the host's normal event/frame boundary.
src/examples/i18n_shared.rs
use super::i18n_catalog::catalog;
use glory::{
i18n::{Args, I18n, Locale},
prelude::*,
};
// Both the server and browser call this before constructing the widget.
pub fn locale_for_path(path: &str) -> Locale {
let path = path.split(['?', '#']).next().unwrap_or(path);
let tag = if path == "/zh" || path.starts_with("/zh/") { "zh-CN" } else { "en" };
tag.parse().expect("static language tag")
}
pub fn language_demo(initial: Locale) -> impl Widget {
widget(move |scope| {
let language = scope.cage(initial.clone());
let count = scope.cage(1_i32);
let i18n = I18n::new(catalog(), language);
let message = i18n
.message(scope.owner(), "inbox", move || {
let mut args = Args::new();
args.set("count", count.get());
args
})
.expect("live message owner");
let add = i18n.message(scope.owner(), "add", Args::new).expect("live message owner");
// The application chooses its error policy. These resources are tested.
let label = scope.bond(move || message.get().expect("valid inbox translation"));
let add_label = scope.bond(move || add.get().expect("valid action translation"));
col()
.class("i18n-demo")
.fill((
text().class("i18n-demo-message").content(label),
row().class("i18n-demo-actions").fill((
button()
.class("counter-button")
.label("English")
.on(activate, move |_| language.revise(|id| *id = "en".parse().unwrap())),
button().class("counter-button").label("简体中文").on(activate, move |_| {
language.revise(|id| *id = "zh-CN".parse().unwrap())
}),
button()
.class("counter-button counter-primary")
.label(add_label)
.on(activate, move |_| count.revise(|count| *count += 1)),
)),
))
.show_in(scope)
})
}
Allocate messages during construction
Create the context and message Bonds once in the widget's construction scope, outside pure derived computations. Argument closures return Args with owned or static values and may read Cages, but must not write state or allocate new reactive nodes. Disposing the Owner invalidates these messages and releases their captures. Cloning I18n shares resources and language handles; it does not retain an Owner or create observers.
Try language switching and plurals
Start at one message, add another, and switch between English and Simplified Chinese. The local language buttons preserve the count. This website's header selector instead changes the localized URL and remounts the current page; keep business state above that route boundary if it must survive page changes.
Choose the language before server rendering
Use the same locale_for_path policy on both targets: /zh and /zh/... select zh-CN; other paths select en. Each request constructs a fresh widget and state. The catalog may be shared, but the active language must remain request/app local. snapshot serializes the versioned tree and its translated initial values. Insert Snapshot::html verbatim into the exclusive mount, add BASE_CSS, and set the outer HTML language. This minimal host example only serves the demo; a full site should also localize its title, description, canonical URL and language alternates.
src/examples/i18n_server.rs
use super::i18n_shared::{language_demo, locale_for_path};
pub fn render_page(path: &str) -> Result<String, Box<dyn std::error::Error>> {
let initial = locale_for_path(path);
let snapshot = glory::ssr::snapshot(language_demo(initial.clone()))?;
Ok(format!(
r#"<!doctype html>
<html lang="{initial}"><head><meta charset="utf-8">
<title>Glory i18n demo</title>
<style data-gly-base="v1">{}</style>
</head><body><main id="app">{}</main>
<script type="module">import init from '/app.js'; await init();</script>
</body></html>"#,
glory::html::BASE_CSS,
snapshot.html
))
}
Hydrate with the same initial language
The browser reads the URL before constructing its widget, uses the same resources and initial count of 1, then calls hydrate_with. Call start_demo from your own wasm-bindgen start entry and retain the WebApp guard, as shown here. Serve the generated JavaScript as /app.js beside its WASM file; the server must return render_page for the requested localized route. The code blocks shown here are included from the same source files compiled and tested by this website.
src/examples/i18n_browser.rs
use super::i18n_shared::{language_demo, locale_for_path};
use std::cell::RefCell;
use wasm_bindgen::JsValue;
thread_local! {
static APP: RefCell<Option<glory::web::WebApp>> = const { RefCell::new(None) };
}
// Call this from your app's #[wasm_bindgen(start)] entry.
pub fn start_demo() -> Result<(), JsValue> {
let window = web_sys::window().ok_or_else(|| JsValue::from_str("Missing window"))?;
let document = window.document().ok_or_else(|| JsValue::from_str("Missing document"))?;
let root =
document.get_element_by_id("app").ok_or_else(|| JsValue::from_str("Missing mount"))?;
// Use the same URL policy before hydration; do not override it with navigator.language.
let initial = locale_for_path(&window.location().pathname()?);
let app = glory::web::hydrate_with(root, move |_| language_demo(initial))
.map_err(|error| JsValue::from_str(&error.to_string()))?;
APP.with(|slot| *slot.borrow_mut() = Some(app));
Ok(())
}
Match the first tree exactly
Do not replace a Chinese URL's initial locale with navigator.language or a stored preference before adoption. The server and browser must agree on resources, locale, arguments, structure and initial state. Plain ssr::render output is not a hydration Snapshot. Apply a preference only through deliberate navigation or a state change after successful hydration.
Choose fallback and validation policies
Catalog::format falls back only when the requested language, message or attribute is missing; resolver errors such as absent arguments, unresolved references and cycles remain errors. I18n::format and message use that fallback policy. Use format_exact for a published translation that must be complete, and handle the Error result according to your application's needs.
validate_complete compares message and term values, attributes, and directly referenced variable names against the fallback. It allows different plural categories and word order. This schema check does not prove every branch resolves: exercise parameterized messages and test your translated SSR/client paths as well.
Fluent's bidirectional isolation is enabled, so arguments may be surrounded by invisible isolation characters. Preserve them in displayed output. Treat translations as text rather than trusted HTML; use ordinary semantic widgets for links and rich structure.
glory-i18n does not discover browser preferences, persist a language, route URLs, or update document metadata. Those are application policies. This website uses English URLs and /zh/ URLs, preserves the chapter, query and fragment in its language selector, and emits canonical URLs, hreflang alternates and a bilingual sitemap.
Read the complete SSR and hydration guide →