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:
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.
💡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, 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.
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.
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, 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.
After we destructure our parts into separate variables—
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
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 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
_p() functions to display localized UI strings, like “yesterday”, “just now”, and more.
__() works with simple strings, and
Let’s take a look at how we format a
date given to
humanFriendlyDate that is only hours old.
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
Date.prototype.toLocaleTimeStringfunction. You can read more about that function on the MDN docs.
Minutes & Seconds Ago
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).
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.
That’s our demo pretty well done. If you’re building an internationalized app, take a look at PhraseApp for a professional localization solution. With ever-growing integrations, a powerful translation web admin console, and a focus on both developers and translators, PhraseApp can really simplify your team’s localization workflow. Take a look at all of PhraseApp’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 🙂