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.
- https://www.php.net/manual/en/class.intldateformatter.php
- https://en.wikipedia.org/wiki/Internationalization_and_localization
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:
ini_get('intl.default_locale')
setlocale(LC_ALL, '0');
$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 dateIntlDateFormatter::FULL
with weekdayIntlDateFormatter::LONG
IntlDateFormatter::MEDIUM
IntlDateFormatter::SHORT
$timeType
If NULL
, it will select IntlDateFormatter::FULL
.
IntlDateFormatter::NONE
to skip the output of the timeIntlDateFormatter::FULL
with time zone descriptionIntlDateFormatter::LONG
IntlDateFormatter::MEDIUM
IntlDateFormatter::SHORT
$timezone
If NULL
, it will lookup its default value in this order:
date_default_timezone_get()
ini_get('date.timezone')
'UTC'
$calendar
If NULL
, it will select IntlDateFormatter::GREGORIAN
.
$pattern
-
https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
-
If not
NULL
, it will override$dateType
and$timeType
arguments. -
If
NULL
, it will defer to$dateType
and$timeType
arguments.