Locale-aware Date and DateTime formatting library

At work, I recently needed to do locale-aware formatting of a date-time value, where I did not know the locale ahead of time. Because of details of the project, I needed to do this synchronously, so I was looking for an Elm library that would format date-times for any locale (the sort of thing that a JS developer would use Luxon or the Intl API for). A cursory web search indicates that I am far from the first person to have this desire, and yet a fully Elm end-to-end solution for this problem does not exist yet. Without rehashing anything from the past, I would like to propose an API for such a library.

(My work problem is currently solved via the workaround of having JS pass in the formatted strings along with the date-time values, since all of my date-time values are being passed in a port from a JS API)

Goal and scope

The goal is to reach a pure Elm API for locale-aware date and date-time formatting that feels like Elm, while being familiar enough to JS developers to make the learning curve of Elm easier rather than harder. This API will cover a subset of the behavior from Intl.DateTimeFormat.

Prior Work

This research greatly aided by https://korban.net/elm/catalog/packages/data/time

Published

All of these libraries require the library user to know the format instead of being able to derive the format based on the user’s locale.

Unpublished

Gaps

  • Deriving a format for a given locale. E.g. given that the user is in “en-US”, and we want to format a date-time value in a Short way (in the Gregorian calendar), the UTS #35 format string is ”M/d/yy, h:mm a”, according to Unicode CLDR 39.
  • Parsing/canonicalization of Locales. wolfadex/locale-negotiation provides locale negotiation based on BCP47 strings, but handles values like “de_DE” or “de-DE-EURO” (nonstandard but real-world examples) in a way that I find unintuitive. Most of the time, the language and region are all that we need from a locale identifier, but this may not always be the case.

The first gap is the thing I actually want to solve. However, I believe that a Locale type can encapsulate a lot of complexities and make the API simpler.

Proposal

module Locale exposing (..)

type Locale = Opaque

toBCP47 : Locale -> String

{-| fromString "en-US" == Just en_US
-}
fromString : String -> Maybe Locale 

-- A few default locales: 

en : Locale 

en_US : Locale 

en_GB : Locale 

es : Locale 
module Format.Length exposing (Length(..))

type Length = Full | Long | Medium | Short
module Format.DateTime exposing (..)

type FormatType = DateOnly Length 
	| TimeOnly Length
	| DateAndTime { date : Length, time : Length }

format : FormatType -> Locale -> Zone -> Posix -> String
module Format.Date exposing (..)
import Date exposing (Date) -- from justinmimbs/date

format : Length -> Locale -> Date -> String

As a library user, this would allow me to get some locale strings either from navigator.language, navigator.languages, or some other source (probably passed in with my program flags), convert them into a Locale type that I store in my model (manually choosing a default locale), and then use that Locale to do all of my date and date-time formatting, which is what I believe most people who want to “use Intl” actually want to do.

Summary

The above API only covers the small part of the browser’s Intl.DateTimeFormat API that I consider to be the most important, and not already covered by previous high quality libraries. I believe that this API is fairly idiomatic Elm, while being familiar enough to JS developers to not make the learning curve more difficult.

Please provide any feedback about:

  • Ways to make this API more idiomatically Elm
  • Ways to make this API more friendly to newcomers
  • Other parts of the Intl.DateTimeFormat API that you have seen use cases for in Elm.
  • Non-Gregorian Calendar support. Is it needed as part of a library like this?
2 Likes

Cool to see you’re looking into this! There are a few things that might be worth taking into account.

Unpublished

Besides the locale negotiation I also started looking into (more) native support for Fluent in Elm and this is one of the things I was looking at as the current Fluent web solutions use the Intl API. I don’t have anything worth sharing yet in that space, but it did come up for me.

Kernel

If you’re looking to make something with kernel code then you’ll need to take into account things like compile targets (will this work in Node, Deno, only the browser, what about wasm). There’s already the elm/browser package for browser specific APIs.

or User Space

If you’re thinking this should be built in user space then how do you get the user requested locales and in what format? I have an example of how I do that with my Fluent work, where I’m using navigator.languages, which is still considered experimental, and returns an array. There’s also navigator.language which isn’t experimental but only returns a single locale.

Non-Elm Solutions

At work we use GitHub - github/time-elements: Web component extensions to the standard <time> element. and it’s easy to incorporate in nearly any app. Yes it’s not Elm but it gets the job done with nearly 0 effort from the developer.


Might also be looking at Fluent and how they handle locale type stuff since they target multiple languages and platforms and have to take that into account.

:wave: Fluent looks really useful! I would be quite happy if a library like this could be helpful in bringing Fluent to Elm. It looks like this library would need to support the more granular options from Intl.DateTimeFormat in order to be fully usable by Fluent.

My intention is actually to implement this entirely in user space, which would entirely sidestep the compile target problem and also sidestep the making-a-new-kernel-library coordination problem.

