Errors

The IntlError variants, which call returns which, what their messages look like, and how to handle bad input from users and bad options from code.

Every fallible call returns Result<T, IntlError>. The crate also exports the alias dxn_intl::Result<T, E = IntlError>. IntlError is Clone, Debug, PartialEq and Eq, is Send + Sync, and implements std::error::Error, so it works with ?, Box<dyn std::error::Error>, anyhow and thiserror. Formatting itself (PluralRules::category) cannot fail.

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

fn price(tag: &str, code: &str, amount: f64) -> Result<String, IntlError> {
    let formatter = NumberFormatter::new(&parse_locale(tag)?, NumberOptions::currency(code))?;
    formatter.format(amount)
}

assert_eq!(price("en-US", "USD", 5.0)?, "$5.00");
assert!(matches!(price("en-US", "EURO", 5.0), Err(IntlError::InvalidCurrency(_))));
assert!(matches!(price("not a tag", "USD", 5.0), Err(IntlError::InvalidLocale(_))));
assert!(matches!(price("en-US", "USD", f64::NAN), Err(IntlError::InvalidNumber(_))));

#Variants

Every variant carries one String: the offending input or a reason.

VariantMeaningMessage
InvalidLocale(tag)The text is not a BCP 47 (or POSIX) language tag`not a tag` is not a valid language tag
InvalidNumber(text)Malformed number text, or a NaN or infinite float`abc` is not a number that can be formatted
InvalidOptions(reason)Options that do not go together, or an unknown keywordinvalid options: minimum fraction digits 3 is above the maximum 1
InvalidDateTime(reason)A date or time outside the calendarinvalid date or time: The day = 30 argument is out of range 1..=28
InvalidCurrency(code)Not three ASCII letters`EURO` is not an ISO 4217 currency code
Data(reason)ICU4X could not provide the locale datalocale data: ...

Data comes from ICU4X’s DataError. The crate compiles its data in, so you should not meet it in normal use; handle it as an internal failure.

The text inside InvalidDateTime and some InvalidOptions comes from ICU4X and is not stable. A date option that cannot be shown reads invalid options: InvalidDateFields or invalid options: MissingDateFields. Match on the variant, show your own message to users, and log the text.

#Which call returns what

CallErrors
parse_localeInvalidLocale
Number::from_str ("1.5".parse())InvalidNumber
Number::try_from(f64 or f32)InvalidNumber for NaN and infinities
NumberFormatter::newInvalidOptions (minimum above maximum, more than 100 digits), InvalidCurrency, Data
NumberFormatter::format, resolveInvalidNumber when the argument does not convert (NaN, infinity)
PluralRules::newData
PluralCategory::from_strInvalidOptions
DateTime::date, new, from_unix_millisInvalidDateTime
HourCycle::from_strInvalidOptions
DateTimeFormatter::newInvalidOptions (month, year-month or year with a time; neither date nor time), Data
DateTimeFormatter::formatInvalidDateTime for a DateTime outside the calendar or the year range

#Handling them

Errors split into two groups by who caused them. Input from a user (a typed amount, a tag from a header) fails at the edge and deserves a message or a default. Options and codes from your own code fail on the first call during development and then never again, so ? is enough.

use dxn_intl::{IntlError, Locale, Number, parse_locale};

/// The user's locale from a header or setting, or the app default when it is not a tag.
fn locale_or(default: &Locale, tag: &str) -> Locale {
    parse_locale(tag).unwrap_or_else(|_| default.clone())
}

/// A typed amount: `Ok(None)` for empty input, a message for bad input.
fn parse_amount(input: &str) -> Result<Option<Number>, String> {
    if input.trim().is_empty() {
        return Ok(None);
    }
    match input.parse::<Number>() {
        Ok(number) => Ok(Some(number)),
        Err(IntlError::InvalidNumber(text)) => Err(format!("`{text}` is not a number")),
        Err(other) => Err(other.to_string()),
    }
}

let default = parse_locale("en")?;
assert_eq!(locale_or(&default, "C").to_string(), "en");
assert_eq!(locale_or(&default, "de_AT.UTF-8").to_string(), "de-AT");

assert!(parse_amount("")?.is_none());
assert_eq!(parse_amount("12.50")?.map(|n| n.to_string()), Some("12.50".to_string()));
assert_eq!(parse_amount("12,50"), Err("`12,50` is not a number".to_string()));

IntlError is not #[non_exhaustive], so a match can list all six variants. Add a _ arm anyway if you want a later 0.x release to compile unchanged when a variant is added.

#Next

Limitations lists what the crate does not do. Cookbook has recipes that handle errors at the edge.