Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Internationalization

In this chapter, you will:

  • Set up locale identifiers and understand fallback chains
  • Write TOML translation files with plural support
  • Use the text! macro for localized, reactive text views
  • Format dates, numbers, units, and lists according to locale
  • Switch locales at runtime with instant UI updates

Your app has users worldwide – it is time to make it speak their language. Getting internationalization right means more than translating strings. Different languages have different plural rules (“1 apple” vs. “2 apples” vs. Russian’s four forms), different date formats, and different list conventions. WaterUI’s dedicated waterui-locale crate handles all of this, integrated with the reactive system so the UI updates instantly when the user changes their language.

Overview

The i18n system is built on several pillars:

  • ICU4X for locale identifiers, plural rules, and list formatting.
  • TOML translation files for storing localized strings.
  • The text! macro for embedding localizable text in views.
  • Reactive locale tracking via nami::Binding<Locale> so views re-render when the locale changes.

The Locale Type

waterui_locale::Locale wraps an ICU4X icu_locid::Locale. It preserves Unicode extension data (calendar, hour cycle, number system), making it suitable for complete locale preferences rather than just language tags.

use waterui_locale::Locale;
use core::str::FromStr;

let locale = Locale::from_str("en-US").unwrap();
let tag = locale.canonical_tag(); // "en-US"
let lang = locale.language.as_str(); // "en"

Built-in Locale Constants

The locales module provides pre-defined constants so you do not have to parse strings at runtime:

use waterui_locale::locales;

let _ = locales::EN;        // English
let _ = locales::EN_US;     // English (United States)
let _ = locales::EN_GB;     // English (United Kingdom)
let _ = locales::ZH_CN;     // Chinese Simplified (China)
let _ = locales::ZH_TW;     // Chinese Traditional (Taiwan)
let _ = locales::ZH_HK;     // Chinese Traditional (Hong Kong)
let _ = locales::ZH_HANS;   // Chinese Simplified (script)
let _ = locales::ZH_HANT;   // Chinese Traditional (script)
let _ = locales::JA;        // Japanese
let _ = locales::KO;        // Korean
let _ = locales::FR;        // French
let _ = locales::DE;        // German
let _ = locales::ES;        // Spanish
let _ = locales::RU;        // Russian
let _ = locales::AR;        // Arabic
let _ = locales::HI;        // Hindi
let _ = locales::PT;        // Portuguese
let _ = locales::PT_BR;     // Portuguese (Brazil)
let _ = locales::PT_PT;     // Portuguese (Portugal)
let _ = locales::SR_LATN;   // Serbian (Latin)
let _ = locales::SR_CYRL;   // Serbian (Cyrillic)

Locale Fallback Chain

When a translation is not available in the user’s exact locale, WaterUI needs to know where to look next. ICU4X’s LocaleFallbacker provides script-aware fallback that handles tricky cases correctly:

use waterui_locale::locale::{get_fallback_chain, Locale};
use core::str::FromStr;

let locale = Locale::from_str("zh-TW").unwrap();
let chain = get_fallback_chain(&locale);
// zh-TW -> zh-Hant -> zh (NOT zh-Hans!)

Note: The fallback chain correctly distinguishes Traditional from Simplified Chinese, Latin from Cyrillic Serbian, and other script-variant pairs. This is critical for delivering the right translations.

Translation Files

Translations are stored in TOML files, parsed by waterui_locale::parser::TranslationFile. Let’s start with the simplest case and build up to complex plural forms.

Simple Translations

"Hello, World!" = "Hello, World!"
"Goodbye" = "Farewell"

The key is the source string (typically the English text used in code). The value is the translated string.

Plural Translations

