Localizing Dates, Currency, and Numbers with Php-Intl
The first part of this series was an introduction of the PHP Intl extension and of how to localize your application’s messages. In this part, we’re going to learn about localizing numbers, dates, calendars, and similar complex data. Let’s get started!
Localizing Decimals
This may sound odd, but one of my main concerns when formatting numbers is working with decimal points, as they differ from place to place. Check Wikipedia for more details about different decimal mark variations.
Style |
---|
1,234,567.89 |
1234567.89 |
1234567,89 |
1,234,567·89 |
1.234.567,89 |
1˙234˙567,89 |
12,34,567.89 |
1’234’567.89 |
1’234’567,89 |
1.234.567’89 |
123,4567.89 |
The PHP Intl extension has a NumberFormatter
which deals with number localization:
$numberFormatter = new NumberFormatter( 'de_DE', NumberFormatter::DECIMAL );
var_dump( $numberFormatter->format(123456789) );
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DECIMAL );
var_dump( $numberFormatter->format(123456789) );
$numberFormatter = new NumberFormatter( 'ar', NumberFormatter::DECIMAL );
var_dump( $numberFormatter->format(123456789) );
$numberFormatter = new NumberFormatter( 'bn', NumberFormatter::DECIMAL );
var_dump( $numberFormatter->format(123456789) );
string(11) "123.456.789"
string(11) "123,456,789"
string(22) "١٢٣٬٤٥٦٬٧٨٩"
string(30) "১২,৩৪,৫৬,৭৮৯"
The first parameter is the locale code, and the second is the formatting style. In this case, we’re formatting decimals.
Formatting Styles
Formatting styles describe how our numbers should be formatted: decimal, currency, duration, etc. Check the list of available formatting styles in the documentation. Let’s try some examples for different styles:
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DECIMAL );
$numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
var_dump( $numberFormatter->format(1234.56789) );
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DECIMAL );
$numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
var_dump( $numberFormatter->format(1234) );
string(8) "1,234.57"
string(8) "1,234.00"
When specifying the fractional part attribute, the value is rounded up using the up mode. We can change that by specifying the rounding style.
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DECIMAL );
$numberFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
$numberFormatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_CEILING);
var_dump($numberFormatter->format(1234.5678) );
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DECIMAL );
$numberFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
$numberFormatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_DOWN);
var_dump($numberFormatter->format(1234.5678) );
string(8) "1,234.57"
string(8) "1,234.56"
The spellout
and duration
options from the first part can also be used here with the following options:
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DURATION );
var_dump( $numberFormatter->format(123) );
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::SPELLOUT );
var_dump( $numberFormatter->format(123) );
string(4) "2:03"
string(24) "one hundred twenty-three"
We can also parse strings to get a formatted value from them:
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::DURATION );
var_dump( $numberFormatter->parse("4:03") );
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::SPELLOUT );
var_dump( $numberFormatter->parse("one hundred") );
float(243)
float(100)
Although we can totally make our application locale agnostic, we should switch between locales to test the various changes.
Localizing Currencies
Formatting numbers as currencies is not very different, we only change the formatting type and add the currency code.
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::CURRENCY );
var_dump( $numberFormatter->formatCurrency(1234.56789, "USD" ) );
string(9) "$1,234.57"
We can avoid typing the currency code for every locale by calling the getSymbol
method on the NumberFormatter
instance.
$numberFormatter = new NumberFormatter( 'en_US', NumberFormatter::CURRENCY );
var_dump( $numberFormatter->formatCurrency(1234.56789, $numberFormatter->getSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL)) );
$numberFormatter = new NumberFormatter( 'fr_FR', NumberFormatter::CURRENCY );
var_dump( $numberFormatter->formatCurrency(1234.56789, $numberFormatter->getSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL)) );
string(9) "$1,234.57"
string(14) "1 234,57 €"
We can use the attributes we mentioned earlier ($numberFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
) with currencies, too.
Timezones
Before using PHP Intl calendars, we need to brush up on timezones and try a quick example:
A time zone is a region that observes a uniform standard time for legal, commercial, and social purposes. Time zones tend to follow the boundaries of countries and their subdivisions because it is convenient for areas in close commercial or other communication to keep the same time.
— Wikipedia
The IntlTimeZone class is responsible for creating and managing timezones. This is no different from timezones in the DateTimeZone class:
$timezone = IntlTimeZone::createDefault();
var_dump($timezone, $timezone->getDisplayName());
$timezone = IntlTimeZone::countEquivalentIDs("GMT");
var_dump($timezone);
$timezone = IntlTimeZone::createTimeZone("GMT");
var_dump($timezone);
object(IntlTimeZone)#1 (4) {
["valid"]=>
bool(true)
["id"]=>
string(3) "UTC"
["rawOffset"]=>
int(0)
["currentOffset"]=>
int(0)
}
string(3) "GMT"
int(10)
object(IntlTimeZone)#1 (4) {
["valid"]=>
bool(true)
["id"]=>
string(3) "GMT"
["rawOffset"]=>
int(0)
["currentOffset"]=>
int(0)
}
Calendars
The PHP Intl extension has a nice, expressive API for doing calendar operations:
$calendar = IntlCalendar::createInstance();
var_dump($calendar->getTimeZone()->getId());
string(3) "UTC"
The createInstance
method accepts a timezone (from the previous section), and a locale code. We can also create a calendar instance from a DateTime
instance.
$calendar = IntlCalendar::fromDateTime( (new DateTime) );
We can do all sorts of comparisons between calendar dates.
$calendar1 = IntlCalendar::fromDateTime( DateTime::createFromFormat('j-M-Y', '11-Apr-2016') );
$calendar2 = IntlCalendar::createInstance();
$durationFormatter = new NumberFormatter( 'en_US', NumberFormatter::DURATION );
$diff = $calendar1->fieldDifference($calendar2->getTime(), IntlCalendar::FIELD_MILLISECOND);
var_dump(
$calendar1->equals($calendar2),
$diff,
$durationFormatter->format( $diff )
);
bool(true)
int(595)
string(4) "9:55"
Important note: Our calendar1
variable is advanced by the diff
value, so the calendars are equal after the fieldDifference
method call. Keep this mutability in mind when using these classes!
The IntlCalendar::FIELD_MILLISECOND
constant defines the type of comparison, year, month, week, etc. Check the documentation for the full list.
If you’ve used the briannesbitt/carbon
package before, you may have liked the expressive syntax for navigating dates. We can do the same using the IntlCalendar
class.
$calendar1 = IntlCalendar::createInstance();
var_dump(IntlDateFormatter::formatObject($calendar1));
$calendar1->add(IntlCalendar::FIELD_MONTH, 1);
var_dump(IntlDateFormatter::formatObject($calendar1));
$calendar1->add(IntlCalendar::FIELD_DAY_OF_WEEK, 1);
var_dump(IntlDateFormatter::formatObject($calendar1));
$calendar1->add(IntlCalendar::FIELD_WEEK_OF_YEAR, 1);
var_dump(IntlDateFormatter::formatObject($calendar1));
string(25) "Apr 12, 2016, 12:06:22 AM"
string(25) "May 12, 2016, 12:06:22 AM"
string(25) "May 15, 2016, 12:06:22 AM"
string(25) "May 22, 2016, 12:06:22 AM"
Check the documentation for more details and examples.
Conclusion
In this two part series, we discovered the PHP Intl extension and the ICU library. The extension still has some other parts like Collators, Spoofchecker, UConverter, etc. We’ll focus on those in subsequent posts, but in the meanwhile, if you have any questions or comments, please leave them below!