How to make a internationalized date display with Web Components
Tags: webdev, webcomponents, customelements, vanillajs
Published: 2020-11-16 18:01:46

There are many places where I wish I could share a date with my colleagues in many different timezones easily.  Let's say I want to have a meeting at `9:00` what does that mean?  Unfortunately, if you write a date they need to know my timezone and translate.  Wouldn't it be nice if apps, especially web-based ones could easily do that work for us with a simple HTML element?

Requirements:

- Need to be able to set a date
- The date displays in the user's timezone
- Different formats supported for flexibility

# Boilerplate

```js
export class WcLocalDate extends HTMLElement {
    static observedAttributes = [];
    constructor() {
        super();
        this.bind(this);
    }
    bind(element){
        this.render = this.render.bind(element)
    }
    render(){
        this.shadow = this.attachShadow({ mode: "open" });
        this.shadow.innerHTML = ``;
    }
    connectedCallback() {
        this.render();
        this.cacheDom();
    }
    cacheDom(){
        this.dom = {
        };
    }
    attributeChangedCallback(name, oldValue, newValue) {
        this[name] = newValue;
    }
}

customElements.define("wc-local-date", WcLocalDate);
```
I can't think of any relevant events so we can omit those but otherwise it's the same one as previous posts.

# Attributes

Normally I'd start with the DOM but there's almost nothing there so let's look at the attributes.  The most obvious is `value` which will take in the date as a string.  We also need a attributes to take in the format.  Again this would be a lovely place to use the full power of CSS custom properties to specify date formats and have them grow shorter with media queries, but we still can't observe them.  So we need some properties.  I came up with 2: `date-style` and `time-style`.  These were chosen because I'm going to be using the Intl.DateFormat API to do localization and it'll be obvious how they line up later.

```js
#value = new Date();
#dateStyle = "full";
#timeStyle = "full";
static observedAttributes = ["value", "date-style", "time-style"];
get value(){
    return this.#value;
}
set value(val){
    this.#value = val;
}
set dateStyle(val){
    this.#dateStyle = val;
}
set timeStyle(val){
    this.#timeStyle = val;
}
```

Just some basic getter/setter stuff to hookup for now.  On `value` though we want do want to convert it from a string to a `Date`.  Also, we want to convert the attribute names to something more consistent with JS conventions.

```js
attributeChangedCallback(name, oldValue, newValue) {
    if(name === "value"){
        this.value = new Date(val);
    } else {
        this[hyphenCaseToCamelCase(name)] = newValue;
    } 
}
```
Pretty basic. `hypenCaseToCamelCase` looks like this:

