Localization and RTL
Embed Fluent catalogs, negotiate locales, format plurals, and keep layout direction consistent.
Embed catalogs in every target
include_str! keeps the same resources available on desktop and WebAssembly. Catalog construction reports invalid Fluent syntax and duplicate identifiers before rendering begins.
let english = Catalog::parse(
langid!("en-US"),
include_str!("locales/en-US/main.ftl"),
)?;
let french = Catalog::parse(
langid!("fr"),
include_str!("locales/fr/main.ftl"),
)?;Pass typed variables
Use text for messages without variables and format with FluentArgs for names, numbers, and plurals. Missing messages, attributes, and invalid formatting return distinct errors.
Apply writing direction to the tree
Use Localizer::is_rtl for the direction scope and for collection widgets whose horizontal arrow behavior changes in RTL. This keeps copy, layout, overlays, and keyboard behavior aligned.
Reference files
These are the implementation and guide files used for this chapter.
docs/i18n.mdcrates/argui-i18n/src/lib.rscrates/argui-widget-gallery/src/pages/i18n.rsEnglish, French, Arabic, plurals, and RTL
The Rust file below is imported verbatim by this page and compiled into the WebAssembly application running underneath it.
use argui::{
i18n::{Catalog, FluentArgs, Localizer, langid},
runtime::{Context, Render},
text::TextStyle,
ui::{Element, FlexWrap, Sides, WritingDirection, percent},
widgets::{Button, default_theme},
};
const ENGLISH: &str = "hello = Hello, { $name }!\nitems = { $count ->\n [one] One message\n*[other] { $count } messages\n}";
const FRENCH: &str = "hello = Bonjour, { $name } !\nitems = { $count ->\n [one] Un message\n*[other] { $count } messages\n}";
const ARABIC: &str = "hello = مرحبًا، { $name }!\nitems = { $count ->\n [one] رسالة واحدة\n*[other] { $count } رسائل\n}";
pub struct Example {
localizer: Localizer,
count: i32,
}
impl Default for Example {
fn default() -> Self {
let catalogs = [
(langid!("en-US"), ENGLISH),
(langid!("fr"), FRENCH),
(langid!("ar"), ARABIC),
]
.map(|(locale, source)| Catalog::parse(locale, source).expect("valid embedded Fluent"));
Self {
localizer: Localizer::new(langid!("en-US"), catalogs).expect("English fallback exists"),
count: 1,
}
}
}
impl Render for Example {
fn render(&mut self, cx: &mut Context<Self>) -> Element {
let themes = default_theme(cx.environment());
let theme = themes.resolve(cx.environment().color_scheme);
let mut args = FluentArgs::new();
args.set("name", "Ada");
args.set("count", self.count);
let direction = if self.localizer.is_rtl() {
WritingDirection::Rtl
} else {
WritingDirection::Ltr
};
let localized_text = |value| {
Element::text(value).text_style(TextStyle {
color: theme.foreground,
..TextStyle::default()
})
};
let localized = Element::column([
localized_text(
self.localizer
.format("hello", &args)
.expect("known message"),
),
localized_text(self.localizer.format("items", &args).expect("known plural")),
])
.gap(8.0)
.direction_scope(direction);
Element::column([
Element::row([
Button::new("en", "English", theme.outline_button())
.on_click(cx.callback(|app| {
app.localizer.select([langid!("en-US")]);
}))
.build(),
Button::new("fr", "Français", theme.outline_button())
.on_click(cx.callback(|app| {
app.localizer.select([langid!("fr")]);
}))
.build(),
Button::new("ar", "العربية", theme.outline_button())
.on_click(cx.callback(|app| {
app.localizer.select([langid!("ar")]);
}))
.build(),
])
.gap(8.0)
.flex_wrap(FlexWrap::Wrap),
localized,
Button::new("more", "Add message", theme.button())
.on_click(cx.callback(|app| app.count += 1))
.build(),
])
.width(percent(1.0))
.height(percent(1.0))
.padding(Sides::length(28.0))
.gap(18.0)
.background(theme.background)
}
}
Starting this lesson’s WebAssembly module…