Localize datetime with IntlDateFormatter

We don't need to create a framework to format localized datetime values. We just need to implement the Intl extension in PHP, either using the object-oriented methods or, even simpler, the procedural functions.

This extension has been available since PHP version 5.3.0.

Understanding localization in PHP

The IntlDateFormatter format() method is like a combination of gettext() and printf(): it looks up a localized format and merges it with a provided value.

gettext() primer

gettext(), with its alias _(), is a simple lookup function: we feed it a key, normally a string in a default language, and it returns its associated value, the translation. The key/value pairs are located in a messages.po files in a directory tree categorized by locale. These key/value pairs are cached by the web server for fast returns.

setlocale(LC_ALL, 'fr_CA');
echo  _('This is text.'); //-> "Ceci est un texte."

printf() primer

printf() is a function that merges values to a formatted string with placeholders.

printf("The string is %s and the number is %d.", 'simple', 25); //-> "The string is simple and the number is 25."

We can combine gettext() and printf():

printf(_('The number is %d.'), 5); //-> "Le numéro est 5."

datefmt_format() primer

datefmt_format() is a lookup function that merges a value to a locale-formatted string.

$fmt = datefmt_create('en_US', IntlDateFormatter::FULL, IntlDateFormatter::FULL, 'America/Los_Angeles', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd h:mm a');
echo datefmt_format($fmt, strtotime('now')); // -> 2023-04-01 4:01 PM

datefmt_format() has an OOP equivalent: IntlDateFormatter::format().

$fmt = new IntlDateFormatter('en_US', IntlDateFormatter::MEDIUM, IntlDateFormatter::MEDIUM, 'America/Los_Angeles');
echo $fmt->format(strtotime('now')); // -> Apr 1, 2023, 4:01:18 PM

We can use the Facade version of the create() method:

$fmt = IntlDateFormatter::create('en_US', IntlDateFormatter::MEDIUM, IntlDateFormatter::MEDIUM, 'America/Los_Angeles');
echo $fmt->format(strtotime('now')); // -> Apr 1, 2023, 4:01:18 PM

As we can see, the datefmt_format() function or the IntlDateFormatter::format() method need to happen in two lines, unless we go with a very long line:

echo datefmt_format(datefmt_create('en_US', IntlDateFormatter::FULL, IntlDateFormatter::FULL, 'America/Los_Angeles', IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd h:mm a'), strtotime('now')); //-> 2023-04-01 4:01 PM

The shortest version of this is:

echo datefmt_format(datefmt_create(NULL), strtotime('now')); //-> Sunday, April 2, 2023 at 3:45:18 AM Coordinated Universal Time

Encapsulating IntlDateFormatter methods

We could encapsulate datefmt_create() and datefmt_format() in one practical custom function with default options that we can override:

function datetime_format($datetime, array $kwargs = []) {
    extract($kwargs + [
        'date_type' => IntlDateFormatter::SHORT,
        'time_type' => IntlDateFormatter::SHORT,
        'locale'    => 'en_US',
        'pattern'   => NULL,
        'timezone'  => NULL,
    ]);
    $fmt = datefmt_create($locale, $date_type, $time_type, $timezone, NULL, $pattern);
    return datefmt_format($fmt, $datetime);
}
echo datetime_format(strtotime('now'), [
    'date_type' => IntlDateFormatter::MEDIUM,
    'time_type' => IntlDateFormatter::MEDIUM,
]); //-> Apr 1, 2023, 4:01:18 PM

Using a default Locale

The Intl extension provides the Locale class to get and set a default locale identifier.

Let's use it in a web application context. We set it in the bootstrap section of a web application:

Locale::setDefault('fr_FR');

We modify our custom function to use the default application locale.

function datetime_format($datetime, array $kwargs = []) {
    extract($kwargs + [
        'date_type' => IntlDateFormatter::SHORT,
        'time_type' => IntlDateFormatter::SHORT,
-       'locale'    => 'en_US',
+       'locale'    => substr(Locale::getDefault(), 0, 5) ?: NULL,
        'pattern'   => NULL,
        'timezone'  => NULL,
    ]);
    $fmt = datefmt_create($locale, $date_type, $time_type, $timezone, NULL, $pattern);
    return datefmt_format($fmt, $datetime);
}

Then, on a page, we localize a datetime value based on the application's locale:

echo datetime_format(strtotime('now'), [
    'date_type' => IntlDateFormatter::FULL,
    'time_type' => IntlDateFormatter::NONE,
]); //-> samedi 1 avril 2023

datefmt_create() arguments

datefmt_create(?string $locale, int $dateType = IntlDateFormatter::FULL, int $timeType = IntlDateFormatter::FULL, IntlTimeZone|DateTimeZone|string|null $timezone = null, IntlCalendar|int|null $calendar = null, ?string $pattern = null): ?IntlDateFormatter

$locale

If NULL, it will lookup its default value in this order:

  1. ini_get('intl.default_locale')
  2. setlocale(LC_ALL, '0');
  3. $LANG value from the set locale of the operating system.

$dateType

If NULL, it will select IntlDateFormatter::FULL.

  • IntlDateFormatter::NONE to skip the output of the date
  • IntlDateFormatter::FULL with weekday
  • IntlDateFormatter::LONG
  • IntlDateFormatter::MEDIUM
  • IntlDateFormatter::SHORT

$timeType

If NULL, it will select IntlDateFormatter::FULL.

  • IntlDateFormatter::NONE to skip the output of the time
  • IntlDateFormatter::FULL with time zone description
  • IntlDateFormatter::LONG
  • IntlDateFormatter::MEDIUM
  • IntlDateFormatter::SHORT

$timezone

If NULL, it will lookup its default value in this order:

  1. date_default_timezone_get()
  2. ini_get('date.timezone')
  3. 'UTC'

$calendar

If NULL, it will select IntlDateFormatter::GREGORIAN.

$pattern