My current plan is to write Parsers for standard BCP47 and Unicode CLDR locale identifiers, with a little bit of wiggle room built in for slightly nonstandard things. For example, “de_DE_EURO” is not standard CLDR (I believe it should be “de_DE_u_cu_eur”), but since we do not need the currency info to do date / datetime formatting, it would be good to parse it the same as “de_DE” which is valid CLDR. My hope is that most real-world systems will stick close enough to either BCP47 or CLDR for this to work out. I think that this library does not need to worry about whether this locale string came out of navigator.language or navigator.languages or any other system, as long as they are close enough to BCP47 or CLDR for the parsers to figure them out.

In my particular use case, I am using a combination of locales from navigator.language and values that come from SalesForce’s User object. The SalesForce values are not quite always valid CLDR (that is where the “de_DE_EURO” example came from), which is why I want the parsers to have a bit of wiggle room in them.

As far as implementing from there, my plan is to use CLDR 39 to generate all of the formatting information for the Locales. Matching a parsed Locale to an available Locale will probably use an algorithm like yours, @wolfadex , although I hope that having the Locale as structured data may help with the edge cases. I haven’t look into this in detail yet. I might just convert the Locales into BCP47 strings and directly use your library, even!

Regarding Non-Elm solutions, I definitely encourage others to evaluate whether a web component will work for them! It will not work for me in my particular use case, because I actually do need it to be both synchronous and to return a String value instead of an Html msg. I acknowledge that this is a rare set of requirements, and people who do not share them should definitely look into web components.

1 Like

I doubt it’s that rare.

Also having something like this unlocks potential for further composition into the ecosystem for Elm libraries. For instance, my own library, gampleman/elm-visualization can do intelligent date formatting for plot scales, doing things like this:

(And similarly with other time units, like showing 16:00, then '15, '30, '45, then 17:00).

However, this only works in English at the moment, leaving users of other locales to implement similar “smart” time formatting entirely from scratch (which is somewhat non-trivial).

2 Likes

Hi! This is pretty good timing.

Fluent

A few months ago, I too started researching what a Fluent implementation in pure Elm could look like:

So far, I’ve written an FTL file parser and have begun work on a code generator that would make Elm modules for each translation file.
The plan is to generate functions that take all the arguments needed for a message and either return a String or a more complex type depending on whether it contains markup. The idea is to provide hooks for markup to support not only elm/html, but any other library like mdgriffith/elm-ui.

Intl API

As mentioned in this thread, the most challenging part is not having synchronous access to the Intl API. Fluent requires this for plural rules, number formatting, date formatting, etc.

For that, I have started working on this:

The idea is to provide a pure Elm implementation of the Intl API, with Elm modules generated for each locale out of the CLDR.

As long as apps only reference the locales they use, Elm’s DCE (or LCI) should keep the generated JS small.

The downside is that you can’t load a locale dynamically unless you include them all. I’ve considered providing decoders instead of a codegen, but that would mean that Plural Rules would have to be “interpreted” at runtime, which could kill performance. Maybe there can be a mixed solution, though.

So far, I’ve written a Plural Rules parser and the most significant part of the Number Formatter.

Both projects are pretty ambitious, and I’m only working on them as a hobby. I haven’t done anything with dates yet, but I will have to eventually. I’d love to combine efforts if you’re interested!

2 Likes

Neat to see so much interest in Fluent! A Fluent-to-Elm transpiler definitely seems like a useful tool.

Elm modules generated for each locale out of the CLDR

This was also my implementation plan :slight_smile:

provide a pure Elm implementation of the Intl API

This is also where I would like to get to (at least for Intl.DateTimeFormat, which is the particular problem I have been having). However, the Intl JS API does not do a good job of making impossible states impossible (dateStyle and timeStyle can be used with each other, but not with any other options, dayPeriod only does anything for 12-hour clocks), which is why I wanted to explore what the Elm version of this API should be.

Since my use case involves using the dateStyle and timeStyle options, my proposed API focused on that, but I think that a new constructor for the FormatType type like | WithOptions Options could be added afterwards. Perhaps the FormatType should be opaque with exposed constructor functions, then, so that it would not be a major version bump to add this later… :thinking:

you can’t load a locale dynamically unless you include them all.

TBH, I had not thought yet about DCE. It seems that another factor to consider is being safe to include in a Model (not including functions in what we expect users to store in the Model). I will need to think on this more. :thinking:

I’d love to combine efforts if you’re interested!

I think that this makes a lot of sense! I’ll need a bit of time to fully absorb what you’ve done so far, but it certainly sounds like we have extremely similar visions and plans.

I was only thinking of application use cases, where doing the conversion asynchronously through a port would usually be inconvenient but not insurmountable. I did not even think about all of the libraries that would like to do this, that would necessarily require synchronous conversion!