Handling Time Zones in a Progressively Decoupled Drupal Application
This year I’ve been working on a fun new Drupal-based event management application for libraries. During the course of development, I’ve become convinced that–beyond caching and naming things–the third level of Computer Science Hell is reserved for time zone handling. Displaying and parsing time zones proved to be a fruitful source of unforeseen issues and difficult to diagnose bugs.
The application we’ve been working on uses a progressively decoupled architecture, meaning interfaces that demand greater interactivity are self-contained client-side applications mounted within the context of a larger server-rendered Drupal site. This allows us to focus our efforts, time and budget on specific key interfaces, while taking advantage of Drupal’s extensive ecosystem to quickly build out a broader featureset.
Some examples of where time zone bugs reared their nasty heads:
- In certain cases, events rendered server-side displayed different date times than the same event rendered client-side.
- In a filterable list of events, time based-filters, such as “Show events between September 6th and 9th.”
- A user’s list of upcoming events would sometimes not include events starting within the next hour or two.
- Two logged in users could see a different time for the same event.
Let’s talk about that last one, as it’s a fairly common Drupal issue and key to understanding how to prevent the former examples.
User’s Preferred Time Zone
Drupal handles time zone display at the user level. When creating or editing their account, a user can specify their preferred time zone.
All users have a preferred time zone–even the logged-out anonymous user. A Drupal site is configured with a default time zone upon creation. This default time zone serves as the preferred time zone of all anonymous users. It also serves as the default preferred time zone for any new user account as it’s created.
When date values are rendered, the user’s preferred time zone is taken into account. Let’s say you have Drupal site with a default time zone set to America/New_York. If you have an event set to start at 4pm EST, all anonymous users–since they’re preferred time zone matches the site default–will see the event starts at 4pm.
However, a user with a preferred time zone of America/Phoenix will see that same event’s time as either 2pm or 1pm – literally depending on how the planets have aligned – thanks to the user’s time zone preference and Arizona’s respectable disregard for daylight savings time.
So the first thing you need to understand to prevent time zone bugs is users can set their preferred time zone.
Dates are Stored in UTC
One thing Drupal does really well is store dates in Coordinated Universal Time or UTC – I’m not sure that’s how acronyms work – but whatever. UTC is a standard, not a time zone. Time zones are relative to UTC. Dates are stored in the database as either Unix timestamps or ISO-8601 formatted strings. In either case they are set and stored in UTC.
Likewise, Javascript date objects are stored internally as UTC date strings. This allows dates to be compared and queried consistently.
The Disconnect
We now know Drupal and JS both store dates internally in a consistent UTC format. However JS dates, by default, are displayed in the user’s local time zone – the time zone set in their computer’s OS. Drupal on the other hand, displays dates in your user account’s preferred time zone. These two won’t always match up.
If the browser and Drupal think you are in two different time zones, dates rendered on the server and client could be off by any number of hours.
For example, you may be in Denver and have America/Denver set as your preferred time zone, but if you’re logged out and Drupal has a default time zone set to America/New York, server rendered times will display 2 hours ahead of client rendered times.
In another scenario, you may live in the same time zone configured as Drupal’s default and don’t have have a preferred time zone set. Everything looks fine until you travel a couple time zones away, and now all the client rendered dates are off.
This is the root cause of the bugs in the first three examples above.
The browser couldn’t care less what preferred time zone you have set in Drupal. It only knows local time and UTC time. Unlike PHP, JS currently does not have native functions for explicitly setting the time zone on a Date object.
Keeping Client- and Server-Rendered Dates in Sync
Now we know Drupal and the browser store dates in UTC. This is good. To make our lives easier, we’ll want to keep our client side dates in UTC as much as possible so that when we query the server with date-based filters, we get proper comparisons of dates greater than or equal to.
But we need to ensure our client-rendered dates match our server-rendered dates when they are displayed on the page. We also need to ensure dates entered via form fields are parsed properly so they match the user’s preferred time zone. There’s two things we need to do.
- Let the client know the user’s preferred time zone.
- Parse and display client-side dates using this time zone.
Passing the User’s Preferred time zone
To do this, we’ll attach the preferred time zone via drupalSettings. This can be done via HOOK_page_attachments() in a .module file. If you don’t know how to create a custom module, there are plenty of resources online.
/**
* Implements hook_page_attachments.
*/
function my_module_page_attachments(array &$attachments) {
// Add user time zone to drupalSettings.
$user_time zone = new \Datetime zone(drupal_get_user_time zone());
$attachments['#attached']['drupalSettings']['my_module']['user'] = [
'time zone' => drupal_get_user_time zone(),
];
// Cache this per user.
$attachments['#cache']['contexts'][] = 'user';
// Clear the cache when the users is updated.
$attachments['#cache']['tags'][] = 'user:' . $current_user->id();
}
With this, we can now access the user’s preferred time zone in the browser from the drupalSettings
global, like so:
const userTimeZone = drupalSettings.my_module.user.time zone;
// Ex: ‘America/Denver’
Displaying, Parsing and Manipulating Dates in the User’s Time Zone
Now that we know the users preferred time zone client side, we can ensure any dates that are displayed and parsed, for example – from an input, are taking into account the correct time zone.
Currently there isn’t good native support for this in browsers. We have to either write our own functionality or use a third party date library. I typically use Moment.js for this. Moment has been around for a while and, as far as I know, has the best time zone handling.
To use Moment’s time zone handling, you’ll need to load the library with time zone support and the most recent time zone dataset. The dataset is required to map time zone names to the appropriate UTC offset – taking into account Daylight Saving Time at the appropriate time of year.
For all the following examples, we’ll assume you’ve loaded the time zone version of Moment with data bundled together as a browser global. There’s a number of other ways to import Moment via npm if you prefer to use it as a module.
<script src="https://momentjs.com/downloads/moment-time zone-with-data-2012-2022.min.js"></script>
Setting a Time Zone on Dates
To begin with, we need to tell Moment what time zone we are dealing with. We’ll also assume userTimeZone
has been set to the string value from drupalSettings above.
// Create a date for now in the user’s time zone
const date = moment().tz(userTimeZone);
Just like Drupal and native JS Date objects, Moment stores the underlying date in UTC. The time zone plugin merely allows us to include a reference to a specific time zone which will be taken into account when manipulating the date with certain methods and formatting the date as a string.
const date = moment('2018-09-13 12:25:00');
date.unix();
// 1536863100
date.format('HH:mm')
// ‘12:25’
date.tz('America/New_York')
// "14:25"
date.unix()
// 1536863100
In this case, we are simply using Moment to display a date in a specific time zone. The underlying UTC date never changes.
Manipulating a Date
Whereas the format() method in Moment simply outputs a string representing a date, other methods manipulate the underlying date object. It’s important to take time zone into consideration when manipulating a date with certain operations to ensure the resulting UTC date is correct.
For example, Moment has a handy startOf()
method that lets you set the date to, for example, the start of the day.
We instinctively think of the start of day as being 12:00 AM. But 12:00 AM in Denver is a different UTC time than 12:00 AM in New York. Therefore, it’s important to ensure our Moment object is set to the desired time zone before manipulating it. Otherwise we will get different results depending on the time of day and local time zone in which the method was executed.
Parsing a Date
In some cases, we need to parse a date. For instance, to correctly convert a Date Time field from Drupal into a JS Date object, we need to ensure it’s parsed into the right time zone. This is pretty straightforward as the date output from Drupal is in UTC. However, the client doesn’t know that. We can simply append the Z
designator to indicate this date in UTC.
moment(`${someDateValue}Z`);
I typically wrap this in a function for easy reuse:
export const dateFromDrupal = date => moment(`${date}Z`).toDate();
And going the reverse direction:
export const dateToDrupal = date => date.toISOString().replace('.000Z', '');
Another use case for parsing dates is when handling user input. For example, if you have a filtering interface in which you need to show events on a given day. The user needs to enter a date. The HTML date input uses a simple string representation of a day, such as 2018-09-15
– in the user’s local time zone.
Now, if we want to take this input and query Drupal for events with a start time between the start and end of this day, we’ll need to convert this string value into a UTC date. We’ll need to do so by parsing this date in the user’s time zone, otherwise we might not get accurate results. In fact, they could be as much as a day off from what we’re expecting.
function changeEventHandler(event) {
if (event.target.value) {
const inputDate = moment.tz(event.target.value, userTimeZone);
const startValue = inputDate.startOf(‘day’);
const endValue = inputDate.endOf(‘day’);
// Do something with the start and end date.
}
}
Date and time zone handling is one thing you should not take for granted when considering decoupling a Drupal application. Having a good understanding of how dates are stored and manipulated will help you identify, diagnose and avoid date related bugs.
Keep these things in mind throughout the development process and you should be fine:
- Store your dates and pass them around in UTC. This will help keep things consistent and reduce any time zone related bugs.
- Take time zone into account when dealing with user input and output. Time zones are relative to the user, so keeping those operations as close to the user interface as possible should keep the internals of the application simple.
- When considering any open source modules that deal with dates, such as React components or date handling libraries, make sure they allow for proper time zone handling. This will save you some headaches down the road.
- Test your application using different user time zones in Drupal and by manually overriding your time zone on your local machine. Sometimes bugs are not apparent until you are on a drastically different time zone than the server.
Good luck!