```js
function hyphenCaseToCamelCase(text){
    return text.replace(/-([a-z])/g, g => g[1].toUpperCase());
}
```
Like most people I stole it from [stackoverflow](https://stackoverflow.com/questions/6660977/convert-hyphens-to-camel-case-camelcase), but it's worth explaining a little.  It looks for any lowercase letter after `-`, then in the replace function we take the second character (the first is `-`), uppercase it and return it. Keep this in the toolbox as it's handy and sharable if you are making component systems.

So, value still needs converting so it makes sense to run it through `new Date(str)`. This makes it easy to decide that we want to support ISO Date formats.

# ISO Dates

If you've been around JS long enough you'll know that dates are a deep dark cave filled with dart guns and sharpened bamboo spears.  In this region of the cave the treasure is being able to sensibly enter a time in ISO (or a subset) and have it be interpreted.

We want this because it frees us (somewhat) from the cursed input format localization problem.  Even if we restricted ourselves to formats with just simple numbers and separators, date formats might have a different ordering depending on the user's culture. As an internationalize date display, we should make sure it's easy for everyone to use.  ISO is easy to write and parse.

Let's look at ISO dates a little more. ISO 8601 as it's formally called is a text format for specifying dates that's pretty standard across computer platforms.  It goes year-month-day, the letter "T" hour-minute-second and then the time offset.  Most parts are optional and they must be padded with 0s.

- 2020-11-06T05:35:00Z
- 2020-11-06T05:35:00-09:00
- 2020-11-06T05:35:00-0900
- 2020-11-06T05:35:00+01:00
- 2020-11-06T05:35:00+0100
- 2020-11-06T05:35.001
- 2020-11-06T05:35
- 2020-11-06
- 2020-11
- 2020

Note that this is what `Date` supports.  The standard also supports some things like offset shorthand `2020-11-06T05:35:00-09` but this doesn't reliably parse.

The `Z` in the first example is specifying UTC (zulu time).  The next 4 are timezone offsets of `hour:minute`, the colon `:` is optional and it can be +/- from UTC.  What about the next ones?  What timezone are they?  Well, if you include the time portion it's the user's timezone, if you do not it's UTC.  Gotcha!

This strange behavior is obviously going to catch users off-guard and have them miss their very important meetings.  So we'll need to build a function that can catch the different format types and normalize it:

```js
//this is static, not a member of WcLocalDate
function asUTC(dateString){
    if (/\d\d\d\d-\d\d-\d\dT\d\d:\d\d$/.test(dateString)) {
        return `${dateString}:000Z`;
    }
    return dateString;
}
```
Regex are often much scarier read than written.  What we're doing is testing for a sequence of `\d` (digits) interspersed with `-` in the like `yyyy-mm-dd` but with a time component `T` + `hh:mm:ss`.  If it matches that, then we'll append the rest to make it UTC. Note the `$` at the end, this is important otherwise you'll partially match longer formats which isn't what we want.  If we don't get this exact format, let's assume they wrote something correct, at least those mistakes should be a bit more obvious.

By the way, since I wrote it while working through the API, if you want to sniff out only formats that convert to static timezones you can use this regex: `/\d\d\d\d(-\d\d(-\d\d(T\d\d:\d\d:\d\d(Z|[+-]\d\d:?\d\d))?)?)?$/`.

# Reacting to changes

If the attributes change we need to re-render the date with the updated parameters so let's update the setters:

```js
setDisplayValue(){
    const options = {};
    if (this.#dateStyle) {
        options.dateStyle = this.#dateStyle;
    }
    if (this.#timeStyle) {
        options.timeStyle = this.#timeStyle
    }
    const formatter = new Intl.DateTimeFormat(undefined, options);
    this.#displayValue = formatter.format(this.#value);
    if(this.dom?.date){
        this.dom.date.textContent = this.#displayValue;
    }
}
get value(){
    return this.#value;
}
set value(val){
    this.#value = val;
    this.setDisplayValue();
}
set dateStyle(val){
    this.#dateStyle = val;
    this.setDisplayValue();
}
set timeStyle(val){
    this.#timeStyle = val;
    this.setDisplayValue();
}
```

We call `this.setDisplayValue()` which takes the parameters in and uses `Intl.DateTimeFormat` to render it in the correct locale.  The first parameter is undefined as this will cause it to use the user's locale.  We also have to check if `this.dom` exists because the attribute updates are triggered before render.

# Testing

Testing the component can be annoying.  The more reliable way is to change your computer's locale and timezone and then set it back when you are done.  Luckily, Chrome gives us some dev tools to set our locale, they're just a bit hidden.

![locale-emulation](https://dev-to-uploads.s3.amazonaws.com/i/a9tf834343q3w5or0xlr.gif)

- Console drawer
- 3 dots
- Sensors

You can now emulate locales and locations.

# Fallbacks

If the browser doesn't support custom elements or if it doesn't support JS we can have it fallback.  This is as simple as just adding a date inside the tags:

```html
<wc-local-date value="2020-11-06T01:00:00Z" date-style="short">2020-11-06T01:00:00Z</wc-local-date>
```
The inner portion will be rendered and is still understandable and if the browser supports it, then it'll enhance to display using the local-date component.

# Demo

{% codepen https://codepen.io/ndesmic/pen/RwRdQLK %}

Note: Codepen seems to set the locale `en` on the HTML tag for me so other language settings might get overridden but that's not the component's fault.