fluent_datetime/lib.rs
1//! # International datetimes in Fluent translations
2//!
3//! fluent-datetime uses [ICU4X], in particular [`icu_datetime`] and
4//! [`icu_calendar`], to format datetimes internationally within
5//! a [Fluent] translation.
6//!
7//! [Fluent]: https://projectfluent.org/
8//! [ICU4X]: https://github.com/unicode-org/icu4x
9//!
10//! # Example
11//!
12//! This example uses [`fluent_bundle`] directly.
13//!
14//! You may prefer to use less verbose integrations; in which case the
15//! [`bundle.add_datetime_support()`](BundleExt::add_datetime_support)
16//! line is the only one you need.
17//!
18//! ```rust
19//! use fluent::fluent_args;
20//! use fluent_bundle::{FluentBundle, FluentResource};
21//! use fluent_datetime::{BundleExt, FluentDateTime};
22//! use fluent_datetime::length;
23//! use icu_calendar::Iso;
24//! use icu_time::DateTime;
25//! use unic_langid::LanguageIdentifier;
26//!
27//! // Create a FluentBundle
28//! let langid_en: LanguageIdentifier = "en-US".parse()?;
29//! let mut bundle = FluentBundle::new(vec![langid_en]);
30//!
31//! // Register the DATETIME function
32//! bundle.add_datetime_support();
33//!
34//! // Add a FluentResource to the bundle
35//! let ftl_string = r#"
36//! today-is = Today is {$date}
37//! today-is-fulldate = Today is {DATETIME($date, dateStyle: "full")}
38//! now-is-time = Now is {DATETIME($date, timeStyle: "medium")}
39//! now-is-longtime = Now is {DATETIME($date, timeStyle: "long")}
40//! now-is-datetime = Now is {DATETIME($date, dateStyle: "full", timeStyle: "short")}
41//! "#
42//! .to_string();
43//!
44//! let res = FluentResource::try_new(ftl_string)
45//! .expect("Failed to parse an FTL string.");
46//! bundle
47//! .add_resource(res)
48//! .expect("Failed to add FTL resources to the bundle.");
49//!
50//! // Create an ICU DateTime
51//! let datetime = DateTime::try_from_str("1989-11-09 23:30", Iso)
52//! .expect("Failed to create ICU DateTime");
53//!
54//! // Convert to FluentDateTime
55//! let mut datetime = FluentDateTime::from(datetime);
56//!
57//! // Format some messages with date arguments
58//! let mut errors = vec![];
59//!
60//! assert_eq!(
61//! bundle.format_pattern(
62//! &bundle.get_message("today-is").unwrap().value().unwrap(),
63//! Some(&fluent_args!("date" => datetime.clone())), &mut errors),
64//! "Today is \u{2068}11/9/89\u{2069}"
65//! );
66//!
67//! assert_eq!(
68//! bundle.format_pattern(
69//! &bundle.get_message("today-is-fulldate").unwrap().value().unwrap(),
70//! Some(&fluent_args!("date" => datetime.clone())), &mut errors),
71//! "Today is \u{2068}Thursday, November 9, 1989\u{2069}"
72//! );
73//!
74//! assert_eq!(
75//! bundle.format_pattern(
76//! &bundle.get_message("now-is-time").unwrap().value().unwrap(),
77//! Some(&fluent_args!("date" => datetime.clone())), &mut errors),
78//! "Now is \u{2068}11:30:00\u{202f}PM\u{2069}"
79//! );
80//!
81//! assert!(
82//! bundle.format_pattern(
83//! &bundle.get_message("now-is-longtime").unwrap().value().unwrap(),
84//! Some(&fluent_args!("date" => datetime.clone())), &mut errors).starts_with(
85//! "Now is \u{2068}11:30:00\u{202f}PM ")
86//! );
87//!
88//! assert_eq!(
89//! bundle.format_pattern(
90//! &bundle.get_message("now-is-datetime").unwrap().value().unwrap(),
91//! Some(&fluent_args!("date" => datetime.clone())), &mut errors),
92//! "Now is \u{2068}Thursday, November 9, 1989 at 11:30\u{202f}PM\u{2069}"
93//! );
94//!
95//! // Set FluentDateTime.options in code rather than in translation data
96//! // This is useful because it sets presentation options that are
97//! // shared between all locales
98//! datetime.options.set_date_style(Some(length::Date::Full));
99//! assert_eq!(
100//! bundle.format_pattern(
101//! &bundle.get_message("today-is").unwrap().value().unwrap(),
102//! Some(&fluent_args!("date" => datetime)), &mut errors),
103//! "Today is \u{2068}Thursday, November 9, 1989\u{2069}"
104//! );
105//!
106//! assert!(errors.is_empty());
107//!
108//! # // I would like to use the ? operator, but Fluent and ICU error types don't implement the std Error trait…
109//! # Ok::<(), Box<dyn std::error::Error>>(())
110//! ```
111#![forbid(unsafe_code)]
112#![warn(missing_docs)]
113use std::borrow::Cow;
114use std::mem::discriminant;
115use std::sync::LazyLock;
116
117use fluent_bundle::bundle::FluentBundle;
118use fluent_bundle::types::FluentType;
119use fluent_bundle::{FluentArgs, FluentError, FluentValue};
120
121use icu_calendar::{Gregorian, Iso};
122use icu_datetime::fieldsets;
123use icu_time::{DateTime, ZonedDateTime};
124
125pub mod length;
126
127fn val_as_str<'a>(val: &'a FluentValue) -> Option<&'a str> {
128 if let FluentValue::String(str) = val {
129 Some(str)
130 } else {
131 None
132 }
133}
134
135/// Options for formatting a DateTime
136#[derive(Debug, Clone, PartialEq)]
137pub struct FluentDateTimeOptions {
138 // See AnyCalendarKind::new if we want to expose explicit calendar choice
139 //calendar: Option<icu_calendar::AnyCalendarKind>,
140 // We don't handle icu_datetime per-component settings atm, it is experimental
141 // and length is expressive enough so far
142 length: length::Bag,
143}
144
145impl Default for FluentDateTimeOptions {
146 /// Defaults to showing a short date
147 ///
148 /// The intent is to emulate [Intl.DateTimeFormat] behavior:
149 /// > The default value for each date-time component option is undefined,
150 /// > but if all component properties are undefined, then year, month, and day default
151 /// > to "numeric". If any of the date-time component options is specified, then
152 /// > dateStyle and timeStyle must be undefined.
153 ///
154 /// In terms of the current Rust implementation:
155 ///
156 /// The default value for each date-time style option is None, but if both
157 /// are unset, we display the date only, using the `length::Date::Short`
158 /// style.
159 ///
160 /// [Intl.DateTimeFormat]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
161 fn default() -> Self {
162 Self {
163 length: length::Bag::empty(),
164 }
165 }
166}
167
168impl FluentDateTimeOptions {
169 /// Set a date style, from verbose to compact
170 ///
171 /// See [`length::Date`].
172 pub fn set_date_style(&mut self, style: Option<length::Date>) {
173 self.length.date = style;
174 }
175
176 /// Set a time style, from verbose to compact
177 ///
178 /// See [`length::Time`].
179 pub fn set_time_style(&mut self, style: Option<length::Time>) {
180 self.length.time = style;
181 }
182
183 fn make_formatter(
184 &self,
185 langid: icu_locale_core::LanguageIdentifier,
186 ) -> Result<DateTimeFormatter, icu_datetime::DateTimeFormatterLoadError> {
187 let fsb = self.length.to_fieldset_builder();
188 let formatter_prefs = langid.into();
189 // build_().unwrap(): If we set any incompatible options, it's a bug
190 Ok(if fsb.zone_style.is_some() {
191 DateTimeFormatter::WithZone(icu_datetime::DateTimeFormatter::try_new(
192 formatter_prefs,
193 fsb.build_composite().unwrap(),
194 )?)
195 } else {
196 DateTimeFormatter::NoZone(icu_datetime::DateTimeFormatter::try_new(
197 formatter_prefs,
198 fsb.build_composite_datetime().unwrap(),
199 )?)
200 })
201 }
202
203 fn merge_args(&mut self, other: &FluentArgs) -> Result<(), ()> {
204 // TODO set an err state on self to match fluent-js behaviour
205 for (k, v) in other.iter() {
206 match k {
207 "dateStyle" => {
208 self.length.date = Some(match val_as_str(v).ok_or(())? {
209 "full" => length::Date::Full,
210 "long" => length::Date::Long,
211 "medium" => length::Date::Medium,
212 "short" => length::Date::Short,
213 _ => return Err(()),
214 });
215 }
216 "timeStyle" => {
217 self.length.time = Some(match val_as_str(v).ok_or(())? {
218 "full" => length::Time::Full,
219 "long" => length::Time::Long,
220 "medium" => length::Time::Medium,
221 "short" => length::Time::Short,
222 _ => return Err(()),
223 });
224 }
225 _ => (), // Ignore with no warning
226 }
227 }
228 Ok(())
229 }
230}
231
232impl std::hash::Hash for FluentDateTimeOptions {
233 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
234 // We could also use serde… or send a simple PR to have derive(Hash) upstream
235 //self.calendar.hash(state);
236 self.length.date.map(|e| discriminant(&e)).hash(state);
237 self.length.time.map(|e| discriminant(&e)).hash(state);
238 }
239}
240
241impl Eq for FluentDateTimeOptions {}
242
243/// An ICU [`DateTime`](icu_time::DateTime) with attached formatting options
244///
245/// Construct from an [`icu_time::DateTime`] using From / Into.
246///
247/// Convert to a [`FluentValue`] with From / Into.
248///
249/// See [`FluentDateTimeOptions`] and [`FluentDateTimeOptions::default`].
250///
251///```
252/// use icu_time::DateTime;
253/// use icu_calendar::Iso;
254/// use fluent_datetime::FluentDateTime;
255///
256/// let datetime = DateTime::try_from_str("1989-11-09 23:30", Iso)
257/// .expect("Failed to create ICU DateTime");
258///
259/// let datetime = FluentDateTime::from(datetime);
260// ```
261#[derive(Debug, Clone, PartialEq)]
262pub struct FluentDateTime {
263 // Iso seemed like a natural default, but [AnyCalendarKind::new]
264 // loads Gregorian in almost all cases. Differences have to do with eras:
265 // proleptic Gregorian has BCE / CE and no year zero, Iso has just one continuous era,
266 // containing year zero (astronomical year numbering)
267 // OTOH, DateTime<Gregorian> does not implement PartialEq and with Iso it does
268
269 // long/full timeStyles will use zone info, forcing us into a ZonedDateTime
270 // On the other hand, [DateTimeFormat.format] explicitly rejects Temporal.ZonedDateTime
271 // https://github.com/tc39/proposal-temporal/blob/514c656854e5ceab4932cfc23ace0f84ca1f6431/meetings/agenda-minutes-2023-03-16.md#zoneddatetime-in-intldatetimeformatformat-2479
272 //
273 // JS Date doesn't carry a time zone, and formatting is implicitly done in
274 // the local time zone.
275 // Temporal.Now.timeZoneId()
276 // new Intl.DateTimeFormat().resolvedOptions().timeZone
277 //
278 // https://tc39.es/ecma402/#sec-createdatetimeformat
279 // Which initializes an internal TimeZone field
280 //
281 // So now we need a dependency on the system time zone.
282 // jiff rolls its own (and is lighter on dependencies and build time;
283 // see windows-sys vs windows-core). It also handles the TZ env var.
284 // Most other crates (chrono, temporal_rs) depend on iana-time-zone
285 // to figure out the system time zone.
286 //
287 // TZ=Europe/Paris deno eval --unstable-temporal --print 'Temporal.Now.timeZoneId()'
288 // TZ=Europe/Berlin deno eval --print 'new Intl.DateTimeFormat("en-US", {dateStyle: "long", timeStyle: "long"}).format(new Date(1989, 11, 9))'
289 //
290 // DateTimeFormat.format: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/format
291 value: DateTime<Iso>,
292 /// Options for rendering
293 pub options: FluentDateTimeOptions,
294}
295
296impl FluentType for FluentDateTime {
297 fn duplicate(&self) -> Box<dyn FluentType + Send> {
298 // Basically Clone
299 Box::new(self.clone())
300 }
301
302 fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
303 intls
304 .with_try_get::<DateTimeFormatter, _, _>(self.options.clone(), |dtf| {
305 dtf.format(&self.value).to_string()
306 })
307 .unwrap_or_default()
308 .into()
309 }
310
311 fn as_string_threadsafe(
312 &self,
313 intls: &intl_memoizer::concurrent::IntlLangMemoizer,
314 ) -> Cow<'static, str> {
315 // Maybe don't try to cache formatters in this case, the traits don't work out
316 let lang = intls
317 .with_try_get::<GimmeTheLocale, _, _>((), |gimme| gimme.0.clone())
318 .expect("Infallible");
319 let Some(langid): Option<icu_locale_core::LanguageIdentifier> =
320 lang.to_string().parse().ok()
321 else {
322 return "".into();
323 };
324 let Ok(dtf) = self.options.make_formatter(langid) else {
325 return "".into();
326 };
327 dtf.format(&self.value).to_string().into()
328 }
329}
330
331impl From<DateTime<Gregorian>> for FluentDateTime {
332 fn from(value: DateTime<Gregorian>) -> Self {
333 // Not using ConvertCalendar because it would introduce DateTime<Ref<AnyCalendar>> and we don't need ref indirection
334 Self {
335 value: DateTime {
336 date: value.date.to_iso(),
337 time: value.time,
338 },
339 options: Default::default(),
340 }
341 }
342}
343
344impl From<DateTime<Iso>> for FluentDateTime {
345 fn from(value: DateTime<Iso>) -> Self {
346 Self {
347 value,
348 options: Default::default(),
349 }
350 }
351}
352
353impl From<FluentDateTime> for FluentValue<'static> {
354 fn from(value: FluentDateTime) -> Self {
355 Self::Custom(Box::new(value))
356 }
357}
358
359static SYSTEM_TZ: LazyLock<jiff::tz::TimeZone> = LazyLock::new(|| jiff::tz::TimeZone::system());
360
361fn clamp_datetime_for_jiff(dt: &DateTime<Iso>) -> Cow<'_, DateTime<Iso>> {
362 if dt.time.second < 60u8.try_into().unwrap() {
363 Cow::Borrowed(dt)
364 } else {
365 let mut dt = dt.clone();
366 dt.time.second = 59u8.try_into().unwrap();
367 dt.time.subsecond = 999_999_999u32.try_into().unwrap();
368 Cow::Owned(dt)
369 }
370}
371
372fn naive_datetime_to_system(
373 dt: &DateTime<Iso>,
374) -> ZonedDateTime<Iso, icu_time::TimeZoneInfo<icu_time::zone::models::AtTime>> {
375 // There are ambiguities and invalidities to handle during DST transitions
376 // naive datetimes that don't in fact exist (winter -> summer)
377 // naive datetimes that can refer to two instants (summer -> winter)
378 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime#ambiguity_and_gaps_from_local_time_to_utc_time
379 // When constructing a ZonedDateTime from a local time, the behavior for
380 // ambiguity and gaps is configurable via the disambiguation option:
381 //
382 // earlier
383 //
384 // If there are two possible instants, choose the earlier one. If there is
385 // a gap, go back by the gap duration.
386 //
387 // later
388 //
389 // If there are two possible instants, choose the later one. If there is a
390 // gap, go forward by the gap duration.
391 //
392 // compatible (default)
393 //
394 // Same behavior as Date: use later for gaps and earlier for ambiguities.
395 // If there are two possible instants, choose the earlier one.
396 // If there is a gap, go forward by the gap duration.
397 //
398 // There are also offset ambiguities, which we don't have to worry about because
399 // naive datetimes are offset free.
400 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/ZonedDateTime#offset_ambiguity
401 // timeStyles long and full map to SpecificShort and SpecificLong,
402 // but they still need the full AtTime model
403 //
404 // jiff to_zoned uses the "compatible" strategy so we can just do the conversion
405 // on the jiff side
406
407 // Errors can come from
408 // https://docs.rs/jiff/latest/jiff/civil/struct.Date.html#method.new
409 // https://docs.rs/jiff/latest/jiff/civil/struct.Time.html#method.new
410 // and are due to invalid ranges
411 // I find it dodgy that jiff doesn't clamp leap seconds here; we'll do it ourselves
412 let jdt = jiff_icu::ConvertTryInto::<jiff::civil::DateTime>::convert_try_into(
413 *clamp_datetime_for_jiff(dt),
414 )
415 .unwrap();
416 jiff_icu::ConvertInto::convert_into(&jdt.to_zoned(SYSTEM_TZ.to_owned()).unwrap())
417}
418
419// With this, we won't necessarily need to build a zoned DateTime at format time
420// It's okay to use general enums; building is module-local and LLVM should be
421// able to keep track of constructed variants
422enum DateTimeFormatter {
423 WithZone(
424 icu_datetime::DateTimeFormatter<fieldsets::enums::CompositeFieldSet>,
425 //icu_datetime::DateTimeFormatter<fieldsets::Combo<fieldsets::enums::CompositeDateTimeFieldSet, fieldsets::enums::ZoneFieldSet>>
426 ),
427 NoZone(icu_datetime::DateTimeFormatter<fieldsets::enums::CompositeDateTimeFieldSet>),
428}
429
430impl DateTimeFormatter {
431 fn format(&self, dt: &DateTime<Iso>) -> icu_datetime::FormattedDateTime<'_> {
432 match self {
433 Self::WithZone(dtf) => dtf.format(&naive_datetime_to_system(dt)),
434 Self::NoZone(dtf) => dtf.format(dt),
435 }
436 }
437}
438
439impl intl_memoizer::Memoizable for DateTimeFormatter {
440 type Args = FluentDateTimeOptions;
441
442 type Error = ();
443
444 fn construct(
445 lang: unic_langid::LanguageIdentifier,
446 args: Self::Args,
447 ) -> Result<Self, Self::Error>
448 where
449 Self: std::marker::Sized,
450 {
451 // Convert LanguageIdentifier from unic_langid to icu
452 let langid: icu_locale_core::LanguageIdentifier =
453 lang.to_string().parse().map_err(|_| ())?;
454 args.make_formatter(langid).map_err(|_| ())
455 }
456}
457
458/// Working around that intl_memoizer API, because IntlLangMemoizer doesn't
459/// expose the language it is caching
460///
461/// This would be a trivial addition but it isn't maintained these days.
462struct GimmeTheLocale(unic_langid::LanguageIdentifier);
463
464impl intl_memoizer::Memoizable for GimmeTheLocale {
465 type Args = ();
466 type Error = std::convert::Infallible;
467
468 fn construct(lang: unic_langid::LanguageIdentifier, _args: ()) -> Result<Self, Self::Error>
469 where
470 Self: std::marker::Sized,
471 {
472 Ok(Self(lang))
473 }
474}
475
476/// A Fluent function for formatted datetimes
477///
478/// Normally you would register this using
479/// [`BundleExt::add_datetime_support`]; you would not use it directly.
480///
481/// However, some frameworks like [l10n](https://lib.rs/crates/l10n)
482/// require functions to be set up like this:
483///
484/// ```ignore
485/// l10n::init!({
486/// functions: { "DATETIME": fluent_datetime::DATETIME }
487/// });
488/// ```
489///
490/// # Usage
491///
492/// ```fluent
493/// today-is = Today is {$date}
494/// today-is-fulldate = Today is {DATETIME($date, dateStyle: "full")}
495/// now-is-time = Now is {DATETIME($date, timeStyle: "medium")}
496/// now-is-datetime = Now is {DATETIME($date, dateStyle: "full", timeStyle: "short")}
497/// ````
498///
499/// See [`DATETIME` in the Fluent guide][datetime-fluent]
500/// and [the `Intl.DateTimeFormat` constructor][Intl.DateTimeFormat]
501/// from [ECMA 402] for how to use this inside a Fluent document.
502///
503/// We currently implement only a subset of the formatting options:
504/// * `dateStyle`
505/// * `timeStyle`
506///
507/// Unknown options and extra positional arguments are ignored, unknown values
508/// of known options cause the date to be returned as-is.
509///
510/// [datetime-fluent]: https://projectfluent.org/fluent/guide/functions.html#datetime
511/// [Intl.DateTimeFormat]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
512/// [ECMA 402]: https://tc39.es/ecma402/#sec-createdatetimeformat
513// Known implementations of Intl.DateTimeFormat.DateTimeFormat(). All use ICU.
514// https://searchfox.org/firefox-main/source/js/src/builtin/intl/DateTimeFormat.js (MPL-2.0)
515// https://searchfox.org/firefox-main/source/js/src/builtin/intl/DateTimeFormat.cpp
516// https://chromium.googlesource.com/v8/v8/+/main/src/objects/js-date-time-format.cc (BSD-3-Clause)
517// https://github.com/WebKit/webkit/blob/main/Source/JavaScriptCore/runtime/IntlDateTimeFormat.cpp (BSD-2-Clause)
518// deno eval --print 'new Intl.DateTimeFormat("en-US", {dateStyle: "full", timeStyle: "full"}).format(new Date())'
519// https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibJS/Runtime/Intl/DateTimeFormatConstructor.cpp (BSD-2-Clause)
520// https://github.com/formatjs/formatjs/tree/main/packages/intl-datetimeformat (MIT)
521// https://github.com/formatjs/formatjs/blob/main/packages/intl-datetimeformat/src/abstract/InitializeDateTimeFormat.ts
522// https://github.com/google/rust_icu/blob/main/rust_icu_ecma402/src/datetimeformat.rs (Apache-2.0, ICU4C)
523// does new_with_pattern but never calls new_with_styles, does not handle dateStyle/timeStyle
524// https://github.com/unicode-org/icu4x/tree/main/ffi/ecma402 (Unicode-3.0; mostly a placeholder, does not impl DateTimeFormat)
525// https://codeberg.org/kiesel-js/kiesel/src/branch/main/src/builtins/intl/date_time_format.zig
526// https://github.com/boa-dev/boa/tree/main/core/engine/src/builtins/intl/date_time_format
527// Boa looks reasonable, could be extracted
528// Except it prints in UTC, not local, and does not respect long/full timeStyles. Test with:
529// cargo run -p boa_cli -- -e 'new Intl.DateTimeFormat("en-US", {dateStyle: "full", timeStyle: "full"}).format(new Date())'
530//
531// styles map to an UDateFormatStyle in ICU4C;
532// I don't understand how ICU4X has reduced the number of styles (removed full, kept only short medium long)
533// https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/udat_8h.html#adb4c5a95efb888d04d38db7b3efff0c5
534// Explanation of the API change and mapping here:
535// https://github.com/unicode-org/icu4x/issues/7523#issuecomment-3820793161
536#[allow(non_snake_case)]
537pub fn DATETIME<'a>(positional: &[FluentValue<'a>], named: &FluentArgs) -> FluentValue<'a> {
538 match positional.first() {
539 Some(FluentValue::Custom(cus)) => {
540 if let Some(dt) = cus.as_any().downcast_ref::<FluentDateTime>() {
541 let mut dt = dt.clone();
542 let Ok(()) = dt.options.merge_args(named) else {
543 return FluentValue::Error;
544 };
545 FluentValue::Custom(Box::new(dt))
546 } else {
547 FluentValue::Error
548 }
549 }
550 // https://github.com/projectfluent/fluent/wiki/Error-Handling
551 // argues for graceful recovery (think lingering trauma from XUL DTD
552 // errors)
553 _ => FluentValue::Error,
554 }
555}
556
557/// Extension trait to register DateTime support on [`FluentBundle`]
558///
559/// [`FluentDateTime`] values are rendered automatically, but you need to call
560/// [`BundleExt::add_datetime_support`] at bundle creation time when using
561/// the [`DATETIME`] function inside FTL resources.
562pub trait BundleExt {
563 /// Registers the [`DATETIME`] function
564 ///
565 /// Call this on a [`FluentBundle`].
566 ///
567 fn add_datetime_support(&mut self) -> Result<(), FluentError>;
568}
569
570impl<R, M> BundleExt for FluentBundle<R, M> {
571 fn add_datetime_support(&mut self) -> Result<(), FluentError> {
572 self.add_function("DATETIME", DATETIME)?;
573 //self.set_formatter(Some(datetime_formatter));
574 Ok(())
575 }
576}