use alloc::{
collections::BTreeMap,
string::{String, ToString},
vec::Vec,
};
use core::fmt::Write;
use core::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use std::sync::Mutex;
use azul_css::AzString;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use icu::collator::{Collator, options::CollatorOptions};
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use icu::decimal::input::Decimal;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use icu::decimal::DecimalFormatter;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use icu::list::{ListFormatter, options::ListFormatterOptions};
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use icu::locale::Locale;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use icu::plurals::PluralRules;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
use writeable::Writeable;
#[cfg(all(target_os = "macos", feature = "icu_macos"))]
#[path = "icu_macos.rs"]
mod icu_macos;
#[cfg(all(target_os = "windows", feature = "icu_windows"))]
#[path = "icu_windows.rs"]
mod icu_windows;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
pub use icu::locale::locale;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
pub use icu::plurals::{PluralCategory as IcuPluralCategory, PluralRules as IcuPluralRules};
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct IcuError {
pub message: AzString,
}
impl IcuError {
pub fn new(msg: impl Into<String>) -> Self {
Self {
message: AzString::from(msg.into()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[repr(C, u8)]
pub enum IcuResult {
Ok(AzString),
Err(IcuError),
}
impl IcuResult {
pub fn ok(s: impl Into<String>) -> Self {
IcuResult::Ok(AzString::from(s.into()))
}
pub fn err(msg: impl Into<String>) -> Self {
IcuResult::Err(IcuError::new(msg))
}
pub fn into_option(self) -> Option<AzString> {
match self {
IcuResult::Ok(s) => Some(s),
IcuResult::Err(_) => None,
}
}
pub fn unwrap_or(self, default: AzString) -> AzString {
match self {
IcuResult::Ok(s) => s,
IcuResult::Err(_) => default,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum PluralCategory {
Zero,
One,
Two,
Few,
Many,
#[default]
Other,
}
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
impl From<IcuPluralCategory> for PluralCategory {
fn from(cat: IcuPluralCategory) -> Self {
match cat {
IcuPluralCategory::Zero => PluralCategory::Zero,
IcuPluralCategory::One => PluralCategory::One,
IcuPluralCategory::Two => PluralCategory::Two,
IcuPluralCategory::Few => PluralCategory::Few,
IcuPluralCategory::Many => PluralCategory::Many,
IcuPluralCategory::Other => PluralCategory::Other,
}
}
}
#[cfg(any(
all(target_os = "macos", feature = "icu_macos"),
all(target_os = "windows", feature = "icu_windows"),
))]
pub(crate) fn decimal_string(integer_part: i64, decimal_places: i16) -> alloc::string::String {
use alloc::string::String;
if decimal_places <= 0 {
let mut s = integer_part.to_string();
for _ in 0..(-decimal_places as usize) {
s.push('0');
}
return s;
}
let dp = decimal_places as usize;
let negative = integer_part < 0;
let abs_val = (integer_part as i128).unsigned_abs();
let abs_str = alloc::format!("{abs_val}");
let body = if abs_str.len() <= dp {
let mut s = String::from("0.");
for _ in 0..(dp - abs_str.len()) {
s.push('0');
}
s.push_str(&abs_str);
s
} else {
let split = abs_str.len() - dp;
let mut s = String::new();
s.push_str(&abs_str[..split]);
s.push('.');
s.push_str(&abs_str[split..]);
s
};
if negative {
alloc::format!("-{body}")
} else {
body
}
}
#[cfg(any(
all(target_os = "macos", feature = "icu_macos"),
all(target_os = "windows", feature = "icu_windows"),
))]
pub(crate) fn plural_for(n: i64, lang: &str) -> PluralCategory {
let lang = lang.split(['-', '_']).next().unwrap_or(lang);
match lang {
"ar" | "arz" | "ckb" => {
let n100 = n.abs() % 100;
if n == 0 {
PluralCategory::Zero
} else if n == 1 {
PluralCategory::One
} else if n == 2 {
PluralCategory::Two
} else if (3..=10).contains(&n100) {
PluralCategory::Few
} else if (11..=99).contains(&n100) {
PluralCategory::Many
} else {
PluralCategory::Other
}
}
"cy" => match n {
0 => PluralCategory::Zero,
1 => PluralCategory::One,
2 => PluralCategory::Two,
3 => PluralCategory::Few,
6 => PluralCategory::Many,
_ => PluralCategory::Other,
},
"ru" | "uk" | "be" | "sr" | "hr" | "bs" | "sh" => {
let n10 = n.abs() % 10;
let n100 = n.abs() % 100;
if n10 == 1 && n100 != 11 {
PluralCategory::One
} else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
PluralCategory::Few
} else {
PluralCategory::Many
}
}
"pl" => {
let n10 = n.abs() % 10;
let n100 = n.abs() % 100;
if n == 1 {
PluralCategory::One
} else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
PluralCategory::Few
} else {
PluralCategory::Many
}
}
"cs" | "sk" => {
if n == 1 {
PluralCategory::One
} else if (2..=4).contains(&n) {
PluralCategory::Few
} else {
PluralCategory::Other
}
}
"sl" => {
let n100 = n.abs() % 100;
if n100 == 1 {
PluralCategory::One
} else if n100 == 2 {
PluralCategory::Two
} else if (3..=4).contains(&n100) {
PluralCategory::Few
} else {
PluralCategory::Other
}
}
"lt" => {
let n10 = n.abs() % 10;
let n100 = n.abs() % 100;
if n10 == 1 && !(11..=19).contains(&n100) {
PluralCategory::One
} else if (2..=9).contains(&n10) && !(11..=19).contains(&n100) {
PluralCategory::Few
} else {
PluralCategory::Other
}
}
"lv" => {
let n10 = n.abs() % 10;
let n100 = n.abs() % 100;
if n == 0 {
PluralCategory::Zero
} else if n10 == 1 && n100 != 11 {
PluralCategory::One
} else {
PluralCategory::Other
}
}
"ro" | "mo" => {
let n100 = n.abs() % 100;
if n == 1 {
PluralCategory::One
} else if n == 0 || (1..=19).contains(&n100) {
PluralCategory::Few
} else {
PluralCategory::Other
}
}
"mt" => {
let n100 = n.abs() % 100;
if n == 1 {
PluralCategory::One
} else if n == 0 || (2..=10).contains(&n100) {
PluralCategory::Few
} else if (11..=19).contains(&n100) {
PluralCategory::Many
} else {
PluralCategory::Other
}
}
"he" | "yi" | "iw" => {
if n == 1 {
PluralCategory::One
} else if n == 2 {
PluralCategory::Two
} else if n != 0 && n % 10 == 0 {
PluralCategory::Many
} else {
PluralCategory::Other
}
}
"ga" => match n {
1 => PluralCategory::One,
2 => PluralCategory::Two,
3..=6 => PluralCategory::Few,
7..=10 => PluralCategory::Many,
_ => PluralCategory::Other,
},
"fr" | "ff" | "kab" => {
if n == 0 || n == 1 {
PluralCategory::One
} else {
PluralCategory::Other
}
}
_ => {
if n == 1 {
PluralCategory::One
} else {
PluralCategory::Other
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum ListType {
And,
Or,
Unit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum DateTimeFieldSet {
YearMonthDay,
MonthDay,
YearMonth,
HourMinute,
HourMinuteSecond,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub enum CollationStrength {
Primary,
Secondary,
#[default]
Tertiary,
Quaternary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum FormatLength {
Short,
Medium,
Long,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct IcuDate {
pub year: i32,
pub month: u8,
pub day: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct IcuTime {
pub hour: u8,
pub minute: u8,
pub second: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct IcuDateTime {
pub date: IcuDate,
pub time: IcuTime,
}
impl IcuDate {
pub const fn new(year: i32, month: u8, day: u8) -> Self {
Self { year, month, day }
}
#[cfg(feature = "icu_chrono")]
pub fn now() -> Self {
use chrono::Datelike;
let now = chrono::Local::now();
Self {
year: now.year(),
month: now.month() as u8,
day: now.day() as u8,
}
}
#[cfg(feature = "icu_chrono")]
pub fn now_utc() -> Self {
use chrono::Datelike;
let now = chrono::Utc::now();
Self {
year: now.year(),
month: now.month() as u8,
day: now.day() as u8,
}
}
}
impl IcuTime {
pub const fn new(hour: u8, minute: u8, second: u8) -> Self {
Self { hour, minute, second }
}
#[cfg(feature = "icu_chrono")]
pub fn now() -> Self {
use chrono::Timelike;
let now = chrono::Local::now();
Self {
hour: now.hour() as u8,
minute: now.minute() as u8,
second: now.second() as u8,
}
}
#[cfg(feature = "icu_chrono")]
pub fn now_utc() -> Self {
use chrono::Timelike;
let now = chrono::Utc::now();
Self {
hour: now.hour() as u8,
minute: now.minute() as u8,
second: now.second() as u8,
}
}
}
impl IcuDateTime {
pub const fn new(date: IcuDate, time: IcuTime) -> Self {
Self { date, time }
}
#[cfg(feature = "icu_chrono")]
pub fn now() -> Self {
Self {
date: IcuDate::now(),
time: IcuTime::now(),
}
}
#[cfg(feature = "icu_chrono")]
pub fn now_utc() -> Self {
Self {
date: IcuDate::now_utc(),
time: IcuTime::now_utc(),
}
}
#[cfg(feature = "icu_chrono")]
pub fn timestamp_now() -> i64 {
chrono::Utc::now().timestamp_millis()
}
#[cfg(feature = "icu_chrono")]
pub fn timestamp_now_seconds() -> i64 {
chrono::Utc::now().timestamp()
}
#[cfg(feature = "icu_chrono")]
pub fn from_timestamp(timestamp_secs: i64) -> Option<Self> {
use chrono::{Datelike, TimeZone, Timelike};
chrono::Utc.timestamp_opt(timestamp_secs, 0).single().map(|dt| {
Self {
date: IcuDate {
year: dt.year(),
month: dt.month() as u8,
day: dt.day() as u8,
},
time: IcuTime {
hour: dt.hour() as u8,
minute: dt.minute() as u8,
second: dt.second() as u8,
},
}
})
}
#[cfg(feature = "icu_chrono")]
pub fn from_timestamp_millis(timestamp_millis: i64) -> Option<Self> {
Self::from_timestamp(timestamp_millis / 1000)
}
}
#[cfg(all(target_os = "macos", feature = "icu_macos"))]
pub use icu_macos::IcuLocalizer;
#[cfg(all(target_os = "windows", feature = "icu_windows"))]
pub use icu_windows::IcuLocalizer;
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
pub struct IcuLocalizer {
locale: Locale,
locale_string: AzString,
data_blob: Option<Vec<u8>>,
decimal_formatter: Option<DecimalFormatter>,
plural_rules_cardinal: Option<PluralRules>,
plural_rules_ordinal: Option<PluralRules>,
list_formatter_and: Option<ListFormatter>,
list_formatter_or: Option<ListFormatter>,
collator: Option<Collator>,
}
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
impl core::fmt::Debug for IcuLocalizer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("IcuLocalizer")
.field("locale", &self.locale_string)
.field("has_data_blob", &self.data_blob.is_some())
.finish()
}
}
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
impl IcuLocalizer {
pub fn new(locale_str: &str) -> Self {
let locale = locale_str.parse::<Locale>().unwrap_or_else(|_| {
"en-US".parse().unwrap()
});
Self {
locale_string: AzString::from(locale.to_string()),
locale,
data_blob: None,
decimal_formatter: None,
plural_rules_cardinal: None,
plural_rules_ordinal: None,
list_formatter_and: None,
list_formatter_or: None,
collator: None,
}
}
pub fn from_system_language(system_language: &AzString) -> Self {
Self::new(system_language.as_str())
}
pub fn load_data_blob(&mut self, data: Vec<u8>) {
self.data_blob = Some(data);
self.decimal_formatter = None;
self.plural_rules_cardinal = None;
self.plural_rules_ordinal = None;
self.list_formatter_and = None;
self.list_formatter_or = None;
self.collator = None;
}
pub fn get_locale(&self) -> AzString {
self.locale_string.clone()
}
pub fn get_language(&self) -> AzString {
AzString::from(self.locale.id.language.to_string())
}
pub fn get_region(&self) -> Option<AzString> {
self.locale.id.region.map(|r| AzString::from(r.to_string()))
}
pub fn set_locale(&mut self, locale_str: &str) -> bool {
match locale_str.parse::<Locale>() {
Ok(locale) => {
self.locale = locale;
self.locale_string = AzString::from(locale_str.to_string());
self.decimal_formatter = None;
self.plural_rules_cardinal = None;
self.plural_rules_ordinal = None;
self.list_formatter_and = None;
self.list_formatter_or = None;
self.collator = None;
true
}
Err(_) => false,
}
}
fn get_decimal_formatter(&mut self) -> &DecimalFormatter {
if self.decimal_formatter.is_none() {
let formatter = DecimalFormatter::try_new(self.locale.clone().into(), Default::default())
.unwrap_or_else(|_| {
DecimalFormatter::try_new(Default::default(), Default::default())
.expect("default locale should always work")
});
self.decimal_formatter = Some(formatter);
}
self.decimal_formatter.as_ref().unwrap()
}
pub fn format_integer(&mut self, value: i64) -> AzString {
let decimal = Decimal::from(value);
let formatter = self.get_decimal_formatter();
let mut output = String::new();
let _ = write!(output, "{}", formatter.format(&decimal));
AzString::from(output)
}
pub fn format_decimal(&mut self, integer_part: i64, decimal_places: i16) -> AzString {
let mut decimal = Decimal::from(integer_part);
decimal.multiply_pow10(-decimal_places);
let formatter = self.get_decimal_formatter();
let mut output = String::new();
let _ = write!(output, "{}", formatter.format(&decimal));
AzString::from(output)
}
fn get_plural_rules_cardinal(&mut self) -> &PluralRules {
if self.plural_rules_cardinal.is_none() {
let rules = PluralRules::try_new(self.locale.clone().into(), Default::default())
.unwrap_or_else(|_| {
PluralRules::try_new(Default::default(), Default::default())
.expect("default locale should always work")
});
self.plural_rules_cardinal = Some(rules);
}
self.plural_rules_cardinal.as_ref().unwrap()
}
pub fn get_plural_category(&mut self, value: i64) -> PluralCategory {
let rules = self.get_plural_rules_cardinal();
let abs_value = value.unsigned_abs() as usize;
rules.category_for(abs_value).into()
}
pub fn pluralize(
&mut self,
value: i64,
zero: &str,
one: &str,
two: &str,
few: &str,
many: &str,
other: &str,
) -> AzString {
let category = self.get_plural_category(value);
let template = match category {
PluralCategory::Zero => zero,
PluralCategory::One => one,
PluralCategory::Two => two,
PluralCategory::Few => few,
PluralCategory::Many => many,
PluralCategory::Other => other,
};
let result = template.replace("{}", &value.to_string());
AzString::from(result)
}
fn get_list_formatter_and(&mut self) -> &ListFormatter {
if self.list_formatter_and.is_none() {
let formatter = ListFormatter::try_new_and(
self.locale.clone().into(),
ListFormatterOptions::default(),
)
.unwrap_or_else(|_| {
ListFormatter::try_new_and(Default::default(), ListFormatterOptions::default())
.expect("default locale should always work")
});
self.list_formatter_and = Some(formatter);
}
self.list_formatter_and.as_ref().unwrap()
}
fn get_list_formatter_or(&mut self) -> &ListFormatter {
if self.list_formatter_or.is_none() {
let formatter = ListFormatter::try_new_or(
self.locale.clone().into(),
ListFormatterOptions::default(),
)
.unwrap_or_else(|_| {
ListFormatter::try_new_or(Default::default(), ListFormatterOptions::default())
.expect("default locale should always work")
});
self.list_formatter_or = Some(formatter);
}
self.list_formatter_or.as_ref().unwrap()
}
pub fn format_list(&mut self, items: &[AzString], list_type: ListType) -> AzString {
let str_items: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
let formatted = match list_type {
ListType::And => {
let formatter = self.get_list_formatter_and();
formatter.format(str_items.iter().copied())
}
ListType::Or => {
let formatter = self.get_list_formatter_or();
formatter.format(str_items.iter().copied())
}
ListType::Unit => {
return AzString::from(str_items.join(", "));
}
};
let mut output = String::new();
let _ = write!(output, "{}", formatted);
AzString::from(output)
}
pub fn format_date(&mut self, date: IcuDate, length: FormatLength) -> IcuResult {
use icu::datetime::fieldsets::YMD;
use icu::datetime::input::Date;
use icu::datetime::DateTimeFormatter;
let icu_date = match Date::try_new_iso(date.year, date.month, date.day) {
Ok(d) => d,
Err(e) => return IcuResult::err(format!("Invalid date: {}", e)),
};
let field_set = match length {
FormatLength::Short => YMD::short(),
FormatLength::Medium => YMD::medium(),
FormatLength::Long => YMD::long(),
};
let formatter = match DateTimeFormatter::try_new(self.locale.clone().into(), field_set) {
Ok(f) => f,
Err(e) => return IcuResult::err(format!("Failed to create formatter: {:?}", e)),
};
let mut output = String::new();
let _ = write!(output, "{}", formatter.format(&icu_date));
IcuResult::ok(output)
}
pub fn format_time(&mut self, time: IcuTime, include_seconds: bool) -> IcuResult {
use icu::datetime::fieldsets;
use icu::datetime::input::Time;
use icu::datetime::NoCalendarFormatter;
let icu_time = match Time::try_new(time.hour, time.minute, time.second, 0) {
Ok(t) => t,
Err(e) => return IcuResult::err(format!("Invalid time: {}", e)),
};
let mut output = String::new();
if include_seconds {
let formatter: NoCalendarFormatter<fieldsets::T> =
match NoCalendarFormatter::try_new(self.locale.clone().into(), fieldsets::T::medium()) {
Ok(f) => f,
Err(e) => return IcuResult::err(format!("Failed to create formatter: {:?}", e)),
};
let _ = write!(output, "{}", formatter.format(&icu_time));
} else {
let formatter: NoCalendarFormatter<fieldsets::T> =
match NoCalendarFormatter::try_new(self.locale.clone().into(), fieldsets::T::short()) {
Ok(f) => f,
Err(e) => return IcuResult::err(format!("Failed to create formatter: {:?}", e)),
};
let _ = write!(output, "{}", formatter.format(&icu_time));
}
IcuResult::ok(output)
}
pub fn format_datetime(&mut self, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
use icu::datetime::fieldsets::YMD;
use icu::datetime::input::{Date, DateTime, Time};
use icu::datetime::DateTimeFormatter;
let icu_date = match Date::try_new_iso(datetime.date.year, datetime.date.month, datetime.date.day) {
Ok(d) => d,
Err(e) => return IcuResult::err(format!("Invalid date: {}", e)),
};
let icu_time = match Time::try_new(datetime.time.hour, datetime.time.minute, datetime.time.second, 0) {
Ok(t) => t,
Err(e) => return IcuResult::err(format!("Invalid time: {}", e)),
};
let icu_datetime = DateTime {
date: icu_date,
time: icu_time,
};
let field_set = match length {
FormatLength::Short => YMD::short().with_time_hm(),
FormatLength::Medium => YMD::medium().with_time_hm(),
FormatLength::Long => YMD::long().with_time_hm(),
};
let formatter = match DateTimeFormatter::try_new(self.locale.clone().into(), field_set) {
Ok(f) => f,
Err(e) => return IcuResult::err(format!("Failed to create formatter: {:?}", e)),
};
let mut output = String::new();
let _ = write!(output, "{}", formatter.format(&icu_datetime));
IcuResult::ok(output)
}
fn get_collator(&mut self) -> &Collator {
if self.collator.is_none() {
let collator = Collator::try_new(self.locale.clone().into(), CollatorOptions::default())
.map(|borrowed| borrowed.static_to_owned())
.unwrap_or_else(|_| {
Collator::try_new(Default::default(), CollatorOptions::default())
.map(|borrowed| borrowed.static_to_owned())
.expect("default locale should always work")
});
self.collator = Some(collator);
}
self.collator.as_ref().unwrap()
}
pub fn compare(&mut self, a: &str, b: &str) -> core::cmp::Ordering {
self.get_collator().as_borrowed().compare(a, b)
}
pub fn sort_strings(&mut self, strings: &mut [AzString]) {
let collator = self.get_collator().as_borrowed();
strings.sort_by(|a, b| collator.compare(a.as_str(), b.as_str()));
}
pub fn sorted_strings(&mut self, strings: &[AzString]) -> Vec<AzString> {
let mut result: Vec<AzString> = strings.to_vec();
self.sort_strings(&mut result);
result
}
pub fn strings_equal(&mut self, a: &str, b: &str) -> bool {
self.compare(a, b) == core::cmp::Ordering::Equal
}
pub fn get_sort_key(&mut self, s: &str) -> Vec<u8> {
let collator = self.get_collator().as_borrowed();
let mut key = Vec::new();
let _ = collator.write_sort_key_to(s, &mut key);
key
}
}
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
impl Default for IcuLocalizer {
fn default() -> Self {
Self::new("en-US")
}
}
#[cfg(all(feature = "icu", not(all(target_os = "macos", feature = "icu_macos")), not(all(target_os = "windows", feature = "icu_windows"))))]
impl Clone for IcuLocalizer {
fn clone(&self) -> Self {
Self {
locale: self.locale.clone(),
locale_string: self.locale_string.clone(),
data_blob: self.data_blob.clone(),
decimal_formatter: None,
plural_rules_cardinal: None,
plural_rules_ordinal: None,
list_formatter_and: None,
list_formatter_or: None,
collator: None,
}
}
}
pub struct IcuLocalizerInner {
cache: Mutex<BTreeMap<String, IcuLocalizer>>,
default_locale: Mutex<AzString>,
data_blob: Mutex<Option<Vec<u8>>>,
}
#[repr(C)]
pub struct IcuLocalizerHandle {
pub ptr: *const IcuLocalizerInner,
pub copies: *const AtomicUsize,
pub run_destructor: bool,
}
unsafe impl Send for IcuLocalizerHandle {}
unsafe impl Sync for IcuLocalizerHandle {}
impl Clone for IcuLocalizerHandle {
fn clone(&self) -> Self {
unsafe {
self.copies
.as_ref()
.map(|m| m.fetch_add(1, AtomicOrdering::SeqCst));
}
Self {
ptr: self.ptr,
copies: self.copies,
run_destructor: true,
}
}
}
impl Drop for IcuLocalizerHandle {
fn drop(&mut self) {
if !self.run_destructor || self.copies.is_null() {
return;
}
unsafe {
let copies = (*self.copies).fetch_sub(1, AtomicOrdering::SeqCst);
if copies == 1 {
let _ = Box::from_raw(self.ptr as *mut IcuLocalizerInner);
let _ = Box::from_raw(self.copies as *mut AtomicUsize);
}
}
}
}
impl core::fmt::Debug for IcuLocalizerHandle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let inner = unsafe { &*self.ptr };
let default_locale = inner.default_locale.lock()
.map(|g| g.clone())
.unwrap_or_else(|_| AzString::from(""));
f.debug_struct("IcuLocalizerHandle")
.field("default_locale", &default_locale)
.finish()
}
}
impl Default for IcuLocalizerHandle {
fn default() -> Self {
Self {
ptr: Box::into_raw(Box::new(IcuLocalizerInner {
cache: Mutex::new(BTreeMap::new()),
default_locale: Mutex::new(AzString::from("en-US")),
data_blob: Mutex::new(None),
})),
copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
run_destructor: true,
}
}
}
impl IcuLocalizerHandle {
pub fn new(default_locale: &str) -> Self {
Self {
ptr: Box::into_raw(Box::new(IcuLocalizerInner {
cache: Mutex::new(BTreeMap::new()),
default_locale: Mutex::new(AzString::from(default_locale)),
data_blob: Mutex::new(None),
})),
copies: Box::into_raw(Box::new(AtomicUsize::new(1))),
run_destructor: true,
}
}
#[inline]
fn inner(&self) -> &IcuLocalizerInner {
unsafe { &*self.ptr }
}
pub fn from_system_language(language: &AzString) -> Self {
Self::new(language.as_str())
}
pub fn get_default_locale(&self) -> AzString {
self.inner().default_locale.lock()
.map(|g| g.clone())
.unwrap_or_else(|_| AzString::from("en-US"))
}
pub fn set_default_locale(&mut self, locale: &str) {
if let Ok(mut guard) = self.inner().default_locale.lock() {
*guard = AzString::from(locale);
}
}
pub fn set_locale(&mut self, locale: &str) {
self.set_default_locale(locale);
}
pub fn load_data_blob(&self, data: &[u8]) -> bool {
let inner = self.inner();
if let Ok(mut blob) = inner.data_blob.lock() {
*blob = Some(data.to_vec());
if let Ok(mut cache) = inner.cache.lock() {
for localizer in cache.values_mut() {
localizer.load_data_blob(data.to_vec());
}
}
true
} else {
false
}
}
fn with_localizer_or<F, R, E>(&self, locale: &str, f: F, fallback: E) -> R
where
F: FnOnce(&mut IcuLocalizer) -> R,
E: FnOnce() -> R,
{
let inner = self.inner();
let blob = inner.data_blob.lock().ok().and_then(|b| b.clone());
inner.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| {
let mut l = IcuLocalizer::new(locale);
if let Some(data) = blob {
l.load_data_blob(data);
}
l
});
f(localizer)
})
.unwrap_or_else(|_| fallback())
}
fn with_localizer<F, R>(&self, locale: &str, f: F) -> R
where
F: FnOnce(&mut IcuLocalizer) -> R,
R: Default,
{
self.with_localizer_or(locale, f, R::default)
}
pub fn get_language(&self, locale: &str) -> AzString {
self.with_localizer(locale, |l| l.get_language())
}
pub fn format_integer(&self, locale: &str, value: i64) -> AzString {
self.with_localizer(locale, |l| l.format_integer(value))
}
pub fn format_decimal(&self, locale: &str, integer_part: i64, decimal_places: i16) -> AzString {
self.with_localizer(locale, |l| l.format_decimal(integer_part, decimal_places))
}
pub fn get_plural_category(&self, locale: &str, value: i64) -> PluralCategory {
self.with_localizer(locale, |l| l.get_plural_category(value))
}
pub fn pluralize(
&self,
locale: &str,
value: i64,
zero: &str,
one: &str,
two: &str,
few: &str,
many: &str,
other: &str,
) -> AzString {
self.with_localizer_or(
locale,
|l| l.pluralize(value, zero, one, two, few, many, other),
|| AzString::from(other),
)
}
pub fn format_list(&self, locale: &str, items: &[AzString], list_type: ListType) -> AzString {
self.with_localizer_or(
locale,
|l| l.format_list(items, list_type),
|| {
let strs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
AzString::from(strs.join(", "))
},
)
}
pub fn format_date(&self, locale: &str, date: IcuDate, length: FormatLength) -> IcuResult {
self.with_localizer_or(
locale,
|l| l.format_date(date, length),
|| IcuResult::err("Lock error".to_string()),
)
}
pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
self.with_localizer_or(
locale,
|l| l.format_time(time, include_seconds),
|| IcuResult::err("Lock error".to_string()),
)
}
pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
self.with_localizer_or(
locale,
|l| l.format_datetime(datetime, length),
|| IcuResult::err("Lock error".to_string()),
)
}
pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
self.with_localizer(locale, |l| {
match l.compare(a, b) {
core::cmp::Ordering::Less => -1,
core::cmp::Ordering::Equal => 0,
core::cmp::Ordering::Greater => 1,
}
})
}
pub fn sort_strings(&self, locale: &str, strings: &[AzString]) -> IcuStringVec {
self.with_localizer_or(
locale,
|l| IcuStringVec::from(l.sorted_strings(strings)),
|| IcuStringVec::from(strings.to_vec()),
)
}
pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
self.with_localizer_or(
locale,
|l| l.strings_equal(a, b),
|| a == b,
)
}
pub fn get_sort_key(&self, locale: &str, s: &str) -> Vec<u8> {
self.with_localizer(locale, |l| l.get_sort_key(s))
}
pub fn format_plural(&self, locale: &str, value: i64, zero: &str, one: &str, other: &str) -> AzString {
let template = self.pluralize(locale, value, zero, one, other, other, other, other);
let formatted_num = self.format_integer(locale, value);
AzString::from(template.as_str().replace("{}", formatted_num.as_str()))
}
pub fn format_list_strings(&self, locale: &str, items: &[&str], list_type: ListType) -> AzString {
let az_items: Vec<AzString> = items.iter().map(|s| AzString::from(*s)).collect();
self.format_list(locale, &az_items, list_type)
}
pub fn clear_cache(&self) {
if let Ok(mut cache) = self.inner().cache.lock() {
cache.clear();
}
}
pub fn cached_locale_count(&self) -> usize {
self.inner().cache
.lock()
.map(|cache| cache.len())
.unwrap_or(0)
}
pub fn cached_locales(&self) -> Vec<AzString> {
self.inner().cache
.lock()
.map(|cache| cache.keys().map(|k| AzString::from(k.clone())).collect())
.unwrap_or_default()
}
}
pub type OptionAzString = azul_css::OptionString;
azul_css::impl_vec!(AzString, IcuStringVec, IcuStringVecDestructor, IcuStringVecDestructorType, IcuStringVecSlice, OptionAzString);
azul_css::impl_vec_clone!(AzString, IcuStringVec, IcuStringVecDestructor);
azul_css::impl_vec_debug!(AzString, IcuStringVec);
use azul_core::callbacks::LayoutCallbackInfo;
pub trait LayoutCallbackInfoIcuExt {
fn icu_get_locale(&self) -> AzString;
fn icu_get_language(&self) -> AzString;
fn icu_format_integer(&self, value: i64) -> AzString;
fn icu_format_decimal(&self, integer_part: i64, decimal_places: i16) -> AzString;
fn icu_get_plural_category(&self, value: i64) -> PluralCategory;
fn icu_pluralize(
&self,
value: i64,
zero: &str,
one: &str,
two: &str,
few: &str,
many: &str,
other: &str,
) -> AzString;
fn icu_format_list(&self, items: &[AzString], list_type: ListType) -> AzString;
fn icu_format_date(&self, date: IcuDate, length: FormatLength) -> IcuResult;
fn icu_format_time(&self, time: IcuTime, include_seconds: bool) -> IcuResult;
fn icu_format_datetime(&self, datetime: IcuDateTime, length: FormatLength) -> IcuResult;
fn icu_compare_strings(&self, a: &str, b: &str) -> i32;
fn icu_sort_strings(&self, strings: &[AzString]) -> IcuStringVec;
fn icu_strings_equal(&self, a: &str, b: &str) -> bool;
}
fn shared_localizer_handle() -> &'static IcuLocalizerHandle {
use std::sync::OnceLock;
static SHARED: OnceLock<IcuLocalizerHandle> = OnceLock::new();
SHARED.get_or_init(|| IcuLocalizerHandle::new("en-US"))
}
impl LayoutCallbackInfoIcuExt for LayoutCallbackInfo {
fn icu_get_locale(&self) -> AzString {
self.get_system_style().language.clone()
}
fn icu_get_language(&self) -> AzString {
let system_style = self.get_system_style();
shared_localizer_handle().get_language(system_style.language.as_str())
}
fn icu_format_integer(&self, value: i64) -> AzString {
let system_style = self.get_system_style();
shared_localizer_handle().format_integer(system_style.language.as_str(), value)
}
fn icu_format_decimal(&self, integer_part: i64, decimal_places: i16) -> AzString {
let system_style = self.get_system_style();
shared_localizer_handle().format_decimal(
system_style.language.as_str(),
integer_part,
decimal_places,
)
}
fn icu_get_plural_category(&self, value: i64) -> PluralCategory {
let system_style = self.get_system_style();
shared_localizer_handle().get_plural_category(system_style.language.as_str(), value)
}
fn icu_pluralize(
&self,
value: i64,
zero: &str,
one: &str,
two: &str,
few: &str,
many: &str,
other: &str,
) -> AzString {
let system_style = self.get_system_style();
shared_localizer_handle().pluralize(
system_style.language.as_str(),
value,
zero,
one,
two,
few,
many,
other,
)
}
fn icu_format_list(&self, items: &[AzString], list_type: ListType) -> AzString {
let system_style = self.get_system_style();
shared_localizer_handle().format_list(system_style.language.as_str(), items, list_type)
}
fn icu_format_date(&self, date: IcuDate, length: FormatLength) -> IcuResult {
let system_style = self.get_system_style();
shared_localizer_handle().format_date(system_style.language.as_str(), date, length)
}
fn icu_format_time(&self, time: IcuTime, include_seconds: bool) -> IcuResult {
let system_style = self.get_system_style();
shared_localizer_handle().format_time(
system_style.language.as_str(),
time,
include_seconds,
)
}
fn icu_format_datetime(&self, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
let system_style = self.get_system_style();
shared_localizer_handle().format_datetime(system_style.language.as_str(), datetime, length)
}
fn icu_compare_strings(&self, a: &str, b: &str) -> i32 {
let system_style = self.get_system_style();
shared_localizer_handle().compare_strings(system_style.language.as_str(), a, b)
}
fn icu_sort_strings(&self, strings: &[AzString]) -> IcuStringVec {
let system_style = self.get_system_style();
shared_localizer_handle().sort_strings(system_style.language.as_str(), strings)
}
fn icu_strings_equal(&self, a: &str, b: &str) -> bool {
let system_style = self.get_system_style();
shared_localizer_handle().strings_equal(system_style.language.as_str(), a, b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_integer_en_us() {
let mut localizer = IcuLocalizer::new("en-US");
assert_eq!(localizer.format_integer(1234567).as_str(), "1,234,567");
}
#[test]
fn test_format_integer_de_de() {
let mut localizer = IcuLocalizer::new("de-DE");
let result = localizer.format_integer(1234567);
assert!(result.as_str().contains('.') || result.as_str().contains('\u{a0}'));
}
#[test]
fn test_plural_category_english() {
let mut localizer = IcuLocalizer::new("en-US");
assert_eq!(localizer.get_plural_category(1), PluralCategory::One);
assert_eq!(localizer.get_plural_category(2), PluralCategory::Other);
assert_eq!(localizer.get_plural_category(0), PluralCategory::Other);
}
#[test]
fn test_format_list_and() {
let mut localizer = IcuLocalizer::new("en-US");
let items = vec![
AzString::from("A"),
AzString::from("B"),
AzString::from("C"),
];
let result = localizer.format_list(&items, ListType::And);
assert!(result.as_str().contains("and"));
}
#[test]
fn test_format_date() {
let mut localizer = IcuLocalizer::new("en-US");
let date = IcuDate {
year: 2025,
month: 1,
day: 15,
};
let result = localizer.format_date(date, FormatLength::Medium);
assert!(matches!(result, IcuResult::Ok(_)));
}
#[test]
fn test_cache_thread_safety() {
let cache = IcuLocalizerHandle::from_system_language(&AzString::from("en-US"));
let cache2 = cache.clone();
assert_eq!(
cache.format_integer("en-US", 1000).as_str(),
cache2.format_integer("en-US", 1000).as_str()
);
}
#[test]
fn test_cache_multi_locale() {
let cache = IcuLocalizerHandle::default();
let en = cache.format_integer("en-US", 1234567);
let de = cache.format_integer("de-DE", 1234567);
assert!(en.as_str().contains(','));
assert!(de.as_str().contains('.') || de.as_str().contains('\u{a0}'));
}
#[test]
fn test_collation_compare() {
let mut localizer = IcuLocalizer::new("en-US");
assert_eq!(localizer.compare("apple", "banana"), core::cmp::Ordering::Less);
assert_eq!(localizer.compare("banana", "apple"), core::cmp::Ordering::Greater);
assert_eq!(localizer.compare("apple", "apple"), core::cmp::Ordering::Equal);
}
#[test]
fn test_collation_sort() {
let mut localizer = IcuLocalizer::new("en-US");
let mut strings = vec![
AzString::from("cherry"),
AzString::from("apple"),
AzString::from("banana"),
];
localizer.sort_strings(&mut strings);
assert_eq!(strings[0].as_str(), "apple");
assert_eq!(strings[1].as_str(), "banana");
assert_eq!(strings[2].as_str(), "cherry");
}
#[test]
fn test_collation_german_umlauts() {
let mut localizer = IcuLocalizer::new("de-DE");
let result = localizer.compare("Ägypten", "Andorra");
assert!(result != core::cmp::Ordering::Equal);
}
#[test]
fn test_sort_key() {
let mut localizer = IcuLocalizer::new("en-US");
let key_a = localizer.get_sort_key("apple");
let key_b = localizer.get_sort_key("banana");
assert!(key_a < key_b);
}
}