From 36ebbc4d7086caaa25e5ec79c3253da96cd478e8 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 1 Feb 2024 18:06:38 +0100 Subject: [PATCH] i18n: utilities to format short dates and times --- Cargo.lock | 151 ++++++++++++++++++++++++++++++ crates/i18n/Cargo.toml | 3 + crates/i18n/src/lib.rs | 2 + crates/i18n/src/translator.rs | 53 +++++++++++ crates/templates/src/functions.rs | 87 +++++++++++++++++ 5 files changed, 296 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f60b4dcc..195da034 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -820,6 +820,16 @@ dependencies = [ "serde", ] +[[package]] +name = "calendrical_calculations" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dfe3bc6a50b4667fafdb6d9cf26731c5418c457e317d8166c972014facf9a5d" +dependencies = [ + "core_maths", + "displaydoc", +] + [[package]] name = "camino" version = "1.1.6" @@ -1075,6 +1085,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core_maths" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +dependencies = [ + "libm", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -2293,6 +2312,79 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_calendar" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb932a690c92f87955e923106181ee0d5682e688ff37fb5c7b296e1fe806edb" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locid", + "icu_locid_transform", + "icu_provider", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22aec7d032735d9acb256eeef72adcac43c3b7572f19b51576a63d664b524ca2" + +[[package]] +name = "icu_datetime" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508c7ed627cc0b031c81203eb98f34433e24b32b39d5b2c0238e4962a00957d" +dependencies = [ + "displaydoc", + "either", + "fixed_decimal", + "icu_calendar", + "icu_datetime_data", + "icu_decimal", + "icu_locid", + "icu_locid_transform", + "icu_plurals", + "icu_provider", + "icu_timezone", + "smallvec", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_datetime_data" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6abc569cb4ee80b30707566f05c5c9ed4bed765f91ce41e7f5a37c5e6a75b3f" + +[[package]] +name = "icu_decimal" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf994f9ed8061c17bb313f28fba6cffc736f0a16c7fab827efc9b73fd3f7778" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locid", + "icu_locid_transform", + "icu_provider", + "writeable", +] + +[[package]] +name = "icu_decimal_data" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2de3548316b697c70f30dec1395c9212db09df1d86a27624ee24872b71326c" + [[package]] name = "icu_list" version = "1.4.0" @@ -2408,6 +2500,51 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "icu_relativetime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47825312a5eb0790bad7b718fa8d41a8ea1e0ba597b4f7bb84bcfe97d7fc5aba" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal", + "icu_locid_transform", + "icu_plurals", + "icu_provider", + "icu_relativetime_data", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_relativetime_data" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b55cc15ea8981fbba78e9347d0c4003d4490c85f76e9adc7f270290046cae8" + +[[package]] +name = "icu_timezone" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35aabe571a7c653c0f543ff1512b8a1b2ad481cfa24b3d25115298d2ff3b50f" +dependencies = [ + "displaydoc", + "icu_calendar", + "icu_locid", + "icu_provider", + "icu_timezone_data", + "tinystr", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_timezone_data" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceee21e181cce2ab44e95923da6b3418df75369f570df82264c29c51ca398d4" + [[package]] name = "id-arena" version = "2.2.1" @@ -3072,12 +3209,15 @@ name = "mas-i18n" version = "0.7.0" dependencies = [ "camino", + "icu_calendar", + "icu_datetime", "icu_list", "icu_locid", "icu_locid_transform", "icu_plurals", "icu_provider", "icu_provider_adapters", + "icu_relativetime", "pad", "pest", "pest_derive", @@ -7149,6 +7289,17 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +[[package]] +name = "zerotrie" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0594125a0574fb93059c92c588ab209cc036a23d1baeb3410fa9181bea551a0" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" version = "0.10.1" diff --git a/crates/i18n/Cargo.toml b/crates/i18n/Cargo.toml index 087b2ff7..c9754c76 100644 --- a/crates/i18n/Cargo.toml +++ b/crates/i18n/Cargo.toml @@ -13,12 +13,15 @@ workspace = true [dependencies] camino.workspace = true +icu_calendar = { version = "1.4.0", features = ["compiled_data", "std"] } +icu_datetime = { version = "1.4.0", features = ["compiled_data", "std"] } icu_list = { version = "1.4.0", features = ["compiled_data", "std"] } icu_locid = { version = "1.4.0", features = ["std",] } icu_locid_transform = { version = "1.4.0", features = ["compiled_data", "std"] } icu_plurals = { version = "1.4.0", features = ["compiled_data", "std"] } icu_provider = { version = "1.4.0", features = ["std", "sync"] } icu_provider_adapters = { version = "1.4.0", features = ["std"] } +icu_relativetime = { version = "0.1.4", features = ["compiled_data", "std"] } pad = "0.1.6" pest = "2.7.6" pest_derive = "2.7.6" diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs index 453bd8a3..c2c5866c 100644 --- a/crates/i18n/src/lib.rs +++ b/crates/i18n/src/lib.rs @@ -16,6 +16,8 @@ pub mod sprintf; pub mod translations; mod translator; +pub use icu_calendar; +pub use icu_datetime; pub use icu_locid::locale; pub use icu_provider::DataLocale; diff --git a/crates/i18n/src/translator.rs b/crates/i18n/src/translator.rs index 60fa1d5a..5523a9ad 100644 --- a/crates/i18n/src/translator.rs +++ b/crates/i18n/src/translator.rs @@ -24,6 +24,7 @@ use icu_provider::{ DataRequest, DataRequestMetadata, }; use icu_provider_adapters::fallback::LocaleFallbackProvider; +use icu_relativetime::{options::Numeric, RelativeTimeFormatter, RelativeTimeFormatterOptions}; use thiserror::Error; use writeable::Writeable; @@ -298,6 +299,58 @@ impl Translator { Ok(list) } + /// Format a relative date + /// + /// # Parameters + /// + /// * `locale` - The locale to use. + /// * `days` - The number of days to format, where 0 = today, 1 = tomorrow, + /// -1 = yesterday, etc. + /// + /// # Errors + /// + /// Returns an error if the requested locale is not found. + pub fn relative_date( + &self, + locale: &DataLocale, + days: i64, + ) -> Result { + // TODO: this is not using the fallbacker + let formatter = RelativeTimeFormatter::try_new_long_day( + locale, + RelativeTimeFormatterOptions { + numeric: Numeric::Auto, + }, + )?; + + let date = formatter.format(days.into()); + Ok(date.write_to_string().into_owned()) + } + + /// Format time + /// + /// # Parameters + /// + /// * `locale` - The locale to use. + /// * `time` - The time to format. + /// + /// # Errors + /// + /// Returns an error if the requested locale is not found. + pub fn short_time( + &self, + locale: &DataLocale, + time: &T, + ) -> Result { + // TODO: this is not using the fallbacker + let formatter = icu_datetime::TimeFormatter::try_new_with_length( + locale, + icu_datetime::options::length::Time::Short, + )?; + + Ok(formatter.format_to_string(time)) + } + /// Get a list of available locales. #[must_use] pub fn available_locales(&self) -> Vec<&DataLocale> { diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index b80a8427..9b26b868 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -325,6 +325,93 @@ impl Object for TranslateFunc { Ok(Value::from_safe_string(buf)) } + + fn call_method(&self, _state: &State, name: &str, args: &[Value]) -> Result { + match name { + "relative_date" => { + let (date,): (String,) = from_args(args)?; + let date: chrono::DateTime = date.parse().map_err(|e| { + Error::new( + ErrorKind::InvalidOperation, + "Invalid date while calling function `relative_date`", + ) + .with_source(e) + })?; + + // TODO: grab the clock somewhere + #[allow(clippy::disallowed_methods)] + let now = chrono::Utc::now(); + + let diff = (date - now).num_days(); + + Ok(Value::from( + self.translator + .relative_date(&self.lang, diff) + .map_err(|_e| { + Error::new( + ErrorKind::InvalidOperation, + "Failed to format relative date", + ) + })?, + )) + } + + "short_time" => { + let (date,): (String,) = from_args(args)?; + let date: chrono::DateTime = date.parse().map_err(|e| { + Error::new( + ErrorKind::InvalidOperation, + "Invalid date while calling function `time`", + ) + .with_source(e) + })?; + + // TODO: we should use the user's timezone here + let time = date.time(); + + Ok(Value::from( + self.translator + .short_time(&self.lang, &TimeAdapter(time)) + .map_err(|_e| { + Error::new(ErrorKind::InvalidOperation, "Failed to format time") + })?, + )) + } + + _ => Err(Error::new( + ErrorKind::InvalidOperation, + "Invalid method on include_asset", + )), + } + } +} + +/// An adapter to make a [`Timelike`] implement [`IsoTimeInput`] +/// +/// [`Timelike`]: chrono::Timelike +/// [`IsoTimeInput`]: mas_i18n::icu_datetime::input::IsoTimeInput +struct TimeAdapter(T); + +impl mas_i18n::icu_datetime::input::IsoTimeInput for TimeAdapter { + fn hour(&self) -> Option { + let hour: usize = chrono::Timelike::hour(&self.0).try_into().ok()?; + hour.try_into().ok() + } + + fn minute(&self) -> Option { + let minute: usize = chrono::Timelike::minute(&self.0).try_into().ok()?; + minute.try_into().ok() + } + + fn second(&self) -> Option { + let second: usize = chrono::Timelike::second(&self.0).try_into().ok()?; + second.try_into().ok() + } + + fn nanosecond(&self) -> Option { + let nanosecond: usize = chrono::Timelike::nanosecond(&self.0).try_into().ok()?; + nanosecond.try_into().ok() + } } struct IncludeAsset {