A Human-friendly Way to Display Dates in TypeScript/JavaScript

The difference between a good product and a great one (and the revenue they generate) often comes down to UX. When displaying dates, we can provide an intuitive experience to our users when we format our dates as "today", "just now", or "4 hours ago". In this article we'll see how we can accomplish that with TypeScript.

User Experience (UX) can often mean the difference between a good product and a great one. In the increasingly competitive digital landscape, we need to always put our products’ users first to ensure that we have an edge in the market. This often means going beyond features that our users need, and facilitating pleasurable experiences our users want. One way we can achieve this is by showing data in a natural way.

When it comes to dates, we humanoids often think of chronological events happening “a few minutes ago” or “on Tuesday”, as opposed to “on 27 June 2019 3:51 pm”. Social media feeds and text messaging applications are good examples of the former, human-friendly approach to displaying dates. Why don’t we take a page from the socials when it’s appropriate and reduce the cognitive friction of our web app users by displaying natural date strings?

“Because it’s too hard”, I hear you say. Well, it’s not trivial. But the core human-friendly date formatting algorithm is simpler than you may think. Let’s build a little app to demonstrate. Our demo is of a very simple social media feed, with posts by friends sorted in reverse-chronological order. Here’s a look at what we’re building:

See the Pen
Human-friendly Dates
by Mohammad Ashour (@mashour)
on CodePen.

Notice that we can view the feed in English and Arabic (it’s localized), and that posts are showing dates relative to the moment the page was loaded.

More human than human

Rendering the Feed

Let’s take a quick look at the code that’s rendering our feed.

That’s the HTML: nothing too crazy so far. We have some static intro markup, followed by two buttons that make up our locale-switcher. These are followed by an empty feed container that we’ll fill with posts from JavaScript.

💡FYI » The CSS classes you’re seeing are Bootstrap classes.

Now let’s view the shape of the mock data we’re rendering.

We’ll switch between English and Arabic depending on the language selected in our locale-switcher. Important to note is the posted_at values in the Post objects. These Dates are generated specifically to ensure a reverse-chronological sorting of posts going back in time from the moment the script was run. But, of course, they’re just plain old JavaScript Dates, so we’ll need to do a bit of work to get them looking like “just now” and “yesterday”. First, let’s take a quick look at how a Post is rendered.

The renderPost function is what generates the HTML for each post in the feed. It’s mostly straight-forward. We have some logic to handle text directionality since we’re supporting Arabic, a right-to-left language. We’re also making use of a function called humanFriendlyDate to display the posted_at dates. This is where the magic happens.

Achieving Localized Human-friendly Dates in TypeScript/JavaScript

Let’s dive into the function that formats a Date as a human-friendly string,  humandFriendlyDate, to see how it ticks.

The first thing we do is convert the given date to a Unix timestamp, using Date.prototype.valueOf(), as well as get the current date & time as a Unix timestamp from Date.now(): this makes our math quite easy later. We then convert our timestamps from milliseconds to seconds, since we don’t need to worry about anything smaller than a second (we’re only interested in human time). Our millisecond-to-second conversion is done via a trivial utility function.

Breaking our Date Difference into Components

We now have a simple integer representation of our dates, which makes it easy to calculate the difference between the current time and the given date. While we’re at it, we also break up this difference into meaningful parts ie. years, months, days, etc. The break up happens using a helper getDateTimeComponents function.

In getDateTimeComponents, given our timestamp in seconds, we start by seeing how many whole years are in it. We set the number of whole years to our components.years field. We then take the remaining number of seconds, and check how many whole months are in it. We keep going down this way until we reach seconds, at which point we’ve broken up our date difference into all needed component parts.

From Years to Seconds: Working Down in Granularity

Let’s return to our humanFriendlyDate function, where we actually use our date difference component parts.

Years Ago

After we destructure our parts into separate variables—years, months, days, etc.—we start at the highest level of granularity: years. If the date we’ve been given is years ago, we want to display the full date to avoid confusion: something along the lines of “June 28th, 2018.”

