use icu_provider_blob::BlobDataProvider;
pub mod bidi;
pub mod segment;
static ICU_DATA: &[u8] = include_bytes!("data/icu_data.blob");
thread_local! {
static PROVIDER: BlobDataProvider = BlobDataProvider::try_new_from_static_blob(ICU_DATA)
.expect(
"src/i18n/data/icu_data.blob is committed and regenerated by \
scripts/make_icu_data.sh; a parse failure here means the blob \
and the installed icu_provider_blob version have drifted",
);
}
pub fn with_data_provider<R>(f: impl FnOnce(&BlobDataProvider) -> R) -> R {
PROVIDER.with(|provider| f(provider))
}
pub fn decimal_separator_for_tag(tag: &str) -> Option<char> {
use icu_decimal::options::DecimalFormatterOptions;
use icu_decimal::DecimalFormatter;
let locale: icu_locale_core::Locale = tag.parse().ok()?;
with_data_provider(|provider| {
let formatter = DecimalFormatter::try_new_with_buffer_provider(
provider,
locale.into(),
DecimalFormatterOptions::default(),
)
.ok()?;
let formatted = formatter.format_to_string(&"0.1".parse().ok()?);
formatted
.strip_prefix('0')?
.strip_suffix('1')?
.chars()
.next()
})
}
pub fn month_name_for_tag(
date: &icu_calendar::Date<icu_calendar::Gregorian>,
long: bool,
tag: &str,
) -> Option<String> {
use icu_datetime::fieldsets::M;
use icu_datetime::pattern::{FixedCalendarDateTimeNames, MonthNameLength};
let (length, pattern) = if long {
(MonthNameLength::StandaloneWide, "LLLL")
} else {
(MonthNameLength::StandaloneAbbreviated, "LLL")
};
let locale: icu_locale_core::Locale = tag.parse().ok()?;
with_data_provider(|provider| {
let mut names =
FixedCalendarDateTimeNames::<icu_calendar::Gregorian, M>::new_without_number_formatting(
locale.into(),
);
names
.load_month_names(&as_data_provider(provider), length)
.ok()?;
let pattern: icu_datetime::pattern::DateTimePattern = pattern.parse().ok()?;
let formatter = names.with_pattern_unchecked(&pattern);
write_field(formatter.format(date))
})
}
pub fn weekday_name_for_tag(
weekday: icu_calendar::types::Weekday,
long: bool,
tag: &str,
) -> Option<String> {
use icu_datetime::fieldsets::E;
use icu_datetime::pattern::{FixedCalendarDateTimeNames, WeekdayNameLength};
let (length, pattern) = if long {
(WeekdayNameLength::StandaloneWide, "cccc")
} else {
(WeekdayNameLength::StandaloneAbbreviated, "ccc")
};
let locale: icu_locale_core::Locale = tag.parse().ok()?;
with_data_provider(|provider| {
let mut names =
FixedCalendarDateTimeNames::<icu_calendar::Gregorian, E>::new_without_number_formatting(
locale.into(),
);
names
.load_weekday_names(&as_data_provider(provider), length)
.ok()?;
let pattern: icu_datetime::pattern::DateTimePattern = pattern.parse().ok()?;
let formatter = names.with_pattern_unchecked(&pattern);
write_field(formatter.format(&weekday))
})
}
pub fn day_period_for_tag(time: &icu_datetime::input::Time, tag: &str) -> Option<String> {
use icu_datetime::fieldsets::T;
use icu_datetime::pattern::{DayPeriodNameLength, FixedCalendarDateTimeNames};
let locale: icu_locale_core::Locale = tag.parse().ok()?;
with_data_provider(|provider| {
let mut names =
FixedCalendarDateTimeNames::<icu_calendar::Gregorian, T>::new_without_number_formatting(
locale.into(),
);
names
.load_day_period_names(
&as_data_provider(provider),
DayPeriodNameLength::Abbreviated,
)
.ok()?;
let pattern: icu_datetime::pattern::DateTimePattern = "a".parse().ok()?;
let formatter = names.with_pattern_unchecked(&pattern);
write_field(formatter.format(time))
})
}
pub fn short_date_for_tag(
date: &icu_calendar::Date<icu_calendar::Gregorian>,
tag: &str,
) -> Option<String> {
use icu_datetime::fieldsets::YMD;
use icu_datetime::FixedCalendarDateTimeFormatter;
let locale: icu_locale_core::Locale = tag.parse().ok()?;
with_data_provider(|provider| {
let formatter = FixedCalendarDateTimeFormatter::try_new_with_buffer_provider(
provider,
locale.into(),
YMD::short(),
)
.ok()?;
Some(write_pattern(formatter.format(date)))
})
}
pub fn short_time_for_tag(time: &icu_datetime::input::Time, tag: &str) -> Option<String> {
use icu_datetime::fieldsets::T;
use icu_datetime::FixedCalendarDateTimeFormatter;
let locale: icu_locale_core::Locale = tag.parse().ok()?;
with_data_provider(|provider| {
let formatter = FixedCalendarDateTimeFormatter::<icu_calendar::Gregorian, T>::try_new_with_buffer_provider(
provider,
locale.into(),
T::short().with_time_precision(icu_datetime::options::TimePrecision::Minute),
)
.ok()?;
Some(write_pattern(formatter.format(time)))
})
}
fn as_data_provider(
provider: &BlobDataProvider,
) -> icu_provider::buf::DeserializingBufferProvider<'_, BlobDataProvider> {
use icu_provider::buf::AsDeserializingBufferProvider;
provider.as_deserializing()
}
fn write_field(formatted: impl writeable::TryWriteable) -> Option<String> {
formatted
.try_write_to_string()
.ok()
.map(|written| written.into_owned())
}
fn write_pattern(formatted: impl writeable::Writeable) -> String {
formatted.write_to_string().into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use fixed_decimal::Decimal;
use icu_decimal::options::DecimalFormatterOptions;
use icu_decimal::DecimalFormatter;
#[test]
fn icu_decimal_formats_via_blob_provider() {
const CASES: &[(&str, &str)] = &[
("und", "1,234"),
("ca-ES", "1.234"),
("de-AT", "1\u{a0}234"),
("de-DE", "1.234"),
("en-CA", "1,234"),
("en-GB", "1,234"),
("en-US", "1,234"),
("fr-FR", "1\u{202f}234"),
("it-IT", "1234"),
("pl-PL", "1234"),
("ru-RU", "1\u{a0}234"),
];
for &(tag, expected) in CASES {
let out = with_data_provider(|provider| {
let formatter = DecimalFormatter::try_new_with_buffer_provider(
provider,
tag.parse::<icu_locale_core::Locale>()
.unwrap_or_else(|e| panic!("{tag:?} is a valid BCP-47 tag: {e}"))
.into(),
DecimalFormatterOptions::default(),
)
.unwrap_or_else(|e| panic!("{tag} is baked into src/i18n/data/icu_data.blob: {e}"));
formatter.format_to_string(&Decimal::from(1234))
});
assert_eq!(out, expected, "locale {tag}");
}
}
#[test]
fn decimal_separator_for_tag_resolves_region_divergence() {
assert_eq!(decimal_separator_for_tag("de-DE"), Some(','));
assert_eq!(decimal_separator_for_tag("de-CH"), Some('.'));
assert_eq!(decimal_separator_for_tag("en-ZA"), Some(','));
assert_eq!(decimal_separator_for_tag("es-MX"), Some('.'));
}
#[test]
fn decimal_separator_for_tag_is_none_for_a_tag_with_no_data() {
assert_eq!(decimal_separator_for_tag("zz-ZZ"), None);
}
#[test]
fn decimal_separator_for_tag_is_none_for_an_unparseable_tag() {
assert_eq!(decimal_separator_for_tag(""), None);
assert_eq!(decimal_separator_for_tag("not a bcp47 tag!"), None);
}
fn reference_date() -> icu_calendar::Date<icu_calendar::Gregorian> {
icu_calendar::Date::try_new_gregorian(2026, 8, 10).expect("2026-08-10 is a real date")
}
#[test]
fn month_name_for_tag_localizes() {
let d = reference_date();
assert_eq!(
month_name_for_tag(&d, true, "en-US").as_deref(),
Some("August")
);
assert_eq!(
month_name_for_tag(&d, false, "en-US").as_deref(),
Some("Aug")
);
assert_eq!(
month_name_for_tag(&d, true, "fr-FR").as_deref(),
Some("août")
);
assert_eq!(
month_name_for_tag(&d, true, "ru-RU").as_deref(),
Some("август")
);
assert_eq!(
month_name_for_tag(&d, true, "de-DE").as_deref(),
Some("August")
);
}
#[test]
fn weekday_name_for_tag_localizes() {
let monday = reference_date().weekday();
assert_eq!(
weekday_name_for_tag(monday, true, "en-US").as_deref(),
Some("Monday")
);
assert_eq!(
weekday_name_for_tag(monday, false, "en-US").as_deref(),
Some("Mon")
);
assert_eq!(
weekday_name_for_tag(monday, true, "de-DE").as_deref(),
Some("Montag")
);
assert_eq!(
weekday_name_for_tag(monday, false, "de-DE").as_deref(),
Some("Mo")
);
assert_eq!(
weekday_name_for_tag(monday, true, "fr-FR").as_deref(),
Some("lundi")
);
}
#[test]
fn the_reference_date_is_a_monday() {
assert_eq!(
reference_date().weekday(),
icu_calendar::types::Weekday::Monday
);
}
#[test]
fn month_names_are_the_standalone_set_not_the_format_set() {
let d = reference_date();
assert_eq!(
month_name_for_tag(&d, true, "ru-RU").as_deref(),
Some("август")
);
assert_eq!(
month_name_for_tag(&d, true, "pl-PL").as_deref(),
Some("sierpień")
);
assert_eq!(
month_name_for_tag(&d, true, "ca-ES").as_deref(),
Some("agost")
);
}
#[test]
fn date_names_are_none_for_unusable_tags() {
let d = reference_date();
let monday = d.weekday();
assert_eq!(month_name_for_tag(&d, true, "zz-ZZ"), None);
assert_eq!(month_name_for_tag(&d, true, "not a bcp47 tag!"), None);
assert_eq!(weekday_name_for_tag(monday, true, "zz-ZZ"), None);
assert_eq!(weekday_name_for_tag(monday, true, ""), None);
}
fn reference_time() -> icu_datetime::input::Time {
icu_datetime::input::Time::try_new(21, 30, 5, 0).expect("a valid time of day")
}
const NNBSP: &str = "\u{202f}";
#[test]
fn day_period_for_tag_localizes() {
let t = reference_time();
assert_eq!(day_period_for_tag(&t, "en-US").as_deref(), Some("PM"));
assert_eq!(day_period_for_tag(&t, "es-MX").as_deref(), Some("p.m."));
assert_eq!(
day_period_for_tag(&t, "ca-ES"),
Some(format!("p.{NNBSP}m.")),
"ca-ES separates the halves with U+202F"
);
}
#[test]
fn short_date_and_time_for_tag_localize() {
let d = reference_date();
let t = reference_time();
assert_eq!(short_date_for_tag(&d, "en-US").as_deref(), Some("8/10/26"));
assert_eq!(short_date_for_tag(&d, "de-DE").as_deref(), Some("10.08.26"));
assert_eq!(
short_date_for_tag(&d, "en-GB").as_deref(),
Some("10/08/2026")
);
assert_eq!(short_time_for_tag(&t, "de-DE").as_deref(), Some("21:30"));
assert_eq!(
short_time_for_tag(&t, "en-US"),
Some(format!("9:30{NNBSP}PM")),
"CLDR joins the time to its day period with U+202F"
);
}
#[test]
fn day_period_and_short_forms_are_none_for_unusable_tags() {
let d = reference_date();
let t = reference_time();
assert_eq!(day_period_for_tag(&t, "zz-ZZ"), None);
assert_eq!(day_period_for_tag(&t, "not a bcp47 tag!"), None);
assert_eq!(short_date_for_tag(&d, "zz-ZZ"), None);
assert_eq!(short_time_for_tag(&t, ""), None);
}
}