English has two plural forms (“1 apple” vs “2 apples”), but many languages have more. The {#variable} syntax in the key marks a plural source:

"I have {#count} apple" = {
    one = "I have {count} apple",
    other = "I have {count} apples"
}

Note the difference: the key uses {#count} (with #) to identify the plural source. The values use {count} (without #) for simple interpolation.

Available Plural Forms

The plural form keys follow CLDR categories:

KeyUsed By
zeroArabic, Welsh, …
oneEnglish, German, Spanish, French, …
twoArabic, Welsh, …
fewRussian, Polish, Czech, …
manyRussian, Polish, Arabic, …
otherRequired – fallback for all languages

Not all languages use all forms. Chinese and Japanese only use other. English uses one and other. Russian uses one, few, many, and other.

Dual Plural Translations

When a sentence contains two independently-pluralized quantities, use the DualPluralForms format:

"I have {#apples} apple and {#oranges} orange" = {
    one_one = "I have {apples} apple and {oranges} orange",
    one_other = "I have {apples} apple and {oranges} oranges",
    other_one = "I have {apples} apples and {oranges} orange",
    other_other = "I have {apples} apples and {oranges} oranges"
}

Parsing Translation Files

use waterui_locale::parser::{TranslationFile, TranslationValue};
use waterui_locale::PluralCategory;

let content = include_str!("../locales/en.toml");
let file = TranslationFile::parse(content).unwrap();

match file.get("Goodbye") {
    Some(TranslationValue::Simple(s)) => {
        assert_eq!(s, "Farewell");
    }
    _ => unreachable!(),
}

match file.get("I have {#count} apple") {
    Some(TranslationValue::Plural(forms)) => {
        assert_eq!(forms.get(PluralCategory::One), "I have {count} apple");
        assert_eq!(forms.get(PluralCategory::Other), "I have {count} apples");
    }
    _ => unreachable!(),
}

Plural Rules

Pluralization is one of the trickiest parts of i18n. waterui_locale::select_plural determines the correct plural category for a number in a given locale:

use waterui_locale::{select_plural, locales, PluralCategory};

// English
assert_eq!(select_plural(&locales::EN, &1), PluralCategory::One);
assert_eq!(select_plural(&locales::EN, &2), PluralCategory::Other);
assert_eq!(select_plural(&locales::EN, &0), PluralCategory::Other);

// Chinese (no plural distinction)
assert_eq!(select_plural(&locales::ZH_CN, &1), PluralCategory::Other);
assert_eq!(select_plural(&locales::ZH_CN, &100), PluralCategory::Other);

// Russian (complex rules)
assert_eq!(select_plural(&locales::RU, &1), PluralCategory::One);
assert_eq!(select_plural(&locales::RU, &2), PluralCategory::Few);
assert_eq!(select_plural(&locales::RU, &5), PluralCategory::Many);
assert_eq!(select_plural(&locales::RU, &21), PluralCategory::One);

// French (0 and 1 are both "one")
assert_eq!(select_plural(&locales::FR, &0), PluralCategory::One);
assert_eq!(select_plural(&locales::FR, &1), PluralCategory::One);
assert_eq!(select_plural(&locales::FR, &2), PluralCategory::Other);

Plural rules use absolute values (negative numbers are treated as their positive counterpart) and handle fractional values correctly.

Validation

valid_categories returns the set of plural categories that are meaningful for a locale. Use this to validate translation files – you can warn if a translator provides a few form for English (which never uses it):

use waterui_locale::plural::valid_categories;
use waterui_locale::locales;

let en_cats = valid_categories(&locales::EN);
// [One, Other]

let zh_cats = valid_categories(&locales::ZH_CN);
// [Other]

let ru_cats = valid_categories(&locales::RU);
// [One, Few, Many, Other]

The text! Macro

With translation files and plural rules in place, you need a way to use them in your views. The text! macro creates a LocalizedText view that automatically reacts to locale changes:

use waterui::prelude::*;

fn greeting(name: &str) -> impl View {
    text!("Hello, {name}!").bold().size(24.0)
}

When the runtime locale changes (for example, the user switches their device language), all text! views re-render with the new locale’s translations.

Styling Methods on LocalizedText

LocalizedText supports the same fluent styling methods as regular text:

MethodPurpose
.size(f32)Set the font size
.bold()Make the text bold
.italic()Make the text italic
.font(font)Set a custom font
.title()Use the title font style
.headline()Use the headline font style
.sub_headline()Use the subheadline font style
.body()Use the body font style
.caption()Use the caption font style
.footnote()Use the footnote font style

Locale-Aware Formatting

Translating strings is only part of the story. Numbers, dates, units, and lists all have locale-specific conventions.

LocalizedDisplay Trait

Types that implement LocalizedDisplay can format themselves differently depending on the locale:

use waterui_locale::{LocalizedDisplay, locales};

let value = 42;
let formatted = value.to_localized_string(&locales::EN);

A blanket implementation covers all Display types, though they will not produce locale-specific output. Custom types can override for locale-aware formatting.

Unit Formatting

The waterui_locale::format::unit module provides type-safe physical units with locale-aware display. The same distance reads differently in different languages:

use waterui_locale::format::unit::{Length, Meter, Kilometer, Mile};
use waterui_locale::locales;

let distance = Length::<Meter>::new(100.0);

distance.to_localized_string(&locales::EN);    // "100 m"
distance.to_localized_string(&locales::ZH_CN); // "100米"
distance.to_localized_string(&locales::JA);    // "100メートル"
distance.to_localized_string(&locales::KO);    // "100미터"

Units support conversion:

use waterui_locale::format::unit::{Length, Kilometer, Meter, Mile};

let km = Length::<Kilometer>::new(1.0);
let meters = km.to::<Meter>();     // 1000.0 m
let miles = km.to::<Mile>();       // ~0.621 mi

And arithmetic:

use waterui_locale::format::unit::{Length, Kilometer, Meter};

let km = Length::<Kilometer>::new(1.0);
let m = Length::<Meter>::new(500.0);
let total = km + m; // 1.5 km

Available unit types include:

  • Length: Meter, Kilometer, Mile, Feet
  • Mass: Kilogram, Gram

Date Formatting

use waterui_locale::format::date::{SimpleDate, DateStyle, format_date};
use waterui_locale::locales;

let date = SimpleDate::new(2026, 2, 15);

format_date(&locales::EN, &date, DateStyle::Short);   // "2/15/26"
format_date(&locales::EN, &date, DateStyle::Long);     // "February 15, 2026"
format_date(&locales::JA, &date, DateStyle::Long);     // "2026年2月15日"
format_date(&locales::ZH_CN, &date, DateStyle::Long);  // "2026年2月15日"
format_date(&locales::DE, &date, DateStyle::Short);     // "15.02.26"

List Formatting

LocalizedList formats arrays according to locale conventions – commas, conjunctions, and separators all vary:

use waterui_locale::format::LocalizedList;
use waterui_locale::{LocalizedDisplay, locales};

let items = LocalizedList(&["Apple", "Banana", "Orange"]);

items.to_localized_string(&locales::EN);    // "Apple, Banana, and Orange"
items.to_localized_string(&locales::ZH_CN); // "Apple、Banana和Orange"

Dynamic Locale Switching

The locale system integrates with waterkit-regional for runtime locale changes. A shared Binding<Locale> is maintained per thread. When the system locale changes (or the user explicitly selects a language), the binding updates, and all LocalizedText views re-render.

Setting the Locale in the Environment

To override the locale for a subtree of views, insert a Locale into the environment:

use waterui::prelude::*;
use waterui_locale::locales;

fn japanese_section() -> impl View {
    vstack((
        text!("Hello"),
        text!("Goodbye"),
    )).with(locales::JA)
}

All text! views within this subtree will resolve against ja instead of the system locale.

Locale as an Extractor

Locale implements Extractor, so you can use it with use_env to build locale-aware components:

use waterui::prelude::*;
use waterui_locale::Locale;

fn locale_aware_view() -> impl View {
    use_env(|locale: Locale| {
        text(format!("Current locale: {}", locale.canonical_tag()))
    })
}

Complete Example

Here is a minimal localized application that demonstrates plural-aware text with reactive state:

use waterui::prelude::*;
use waterui::app::App;
use waterui_locale::locales;

pub fn main() -> impl View {
    let count = Binding::i32(0);

    vstack((
        text!("I have {#count} apple", count = count.clone())
            .headline(),
        hstack((
            button("Add")
                .state(&count)
                .action(|State(c): State<Binding<i32>>| c.set(c.get() + 1)),
            button("Remove")
                .state(&count)
                .action(|State(c): State<Binding<i32>>| c.set((c.get() - 1).max(0))),
        )),
    ))
}

pub fn app(env: Environment) -> App {
    App::new(main, env)
}

waterui_ffi::export!();

With the matching translation files:

locales/en.toml:

"I have {#count} apple" = { one = "I have {count} apple", other = "I have {count} apples" }

locales/zh-CN.toml:

"I have {#count} apple" = "我有{count}个苹果"

locales/ja.toml:

"I have {#count} apple" = "{count}個のりんごがあります"

Chinese and Japanese do not need plural forms because they only use Other.

Try it yourself: Add a French translation file. Remember that in French, both 0 and 1 use the one form, so you will need one and other entries.

Summary

APIPurpose
LocaleICU4X-backed locale identifier
locales::EN, locales::ZH_CN, …Pre-defined locale constants
get_fallback_chain(locale)Script-aware locale fallback
TranslationFile::parse(toml)Parse a TOML translation file
select_plural(locale, n)CLDR-compliant plural category
valid_categories(locale)Valid plural forms for a locale
text!("...")Localizable text view macro
LocalizedTextLocale-reactive text view
LocalizedDisplayLocale-aware formatting trait
LocalizedListLocale-aware list formatting
Length, MassType-safe units with conversion
format_date(locale, date, style)Locale-aware date formatting
.with(locale)Override locale for a view subtree

What’s Next

Your app speaks multiple languages. But as it grows in complexity, you will want to organize cross-cutting concerns – theming, analytics, error views – into reusable units. In the next chapter, you will learn how WaterUI’s plugin system lets you extend the framework without modifying core code.