Locale-aware formatting for Rust

Plurals, numbers and dates, the same everywhere.

dxn_intl formats the things an app shows in the user's language: plural forms for every language CLDR has, decimal, percent and currency numbers, and dates and times in the locale's calendar and clock. It runs on ICU4X's compiled-in data and reads nothing from the system, so a native app, a server and a WebAssembly page print the same text.

[dependencies]
dxn_intl = { git = "https://github.com/dXnSix/dXn.Intl", tag = "v0.1.1" }
main.rs
use dxn_intl::{
    DateTime, DateTimeFormatter, DateTimeOptions, Length, NumberFormatter, NumberOptions,
    PluralCategory, PluralKind, PluralRules, negotiate, parse_locale,
};

fn main() -> Result<(), dxn_intl::IntlError> {
    // The best of the app's locales for what the user prefers.
    let available = [parse_locale("en")?, parse_locale("de")?, parse_locale("hr")?];
    let locale = negotiate(&[parse_locale("de-AT")?], &available, &available[0]);
    assert_eq!(locale.to_string(), "de");

    let plurals = PluralRules::new(&parse_locale("hr")?, PluralKind::Cardinal)?;
    assert_eq!(plurals.category(23), PluralCategory::Few);

    let money = NumberFormatter::new(&locale, NumberOptions::currency("EUR"))?;
    assert_eq!(money.format(1234.5)?, "1.234,50\u{a0}€");

    let dates = DateTimeFormatter::new(&locale, DateTimeOptions::date(Length::Long))?;
    assert_eq!(dates.format(&DateTime::date(2026, 10, 27)?)?, "27. Oktober 2026");
    Ok(())
}
Data
ICU4X 2.4, CLDR compiled in
Rust
1.97+ (edition 2024)
Targets
native + wasm32-unknown-unknown
System access
none: no files, clock or env
Unsafe code
forbidden
License
MIT OR Apache-2.0

Quick tour

#Plural forms that follow the visible digits

A plural category is a property of how the number is shown. In English “1 item” takes one and “1.0 items” takes other, so Number keeps the digits you give it.

use dxn_intl::{Number, PluralCategory, PluralKind, PluralRules, parse_locale};

let en = PluralRules::new(&parse_locale("en")?, PluralKind::Cardinal)?;
assert_eq!(en.category(1), PluralCategory::One);
assert_eq!(en.category("1.0".parse::<Number>()?), PluralCategory::Other);

let pl = PluralRules::new(&parse_locale("pl")?, PluralKind::Cardinal)?;
assert_eq!(pl.category(3), PluralCategory::Few);
assert_eq!(pl.category(5), PluralCategory::Many);

Plurals covers ordinals, the categories each language uses and how to pick a word from a category.

#Money in the user’s locale

The same amount reads differently per locale: symbol position, separators, spacing and the currency’s own number of digits.

use dxn_intl::{NumberFormatter, NumberOptions, parse_locale};

let de = NumberFormatter::new(&parse_locale("de-DE")?, NumberOptions::currency("EUR"))?;
assert_eq!(de.format(1234.5)?, "1.234,50\u{a0}€"); // a non-breaking space before €

let us = NumberFormatter::new(&parse_locale("en-US")?, NumberOptions::currency("USD"))?;
assert_eq!(us.format(1234.5)?, "$1,234.50");

Numbers covers percent, fraction digits, grouping, sign display and resolve.

#A timestamp in the user’s zone

Dates are wall times on the ISO calendar. There is no time zone database in the crate: you give the instant and the zone’s offset at that instant, and it shows the locale’s words and clock.

use dxn_intl::{DateTime, DateTimeFormatter, DateTimeOptions, Length, parse_locale};

let millis = 1_793_112_330_000; // 2026-10-27T14:45:30Z
let wall = DateTime::from_unix_millis(millis, 3600)?; // UTC+1
let options = DateTimeOptions::date_time(Length::Medium);

let de = DateTimeFormatter::new(&parse_locale("de")?, options)?;
assert_eq!(de.format(&wall)?, "27.10.2026, 15:45");
let en = DateTimeFormatter::new(&parse_locale("en-US")?, options)?;
assert_eq!(en.format(&wall)?, "Oct 27, 2026, 3:45\u{202f}PM");

Dates and times explains the options, hour cycles and how to get the offset.

#Pick the locale the user asked for

negotiate walks the user’s list in order, trying each entry’s CLDR fallback chain (de-AT then de) before the next entry, and keeps the user’s Unicode extensions such as -u-hc-h23.

use dxn_intl::{direction, negotiate, parse_locale};

let available = [parse_locale("en")?, parse_locale("de")?, parse_locale("ar")?];
let requested = [parse_locale("de-CH-u-hc-h12")?, parse_locale("fr")?];
let locale = negotiate(&requested, &available, &available[0]);
assert_eq!(locale.to_string(), "de-u-hc-h12");

assert_eq!(direction(&parse_locale("ar")?).as_str(), "rtl");

Locales covers POSIX tags, script matching and writing direction.