In my two previous articles(1,2), we saw how to use PHP's IntDateFormatter to display dates in a format that is typical for different locales. In this one, we'll look at the techniques and issues related to getting a user's locale and timezone.
Locale
In my previous article, the code produced a date and time in various formats depending on the $locale variable set in the code. As you probably figured out, if you're just going to serve one locale for all users, you might as well use the date() function and skip all the details that come with the IntlDateFormatter.
Since you're sending the locale as an argument to the IntlDateFormatter, you don't have to set the locale explicitly, but you may want to retrieve it for debugging. In very old versions of PHP, this was done using the setLocale() function, with this odd bit of code:
$locale = setlocale(LC_ALL, 0);
The code above (which still works) makes use of a trick that should work in any version of PHP. The setLocale() function always returns the current locale setting (assuming that one is set). Using 0 for the second argument tells PHP not to alter the current setting even though you're calling setLocale().
A simpler (and more readable and reliable) method has been available since PHP 5.3:
/* Object-oriented version */
$locale = Locale::getDefault();
/* Procedural version */
$locale = locale_get_default();
Both versions above do the same thing. They return a string containing the current locale setting (e.g., en or en_US).
If you have, or are creating, a multi-language site, you may already have access to the locale, either through the URL or as part of whatever multi-language extra or plugin you're using. If you're explicitly setting the locale somewhere (more on this in a bit), you can get it with one of the function calls just above. If you want the locale of a visiting user to your website, you can ask PHP to get it for you with this code:
$locale = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
This isn't foolproof, so you should always set a default value like this:
$locale = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
if (empty($locale)) {
$locale = 'en_US';
}
Another, fairly foolproof method is to create a form and simple ask the user to select a locale from a drop-down list, then set a User Setting with the key locale to hold the value. Then, whenever you need it, you can get it with this code:
$locale = $modx->getOption('locale', null);
As you might guess, this will only work properly for logged-in users. For others, and for users that don't have it set yet, it will fall back to the value of the MODX locale System Setting (if it's set).
Setting the Locale
As I mentioned above, you usually don't need to do this, since you'll be sending the locale as an argument to the IntlDateFormatter. Sending the argument is the preferred method because you don't have to worry about some other bit of code setting a different locale before your code executes.
If you're determined to set the default locale, calling setLocale() is not the recommended method. Instead, use one of these methods, which have been available since PHP 5.3:
/* Object-oriented version */
Locale::SetDefault('en-US');
/* Procedural version */
locale_set_default('en-US');
Both versions return true on success and false on failure. Be sure to test the return value and handle failures with something like this:
$locale = 'en-US');
$success = Locale::SetDefault($locale);
if (! $success) {
$modx->log(modX::LOG_LEVEL_ERROR,
'Could not set locale with string: ' . $locale);
}
If you want to use the currently set locale, you can send NULL for the $locale argument to the IntlDateFormatter, but it's wise to make sure it's set before doing so.
TimeZone
The whole issue of user timezones is a rat's nest. There is no easy way to get the user's timezone in PHP. You can send the user's IP number an API that will return a timezone, but it's slow, the user's IP may not reflect where they live, and the API servers often either go away, or begin requiring a paid subscription, like this one.
You can also do it with JavaScript, but it's not always reliable, and you have to set things up so your own code can communicate with the JS code. The JS code below will get the user's timezone, and additional JS code could display it, but in order to use the value in your PHP code, you might have to create a processor that receives an AJAX call and saves the value somewhere (maybe in the User Setting we mentioned earlier).
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
const getOffset = (tz) => Intl.DateTimeFormat(
"ia", {
timeZoneName: "shortOffset",
timeZone : tz
})
.formatToParts()
.find((i) => i.type === "timeZoneName").value // => "GMT+/-hh:mm"
.slice(3); //=> +/-hh:mm
console.log(tz + ' UTC' + getOffset(tz))
At some point, you have to wonder if your users actually need to see the exact time when a resource was saved, published, or edited, and need to see it as if it occurred at their own location. The MODX timestamps will report the time that these things happened. I'm fairly that they are in UTC, though I'm not positive. If you use the timezone of your server in any calls to IntDateFormatter, the time should be corrected to the server's timezone, which may be good enough. Or, you can just leave out the time altogether.
MODX returns a timezone from the value of the date_timezone System Setting.
$timezone = $modx->getOption('date_timezone', null);
In response to the code above, MODX uses the value of a System Setting with the key (date_timezone), or any setting that overrides that one. If no settings with that name have a value, it uses the PHP date.timezone, set in the php.ini file, the .htaccess file, or in code with a call to date_default_timezone_set(). It none of that has happened, UTC (Coordinated Universal Time, aka Greenwich Mean Time, or GMT) will be assumed.
Of course, you can always ask the user to select a timezone from a dropdown list and store the value in a date_timezone user setting, which will override the System Setting. As with the locale, this will only work for logged-in users for whom the User Setting has a value. Others will get the time based on the date_timezone System Setting.
Once the value is stored, the code would be the same as the one-line code snippet just above.
About Bob Ray
Bob Ray is the author of the MODX: The Official Guide and dozens of MODX Extras including QuickEmail, NewsPublisher, SiteCheck, GoRevo, Personalize, EZfaq, MyComponent and many more. His website is Bob’s Guides. It not only includes a plethora of MODX tutorials but there are some really great bread recipes there, as well.
Learn more about Bob Ray.