#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::borrow::Cow;
use std::mem::discriminant;
use std::sync::LazyLock;
use fluent_bundle::bundle::FluentBundle;
use fluent_bundle::types::FluentType;
use fluent_bundle::{FluentArgs, FluentError, FluentValue};
use icu_calendar::{Gregorian, Iso};
use icu_datetime::fieldsets;
use icu_time::{DateTime, ZonedDateTime};
pub mod length;
fn val_as_str<'a>(val: &'a FluentValue) -> Option<&'a str> {
if let FluentValue::String(str) = val {
Some(str)
} else {
None
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FluentDateTimeOptions {
length: length::Bag,
}
impl Default for FluentDateTimeOptions {
fn default() -> Self {
Self {
length: length::Bag::empty(),
}
}
}
impl FluentDateTimeOptions {
pub fn set_date_style(&mut self, style: Option<length::Date>) {
self.length.date = style;
}
pub fn set_time_style(&mut self, style: Option<length::Time>) {
self.length.time = style;
}
fn make_formatter(
&self,
langid: icu_locale_core::LanguageIdentifier,
) -> Result<DateTimeFormatter, icu_datetime::DateTimeFormatterLoadError> {
let fsb = self.length.to_fieldset_builder();
let formatter_prefs = langid.into();
Ok(if fsb.zone_style.is_some() {
DateTimeFormatter::WithZone(icu_datetime::DateTimeFormatter::try_new(
formatter_prefs,
fsb.build_composite().unwrap(),
)?)
} else {
DateTimeFormatter::NoZone(icu_datetime::DateTimeFormatter::try_new(
formatter_prefs,
fsb.build_composite_datetime().unwrap(),
)?)
})
}
fn merge_args(&mut self, other: &FluentArgs) -> Result<(), ()> {
for (k, v) in other.iter() {
match k {
"dateStyle" => {
self.length.date = Some(match val_as_str(v).ok_or(())? {
"full" => length::Date::Full,
"long" => length::Date::Long,
"medium" => length::Date::Medium,
"short" => length::Date::Short,
_ => return Err(()),
});
}
"timeStyle" => {
self.length.time = Some(match val_as_str(v).ok_or(())? {
"full" => length::Time::Full,
"long" => length::Time::Long,
"medium" => length::Time::Medium,
"short" => length::Time::Short,
_ => return Err(()),
});
}
_ => (), }
}
Ok(())
}
}
impl std::hash::Hash for FluentDateTimeOptions {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.length.date.map(|e| discriminant(&e)).hash(state);
self.length.time.map(|e| discriminant(&e)).hash(state);
}
}
impl Eq for FluentDateTimeOptions {}
#[derive(Debug, Clone, PartialEq)]
pub struct FluentDateTime {
value: DateTime<Iso>,
pub options: FluentDateTimeOptions,
}
impl FluentType for FluentDateTime {
fn duplicate(&self) -> Box<dyn FluentType + Send> {
Box::new(self.clone())
}
fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
intls
.with_try_get::<DateTimeFormatter, _, _>(self.options.clone(), |dtf| {
dtf.format(&self.value).to_string()
})
.unwrap_or_default()
.into()
}
fn as_string_threadsafe(
&self,
intls: &intl_memoizer::concurrent::IntlLangMemoizer,
) -> Cow<'static, str> {
let lang = intls
.with_try_get::<GimmeTheLocale, _, _>((), |gimme| gimme.0.clone())
.expect("Infallible");
let Some(langid): Option<icu_locale_core::LanguageIdentifier> =
lang.to_string().parse().ok()
else {
return "".into();
};
let Ok(dtf) = self.options.make_formatter(langid) else {
return "".into();
};
dtf.format(&self.value).to_string().into()
}
}
impl From<DateTime<Gregorian>> for FluentDateTime {
fn from(value: DateTime<Gregorian>) -> Self {
Self {
value: DateTime {
date: value.date.to_iso(),
time: value.time,
},
options: Default::default(),
}
}
}
impl From<DateTime<Iso>> for FluentDateTime {
fn from(value: DateTime<Iso>) -> Self {
Self {
value,
options: Default::default(),
}
}
}
impl From<FluentDateTime> for FluentValue<'static> {
fn from(value: FluentDateTime) -> Self {
Self::Custom(Box::new(value))
}
}
static SYSTEM_TZ: LazyLock<jiff::tz::TimeZone> = LazyLock::new(|| jiff::tz::TimeZone::system());
fn clamp_datetime_for_jiff(dt: &DateTime<Iso>) -> Cow<'_, DateTime<Iso>> {
if dt.time.second < 60u8.try_into().unwrap() {
Cow::Borrowed(dt)
} else {
let mut dt = dt.clone();
dt.time.second = 59u8.try_into().unwrap();
dt.time.subsecond = 999_999_999u32.try_into().unwrap();
Cow::Owned(dt)
}
}
fn naive_datetime_to_system(
dt: &DateTime<Iso>,
) -> ZonedDateTime<Iso, icu_time::TimeZoneInfo<icu_time::zone::models::AtTime>> {
let jdt = jiff_icu::ConvertTryInto::<jiff::civil::DateTime>::convert_try_into(
*clamp_datetime_for_jiff(dt),
)
.unwrap();
jiff_icu::ConvertInto::convert_into(&jdt.to_zoned(SYSTEM_TZ.to_owned()).unwrap())
}
enum DateTimeFormatter {
WithZone(
icu_datetime::DateTimeFormatter<fieldsets::enums::CompositeFieldSet>,
),
NoZone(icu_datetime::DateTimeFormatter<fieldsets::enums::CompositeDateTimeFieldSet>),
}
impl DateTimeFormatter {
fn format(&self, dt: &DateTime<Iso>) -> icu_datetime::FormattedDateTime<'_> {
match self {
Self::WithZone(dtf) => dtf.format(&naive_datetime_to_system(dt)),
Self::NoZone(dtf) => dtf.format(dt),
}
}
}
impl intl_memoizer::Memoizable for DateTimeFormatter {
type Args = FluentDateTimeOptions;
type Error = ();
fn construct(
lang: unic_langid::LanguageIdentifier,
args: Self::Args,
) -> Result<Self, Self::Error>
where
Self: std::marker::Sized,
{
let langid: icu_locale_core::LanguageIdentifier =
lang.to_string().parse().map_err(|_| ())?;
args.make_formatter(langid).map_err(|_| ())
}
}
struct GimmeTheLocale(unic_langid::LanguageIdentifier);
impl intl_memoizer::Memoizable for GimmeTheLocale {
type Args = ();
type Error = std::convert::Infallible;
fn construct(lang: unic_langid::LanguageIdentifier, _args: ()) -> Result<Self, Self::Error>
where
Self: std::marker::Sized,
{
Ok(Self(lang))
}
}
#[allow(non_snake_case)]
pub fn DATETIME<'a>(positional: &[FluentValue<'a>], named: &FluentArgs) -> FluentValue<'a> {
match positional.first() {
Some(FluentValue::Custom(cus)) => {
if let Some(dt) = cus.as_any().downcast_ref::<FluentDateTime>() {
let mut dt = dt.clone();
let Ok(()) = dt.options.merge_args(named) else {
return FluentValue::Error;
};
FluentValue::Custom(Box::new(dt))
} else {
FluentValue::Error
}
}
_ => FluentValue::Error,
}
}
pub trait BundleExt {
fn add_datetime_support(&mut self) -> Result<(), FluentError>;
}
impl<R, M> BundleExt for FluentBundle<R, M> {
fn add_datetime_support(&mut self) -> Result<(), FluentError> {
self.add_function("DATETIME", DATETIME)?;
Ok(())
}
}