use alloc::{
collections::BTreeMap,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use core::fmt::Write;
use std::sync::Mutex;
use azul_css::AzString;
use icu::collator::{Collator, options::CollatorOptions};
use icu::decimal::input::Decimal;
use icu::decimal::DecimalFormatter;
use icu::list::{ListFormatter, options::ListFormatterOptions};
use icu::locale::Locale;
use icu::plurals::PluralRules;
use writeable::Writeable;
use crate::fmt::{FmtArg, FmtArgVec, FmtValue};
pub use icu::locale::locale;
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)]
#[repr(C)]
pub enum PluralCategory {
Zero,
One,
Two,
Few,
Many,
Other,
}
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,
}
}
}
#[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)
}
}
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>,
}
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()
}
}
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();
rules.category_for(value as usize).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
}
}
impl Default for IcuLocalizer {
fn default() -> Self {
Self::new("en-US")
}
}
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,
}
}
}
struct IcuLocalizerInner {
cache: Mutex<BTreeMap<String, IcuLocalizer>>,
default_locale: Mutex<AzString>,
}
#[repr(C)]
#[derive(Clone)]
pub struct IcuLocalizerHandle {
ptr: Arc<IcuLocalizerInner>,
}
impl core::fmt::Debug for IcuLocalizerHandle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let default_locale = self.ptr.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: Arc::new(IcuLocalizerInner {
cache: Mutex::new(BTreeMap::new()),
default_locale: Mutex::new(AzString::from("en-US")),
}),
}
}
}
impl IcuLocalizerHandle {
pub fn new(default_locale: &str) -> Self {
Self {
ptr: Arc::new(IcuLocalizerInner {
cache: Mutex::new(BTreeMap::new()),
default_locale: Mutex::new(AzString::from(default_locale)),
}),
}
}
pub fn from_system_language(language: &AzString) -> Self {
Self::new(language.as_str())
}
pub fn get_default_locale(&self) -> AzString {
self.ptr.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.ptr.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 {
if let Ok(mut cache) = self.ptr.cache.lock() {
cache.clear();
true
} else {
false
}
}
fn with_localizer<F, R>(&self, locale: &str, f: F) -> R
where
F: FnOnce(&mut IcuLocalizer) -> R,
R: Default,
{
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
f(localizer)
})
.unwrap_or_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.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.get_plural_category(value)
})
.unwrap_or(PluralCategory::Other)
}
pub fn pluralize(
&self,
locale: &str,
value: i64,
zero: &str,
one: &str,
two: &str,
few: &str,
many: &str,
other: &str,
) -> AzString {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.pluralize(value, zero, one, two, few, many, other)
})
.unwrap_or_else(|_| AzString::from(other))
}
pub fn format_list(&self, locale: &str, items: &[AzString], list_type: ListType) -> AzString {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.format_list(items, list_type)
})
.unwrap_or_else(|_| {
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.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.format_date(date, length)
})
.unwrap_or_else(|e| IcuResult::err(format!("Lock error: {:?}", e)))
}
pub fn format_time(&self, locale: &str, time: IcuTime, include_seconds: bool) -> IcuResult {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.format_time(time, include_seconds)
})
.unwrap_or_else(|e| IcuResult::err(format!("Lock error: {:?}", e)))
}
pub fn format_datetime(&self, locale: &str, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.format_datetime(datetime, length)
})
.unwrap_or_else(|e| IcuResult::err(format!("Lock error: {:?}", e)))
}
pub fn compare_strings(&self, locale: &str, a: &str, b: &str) -> i32 {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
match localizer.compare(a, b) {
core::cmp::Ordering::Less => -1,
core::cmp::Ordering::Equal => 0,
core::cmp::Ordering::Greater => 1,
}
})
.unwrap_or(0)
}
pub fn sort_strings(&self, locale: &str, strings: &[AzString]) -> IcuStringVec {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
IcuStringVec::from(localizer.sorted_strings(strings))
})
.unwrap_or_else(|_| IcuStringVec::from(strings.to_vec()))
}
pub fn strings_equal(&self, locale: &str, a: &str, b: &str) -> bool {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.strings_equal(a, b)
})
.unwrap_or_else(|_| a == b)
}
pub fn get_sort_key(&self, locale: &str, s: &str) -> Vec<u8> {
self.ptr.cache
.lock()
.map(|mut cache| {
let localizer = cache
.entry(locale.to_string())
.or_insert_with(|| IcuLocalizer::new(locale));
localizer.get_sort_key(s)
})
.unwrap_or_default()
}
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.ptr.cache.lock() {
cache.clear();
}
}
pub fn cached_locale_count(&self) -> usize {
self.ptr.cache
.lock()
.map(|cache| cache.len())
.unwrap_or(0)
}
pub fn cached_locales(&self) -> Vec<AzString> {
self.ptr.cache
.lock()
.map(|cache| cache.keys().map(|k| AzString::from(k.clone())).collect())
.unwrap_or_default()
}
}
struct IcuFormattedValue {
value: FmtValue,
localizer: IcuLocalizerHandle,
locale: String,
}
impl strfmt::DisplayStr for IcuFormattedValue {
fn display_str(&self, f: &mut strfmt::Formatter<'_, '_>) -> strfmt::Result<()> {
use strfmt::DisplayStr;
match &self.value {
FmtValue::Uint(v) => {
self.localizer.format_integer(&self.locale, *v as i64).as_str().display_str(f)
}
FmtValue::Sint(v) => {
self.localizer.format_integer(&self.locale, *v as i64).as_str().display_str(f)
}
FmtValue::Ulong(v) => {
self.localizer.format_integer(&self.locale, *v as i64).as_str().display_str(f)
}
FmtValue::Slong(v) => {
self.localizer.format_integer(&self.locale, *v).as_str().display_str(f)
}
FmtValue::Usize(v) => {
self.localizer.format_integer(&self.locale, *v as i64).as_str().display_str(f)
}
FmtValue::Isize(v) => {
self.localizer.format_integer(&self.locale, *v as i64).as_str().display_str(f)
}
FmtValue::Float(v) => {
let int_part = (*v * 100.0) as i64;
self.localizer.format_decimal(&self.locale, int_part, 2).as_str().display_str(f)
}
FmtValue::Double(v) => {
let int_part = (*v * 100.0) as i64;
self.localizer.format_decimal(&self.locale, int_part, 2).as_str().display_str(f)
}
FmtValue::StrVec(sv) => {
let items: Vec<AzString> = sv.as_ref().iter().cloned().collect();
self.localizer.format_list(&self.locale, &items, ListType::And).as_str().display_str(f)
}
FmtValue::Bool(v) => format!("{v:?}").display_str(f),
FmtValue::Uchar(v) => v.display_str(f),
FmtValue::Schar(v) => v.display_str(f),
FmtValue::Ushort(v) => v.display_str(f),
FmtValue::Sshort(v) => v.display_str(f),
FmtValue::Str(v) => v.as_str().display_str(f),
}
}
}
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;
}
impl LayoutCallbackInfoIcuExt for LayoutCallbackInfo {
fn icu_get_locale(&self) -> AzString {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
handle.get_default_locale()
}
fn icu_get_language(&self) -> AzString {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.get_language(locale)
}
fn icu_format_integer(&self, value: i64) -> AzString {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.format_integer(locale, value)
}
fn icu_format_decimal(&self, integer_part: i64, decimal_places: i16) -> AzString {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.format_decimal(locale, integer_part, decimal_places)
}
fn icu_get_plural_category(&self, value: i64) -> PluralCategory {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.get_plural_category(locale, 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();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.pluralize(locale, 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();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.format_list(locale, items, list_type)
}
fn icu_format_date(&self, date: IcuDate, length: FormatLength) -> IcuResult {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.format_date(locale, date, length)
}
fn icu_format_time(&self, time: IcuTime, include_seconds: bool) -> IcuResult {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.format_time(locale, time, include_seconds)
}
fn icu_format_datetime(&self, datetime: IcuDateTime, length: FormatLength) -> IcuResult {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.format_datetime(locale, datetime, length)
}
fn icu_compare_strings(&self, a: &str, b: &str) -> i32 {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.compare_strings(locale, a, b)
}
fn icu_sort_strings(&self, strings: &[AzString]) -> IcuStringVec {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.sort_strings(locale, strings)
}
fn icu_strings_equal(&self, a: &str, b: &str) -> bool {
let system_style = self.get_system_style();
let handle = IcuLocalizerHandle::from_system_language(&system_style.language);
let locale = system_style.language.as_str();
handle.strings_equal(locale, 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);
}
}