We accomplish this formatting using a helper, formatLocalizedDateWithOrdinal function. This function checks to see whether the current locale is English, and formats the full date with the ordinal day of the month if it is. If the locale is not English, formatLocalizedDateWithOrdinal formats the date without the ordinal.

💡FYI » “Ordinal” is a fancy of way of saying a number that designates its place in an ordering e.g. “1st” is the ordinal for 1, and “2nd” is the ordinal for 2.

📖 Go Deeper » We’re using the built-in Date.prototype.toLocaleDateString to get JavaScript to format our localized date strings for us. You can read more about this function on the MDN docstoLocaleDateString ties into the JavaScript localization API, Intl.

Months Ago & Beyond a Week Ago

You may have noticed that we’re providing the option to format the date with or without the year in formatLocalizedDateWithOrdinal. This is because when the date given to humanFriendlyDate is months ago, or beyond a week ago, we want to format the full date without the year. If the date is beyond a week ago, it would generally be confusing to display something like “two Tuesdays ago.” Showing the day of the month and the month itself, e.g. “August 24th”, makes things much clearer. Since the date is within the current year, we can omit the year part of the date. So we use formatLocalizedDateWithOrdinal, providing the includeYear option as false in this case.

Days Ago

Note the days code above. If the date given to humanFriendlyDate is yesterday, we simply display the word “yesterday”, translated to the current locale. Before that, we check whether the date is older than yesterday and within 7 days ago. If it is we display the day of the week, e.g. “Tuesday”.

A Quick Note on General String Internationalization

We’re using __() and _p() functions to display localized UI strings, like “yesterday”, “just now”, and more. __() works with simple strings, and _p() handles phrases that have pluralization. These two functions are part of a simple, custom i18n library that we added to this demo to facilitate our work. General i18n is a bit out of the scope of this article, however. If you’d like to see how the i18n library is implemented, you can see the full code of this demo on Codepen. And if you want to dive deeper into custom JavaScript/TypeScript i18n, or selecting an existing i18n library, check out one of our guides here:

Hours Ago

Let’s take a look at how we format a date given to humanFriendlyDate that is only hours old.

If the date is less than 12 hours old, we just display something like “3 hours ago”, translated into the current locale of course. If, however—and we check for this first—our date is more than 12 hours old (but less than a day old), we display something more like “today at 9:22 AM”. Where we draw the line between the two-hour formats is largely an aesthetic choice and is configurable via the TODAY_AT_THRESHOLD_IN_HOURS constant.

📖 Go Deeper » To format the “today at X:XX AM” string, we’re using JavaScript’s Date.prototype.toLocaleTimeString function. You can read more about that function on the MDN docs.

Minutes & Seconds Ago

If humanFriendlyDate gets a date that is only minutes old, we just display something akin to “2 minutes ago”, again translated into the current locale.

When the given date is only seconds old, we could consider displaying something like “now” to our users. First, however, we need to define “now”, since from a human perspective “now” is generally a bit fluid. In our case, we define “now” as anything no older than 10 seconds (and we keep this number configurable).

When date is seconds old and older than “now”, we show the user something like “4 seconds ago.” Otherwise, our user sees “just now”. As usual, both these strings are localized.

When all is said and done, we get something that works like the following.

See the Pen
Human-friendly Dates
by Mohammad Ashour (@mashour)
on CodePen.

Peacing Out

That’s our demo pretty well done. If you’re building an internationalized app, take a look at Phrase for a professional localization solution. With ever-growing integrations, a powerful translation web admin console, and a focus on both developers and translators, Phrase can really simplify your team’s localization workflow. Take a look at all of Phrase’s features, and sign up for a free 14-day trial.

That should cover it for our walk down human-readable memory lane. We hope you picked up some info here that will help your websites and apps better delight your users. Happy coding 🙂

5 (100%) 6 votes