Localizing Dates, Currency, and Numbers with Php-Intl

Younes Rafie
Share

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!

Globe stock illustration

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!