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:
| Key | Used By |
|---|---|
zero | Arabic, Welsh, … |
one | English, German, Spanish, French, … |
two | Arabic, Welsh, … |
few | Russian, Polish, Czech, … |
many | Russian, Polish, Arabic, … |
other | Required – 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:
| Method | Purpose |
|---|---|
.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
oneform, so you will needoneandotherentries.
Summary
| API | Purpose |
|---|---|
Locale | ICU4X-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 |
LocalizedText | Locale-reactive text view |
LocalizedDisplay | Locale-aware formatting trait |
LocalizedList | Locale-aware list formatting |
Length, Mass | Type-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.