use crate::{Locale, TextDirection};
use icu_datetime::{DateTimeFormatter, fieldsets, input::Date};
use icu_decimal::{DecimalFormatter, input::Decimal};
use icu_locale::{Direction, LocaleDirectionality, fallback::LocaleFallbacker};
pub use icu_plurals::PluralCategory;
use icu_plurals::PluralRules;
use icu_provider::DataLocale;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct LocalizationError {
message: String,
}
impl LocalizationError {
fn from_display(error: impl fmt::Display) -> Self {
Self {
message: error.to_string(),
}
}
}
pub trait LocalizationCatalog {
fn supported_locales(&self) -> &[Locale];
fn message(&self, locale: &Locale, key: &str) -> Option<&str>;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LocalizedMessage<'a> {
pub locale: Locale,
pub value: &'a str,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PluralForms<'a> {
pub zero: Option<&'a str>,
pub one: Option<&'a str>,
pub two: Option<&'a str>,
pub few: Option<&'a str>,
pub many: Option<&'a str>,
pub other: &'a str,
}
impl<'a> PluralForms<'a> {
#[must_use]
pub fn for_category(self, category: PluralCategory) -> &'a str {
match category {
PluralCategory::Zero => self.zero.unwrap_or(self.other),
PluralCategory::One => self.one.unwrap_or(self.other),
PluralCategory::Two => self.two.unwrap_or(self.other),
PluralCategory::Few => self.few.unwrap_or(self.other),
PluralCategory::Many => self.many.unwrap_or(self.other),
PluralCategory::Other => self.other,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct LocaleResolver;
impl LocaleResolver {
#[must_use]
pub fn resolve(preferred: &[Locale], supported: &[Locale]) -> Option<Locale> {
for preferred_locale in preferred {
let mut fallback = LocaleFallbacker::new()
.for_config(Default::default())
.fallback_for(DataLocale::from(preferred_locale));
loop {
let candidate = *fallback.get();
if let Some(supported_locale) = supported
.iter()
.find(|supported_locale| DataLocale::from(*supported_locale) == candidate)
{
return Some(supported_locale.clone());
}
if candidate == DataLocale::default() {
break;
}
fallback.step();
}
}
supported.first().cloned()
}
#[must_use]
pub fn resolve_message<'a>(
preferred: &[Locale],
catalog: &'a dyn LocalizationCatalog,
key: &str,
) -> Option<LocalizedMessage<'a>> {
let locale = Self::resolve(preferred, catalog.supported_locales())?;
let value = catalog.message(&locale, key)?;
Some(LocalizedMessage { locale, value })
}
#[must_use]
pub fn text_direction(locale: &Locale) -> TextDirection {
match LocaleDirectionality::new_extended().get(&locale.id) {
Some(Direction::RightToLeft) => TextDirection::Rtl,
Some(Direction::LeftToRight) | Some(_) | None => TextDirection::Ltr,
}
}
pub fn format_integer(locale: &Locale, value: i64) -> Result<String, LocalizationError> {
let formatter = DecimalFormatter::try_new(locale.clone().into(), Default::default())
.map_err(LocalizationError::from_display)?;
Ok(formatter.format(&Decimal::from(value)).to_string())
}
pub fn format_iso_date(
locale: &Locale,
year: i32,
month: u8,
day: u8,
) -> Result<String, LocalizationError> {
let formatter = DateTimeFormatter::try_new(locale.clone().into(), fieldsets::YMD::medium())
.map_err(LocalizationError::from_display)?;
let date = Date::try_new_iso(year, month, day).map_err(LocalizationError::from_display)?;
Ok(formatter.format(&date).to_string())
}
pub fn plural_category(
locale: &Locale,
value: i64,
) -> Result<PluralCategory, LocalizationError> {
let rules = PluralRules::try_new_cardinal(locale.clone().into())
.map_err(LocalizationError::from_display)?;
Ok(rules.category_for(value))
}
pub fn select_plural<'a>(
locale: &Locale,
value: i64,
forms: PluralForms<'a>,
) -> Result<&'a str, LocalizationError> {
Ok(forms.for_category(Self::plural_category(locale, value)?))
}
}