Localized date-time formatting?

I'm a rust newbie, and an amateur programmer, and stuck on how to do localized date and date-time formatting for a project I'm working on.

I have a model that includes date formatting configuration that I borrowed from the JS Intl.DateTimeFormat.

I'm using the edtf crate for the parsing, which supports standard ISO dates and date-times, and also extended features like intervals.

In Rust, I have a function that reads the config options, and transforms them into a format spec string.

So, for example, if the output locale is "es", I am aiming to correctly format the month and day in that language; not just the right word, but also grammatical context (sequencing and such):

4 de Abril

For now, I just want to get basic dates working as above, where my date formatting function just has this placeholder:

        let formatted_date: String = match edtf_date {
            // TODO need localized date rendering, using format_string
            Edtf::Date(date) => date.to_string(),

If, as I assume, I need to use Chrono here, what would be an example usage for this case?

Or is this not even possible?

For properly localised date formatting across a range of locales, I would try ICU first. Their tutorial uses date formatting as an example: icu4x/intro.md at main · unicode-org/icu4x · GitHub

There’s a bit of overhead with ICU for working out which data to extract and how to bundle it with your app; for getting started, es is one of the locales in the icu_testdata crate, which makes things easier.

3 Likes

Thanks @carey!

So the unstable-locales featured from chrono is not likely adequate?

1 Like

It may be good enough, but I don’t have any experience with it. It certainly looks easier to use.

However, when you mention the JS Intl API and using grammatical context, I would go for ICU first in any language, not just Rust.

OK. It does indeed look like it may be exactly what I'm looking for.

Alas, I'm a bit lost on how to configure it for basic things like month or year-only output.

BTW, just found this awesome thread on reddit, with input from some of the folks working on the different packages.

Update: I took a look at some source code, and did a little experiment, which is discouraging.

The result with the below code is this:

Chrono, year-only: 2014
Chrono, full, date-only: 28 November 2014
Chrono, month-day only: 28 noviembre
ICU, full date: 14 de octubre de 2020

Of note:

  • configuring chrono to do what I need is easy (though with caveats below)
  • I can't figure out if I can do that in icu, or how
  • but the output for es-PE is actually wrong with chrono (I think; need to double-check), and correct with icu
  • it also looks like Chrono doesn't reorder date parts more generally

use chrono::prelude::*;

use icu::calendar::DateTime;
use icu::datetime::{options::length, DateTimeFormatter};
use icu::locid::locale as icu_locale;


fn main() {

    const LC: icu::locid::Locale = icu_locale!("es-PE");

    let dt = Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap();

    let provider = icu_testdata::any();

    let options_date = length::Bag::from_date_style(length::Date::Long);
    let _options_time = length::Bag::from_time_style(length::Time::Medium);

    let dtf =
        DateTimeFormatter::try_new_with_any_provider(&provider, &LC.into(), options_date.into())
            .expect("Failed to initialize DateTimeFormatter");

    let date = DateTime::try_new_iso_datetime(2020, 10, 14, 13, 21, 28)
        .expect("Failed to create a datetime.");

    // DateTimeFormatter supports the ISO and native calendars as input via DateTime<AnyCalendar>.
    // For smaller codesize you can use TypedDateTimeFormatter<Gregorian> with a DateTime<Gregorian>
    let date = date.to_any();

    let formatted_date = dtf.format(&date).expect("Formatting should succeed");

    println!("Chrono, year-only: {}", dt.format_localized("%Y", Locale::en_US));
    println!("Chrono, full, date-only: {}", dt.format_localized("%e %B %Y", Locale::en_US));
    println!("Chrono, month-day only: {}", dt.format_localized("%e %B", Locale::es_PE)); // can get month-day, but not it's not rendered correctly

    println!("ICU, full date: {}", formatted_date); // month-day handling here is correct, but can't see how to remove the year
}

But I may need to do some of this anyway.

Configuring ICU to output just the day and month, like JavaScript, appears to be possible by enabling the icu_datetime_experimental feature on both icu and icu_testdata. Then with this code:

use icu::calendar::DateTime;
use icu::datetime::{options::components, DateTimeFormatter};
use icu::locid::locale;

fn main() {
    let mut bag = components::Bag::default();
    bag.day = Some(components::Day::NumericDayOfMonth);
    bag.month = Some(components::Month::Long);
    let options = icu::datetime::DateTimeFormatterOptions::Components(bag);

    let dtf = DateTimeFormatter::try_new_experimental_unstable(
        &icu_testdata::unstable(),
        &locale!("es").into(),
        options,
    )
    .unwrap();

    let date = DateTime::try_new_iso_datetime(2020, 9, 12, 12, 35, 0).unwrap();
    let date = date.to_any();

    println!("{}", dtf.format(&date).unwrap());
}

I get “12 de septiembre” for es, “September 12” for en and “12 September” for en_NZ.

1 Like

Perfect; thanks much @carey!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.