Intermediate 15 min

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.

src/i18n.rs
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.rs
Compiled example

English, French, Arabic, plurals, and RTL

The Rust file below is imported verbatim by this page and compiled into the WebAssembly application running underneath it.

Open exact source
app_examples/docs-examples/src/examples/i18n.rs
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)
    }
}