Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 36ebbc4

Browse files
committed
i18n: utilities to format short dates and times
1 parent b498e59 commit 36ebbc4

File tree

5 files changed

+296
-0
lines changed

5 files changed

+296
-0
lines changed

Cargo.lock

Lines changed: 151 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/i18n/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ workspace = true
1313

1414
[dependencies]
1515
camino.workspace = true
16+
icu_calendar = { version = "1.4.0", features = ["compiled_data", "std"] }
17+
icu_datetime = { version = "1.4.0", features = ["compiled_data", "std"] }
1618
icu_list = { version = "1.4.0", features = ["compiled_data", "std"] }
1719
icu_locid = { version = "1.4.0", features = ["std",] }
1820
icu_locid_transform = { version = "1.4.0", features = ["compiled_data", "std"] }
1921
icu_plurals = { version = "1.4.0", features = ["compiled_data", "std"] }
2022
icu_provider = { version = "1.4.0", features = ["std", "sync"] }
2123
icu_provider_adapters = { version = "1.4.0", features = ["std"] }
24+
icu_relativetime = { version = "0.1.4", features = ["compiled_data", "std"] }
2225
pad = "0.1.6"
2326
pest = "2.7.6"
2427
pest_derive = "2.7.6"

crates/i18n/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub mod sprintf;
1616
pub mod translations;
1717
mod translator;
1818

19+
pub use icu_calendar;
20+
pub use icu_datetime;
1921
pub use icu_locid::locale;
2022
pub use icu_provider::DataLocale;
2123

crates/i18n/src/translator.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use icu_provider::{
2424
DataRequest, DataRequestMetadata,
2525
};
2626
use icu_provider_adapters::fallback::LocaleFallbackProvider;
27+
use icu_relativetime::{options::Numeric, RelativeTimeFormatter, RelativeTimeFormatterOptions};
2728
use thiserror::Error;
2829
use writeable::Writeable;
2930

@@ -298,6 +299,58 @@ impl Translator {
298299
Ok(list)
299300
}
300301

302+
/// Format a relative date
303+
///
304+
/// # Parameters
305+
///
306+
/// * `locale` - The locale to use.
307+
/// * `days` - The number of days to format, where 0 = today, 1 = tomorrow,
308+
/// -1 = yesterday, etc.
309+
///
310+
/// # Errors
311+
///
312+
/// Returns an error if the requested locale is not found.
313+
pub fn relative_date(
314+
&self,
315+
locale: &DataLocale,
316+
days: i64,
317+
) -> Result<String, icu_relativetime::RelativeTimeError> {
318+
// TODO: this is not using the fallbacker
319+
let formatter = RelativeTimeFormatter::try_new_long_day(
320+
locale,
321+
RelativeTimeFormatterOptions {
322+
numeric: Numeric::Auto,
323+
},
324+
)?;
325+
326+
let date = formatter.format(days.into());
327+
Ok(date.write_to_string().into_owned())
328+
}
329+
330+
/// Format time
331+
///
332+
/// # Parameters
333+
///
334+
/// * `locale` - The locale to use.
335+
/// * `time` - The time to format.
336+
///
337+
/// # Errors
338+
///
339+
/// Returns an error if the requested locale is not found.
340+
pub fn short_time<T: icu_datetime::input::IsoTimeInput>(
341+
&self,
342+
locale: &DataLocale,
343+
time: &T,
344+
) -> Result<String, icu_datetime::DateTimeError> {
345+
// TODO: this is not using the fallbacker
346+
let formatter = icu_datetime::TimeFormatter::try_new_with_length(
347+
locale,
348+
icu_datetime::options::length::Time::Short,
349+
)?;
350+
351+
Ok(formatter.format_to_string(time))
352+
}
353+
301354
/// Get a list of available locales.
302355
#[must_use]
303356
pub fn available_locales(&self) -> Vec<&DataLocale> {

crates/templates/src/functions.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,93 @@ impl Object for TranslateFunc {
325325

326326
Ok(Value::from_safe_string(buf))
327327
}
328+
329+
fn call_method(&self, _state: &State, name: &str, args: &[Value]) -> Result<Value, Error> {
330+
match name {
331+
"relative_date" => {
332+
let (date,): (String,) = from_args(args)?;
333+
let date: chrono::DateTime<chrono::Utc> = date.parse().map_err(|e| {
334+
Error::new(
335+
ErrorKind::InvalidOperation,
336+
"Invalid date while calling function `relative_date`",
337+
)
338+
.with_source(e)
339+
})?;
340+
341+
// TODO: grab the clock somewhere
342+
#[allow(clippy::disallowed_methods)]
343+
let now = chrono::Utc::now();
344+
345+
let diff = (date - now).num_days();
346+
347+
Ok(Value::from(
348+
self.translator
349+
.relative_date(&self.lang, diff)
350+
.map_err(|_e| {
351+
Error::new(
352+
ErrorKind::InvalidOperation,
353+
"Failed to format relative date",
354+
)
355+
})?,
356+
))
357+
}
358+
359+
"short_time" => {
360+
let (date,): (String,) = from_args(args)?;
361+
let date: chrono::DateTime<chrono::Utc> = date.parse().map_err(|e| {
362+
Error::new(
363+
ErrorKind::InvalidOperation,
364+
"Invalid date while calling function `time`",
365+
)
366+
.with_source(e)
367+
})?;
368+
369+
// TODO: we should use the user's timezone here
370+
let time = date.time();
371+
372+
Ok(Value::from(
373+
self.translator
374+
.short_time(&self.lang, &TimeAdapter(time))
375+
.map_err(|_e| {
376+
Error::new(ErrorKind::InvalidOperation, "Failed to format time")
377+
})?,
378+
))
379+
}
380+
381+
_ => Err(Error::new(
382+
ErrorKind::InvalidOperation,
383+
"Invalid method on include_asset",
384+
)),
385+
}
386+
}
387+
}
388+
389+
/// An adapter to make a [`Timelike`] implement [`IsoTimeInput`]
390+
///
391+
/// [`Timelike`]: chrono::Timelike
392+
/// [`IsoTimeInput`]: mas_i18n::icu_datetime::input::IsoTimeInput
393+
struct TimeAdapter<T>(T);
394+
395+
impl<T: chrono::Timelike> mas_i18n::icu_datetime::input::IsoTimeInput for TimeAdapter<T> {
396+
fn hour(&self) -> Option<mas_i18n::icu_calendar::types::IsoHour> {
397+
let hour: usize = chrono::Timelike::hour(&self.0).try_into().ok()?;
398+
hour.try_into().ok()
399+
}
400+
401+
fn minute(&self) -> Option<mas_i18n::icu_calendar::types::IsoMinute> {
402+
let minute: usize = chrono::Timelike::minute(&self.0).try_into().ok()?;
403+
minute.try_into().ok()
404+
}
405+
406+
fn second(&self) -> Option<mas_i18n::icu_calendar::types::IsoSecond> {
407+
let second: usize = chrono::Timelike::second(&self.0).try_into().ok()?;
408+
second.try_into().ok()
409+
}
410+
411+
fn nanosecond(&self) -> Option<mas_i18n::icu_calendar::types::NanoSecond> {
412+
let nanosecond: usize = chrono::Timelike::nanosecond(&self.0).try_into().ok()?;
413+
nanosecond.try_into().ok()
414+
}
328415
}
329416

330417
struct IncludeAsset {

0 commit comments

Comments
 (0)