data types and standard library
Internationalization
Internationalization, often shortened to i18n, is the work that lets an application support different languages, countries, date formats, number formats, currencies, sorting rules, and plural rules.
It is not only translation. A page can be translated and still show money, dates, names, and sorting in a way that feels wrong for the user's locale. In PHP, much of this work uses the intl extension.
Format currency by locale
Use NumberFormatter when displaying money to users.
<?php
declare(strict_types=1);
$formatter = new NumberFormatter('fr_FR', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency(1299.50, 'EUR') . PHP_EOL;
// Prints:
// 1 299,50 €
The stored amount should still be precise, often as integer minor units such as pennies or cents. The formatter is for display.
Format numbers by locale
Decimal and thousands separators vary by locale.
<?php
declare(strict_types=1);
$formatter = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
echo $formatter->format(1234567.89) . PHP_EOL;
// Prints:
// 1.234.567,89
Do not build this with str_replace(). Locale rules are broader than swapping a comma and a dot.
Format dates for humans
Use IntlDateFormatter for user-facing dates. Keep storage and comparison as DateTimeImmutable values.
<?php
declare(strict_types=1);
$date = new DateTimeImmutable('2026-05-20 09:00', new DateTimeZone('Europe/Paris'));
$formatter = new IntlDateFormatter(
'fr_FR',
IntlDateFormatter::LONG,
IntlDateFormatter::SHORT,
'Europe/Paris'
);
echo $formatter->format($date) . PHP_EOL;
// Prints:
// 20 mai 2026 à 09:00
Actual punctuation can vary by ICU version, so tests should focus on the chosen locale and the important date parts rather than brittle spacing.
Keep translation keys separate from text
A simple translation map shows the shape. Larger applications usually use framework translation files.
<?php
declare(strict_types=1);
$translations = [
'en_GB' => ['checkout.pay' => 'Pay now'],
'fr_FR' => ['checkout.pay' => 'Payer maintenant'],
];
function translate(array $translations, string $locale, string $key): string
{
return $translations[$locale][$key] ?? $translations['en_GB'][$key] ?? $key;
}
echo translate($translations, 'fr_FR', 'checkout.pay') . PHP_EOL;
// Prints:
// Payer maintenant
Use stable keys in code. Do not use the English sentence itself as the only identifier.
Handle plural text deliberately
Plural rules differ between languages. A small English-only function is acceptable for a narrow internal tool, but international products should use a message formatter or framework pluralisation support.
<?php
declare(strict_types=1);
function englishItemCount(int $count): string
{
return $count === 1 ? '1 item' : $count . ' items';
}
echo englishItemCount(1) . PHP_EOL;
echo englishItemCount(3) . PHP_EOL;
// Prints:
// 1 item
// 3 items
The important habit is not concatenating translated fragments such as "You have " . $count . " items" when the whole sentence may need to change.
Sort names with a collator
Sorting user-facing text is locale-sensitive.
<?php
declare(strict_types=1);
$names = ['Élodie', 'Alice', 'Zoë'];
$collator = new Collator('fr_FR');
$collator->sort($names);
echo implode(', ', $names) . PHP_EOL;
// Prints:
// Alice, Élodie, Zoë
Plain sort() compares byte or binary-ish order, which can produce surprising results for accented text.
Decide where the locale comes from
Applications usually choose a locale from user settings, route prefixes, account settings, or the Accept-Language header. Whatever the source, validate it against supported locales.
<?php
declare(strict_types=1);
function supportedLocale(string $requestedLocale): string
{
$supported = ['en_GB', 'fr_FR', 'de_DE'];
return in_array($requestedLocale, $supported, true) ? $requestedLocale : 'en_GB';
}
echo supportedLocale('fr_FR') . PHP_EOL;
echo supportedLocale('unknown') . PHP_EOL;
// Prints:
// fr_FR
// en_GB
Do not let arbitrary locale strings flow through the application unchecked.
What to remember
Internationalization covers translation, formatting, parsing, sorting, plural rules, and locale selection. Store data in stable application formats, then format for the user's locale at the output boundary. Use the intl extension or framework i18n tools instead of hand-building locale rules.
Practice
Task: Format an order summary for a locale
Write a small order summary formatter.
Requirements
- Use
declare(strict_types=1);. - Accept a locale, currency code, total in minor units, and item count.
- Validate the locale against a small supported list.
- Format the money with
NumberFormatter. - Use a simple English/French translation map for the label.
- Include a simple plural-aware item count for English and French.
- Print a French summary and a fallback English summary.
- Include the expected output as comments in the same PHP code block.
The example should keep stored data separate from user-facing formatting.
Show solution
<?php
declare(strict_types=1);
function supportedLocale(string $locale): string
{
return in_array($locale, ['en_GB', 'fr_FR'], true) ? $locale : 'en_GB';
}
function itemCountLabel(string $locale, int $count): string
{
if ($locale === 'fr_FR') {
return $count === 1 ? '1 article' : $count . ' articles';
}
return $count === 1 ? '1 item' : $count . ' items';
}
function orderSummary(string $locale, string $currency, int $totalMinorUnits, int $itemCount): string
{
$locale = supportedLocale($locale);
$labels = [
'en_GB' => 'Order total',
'fr_FR' => 'Total de la commande',
];
$formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
$amount = $totalMinorUnits / 100;
$money = $formatter->formatCurrency($amount, $currency);
if (!is_string($money)) {
throw new RuntimeException('Currency could not be formatted.');
}
return $labels[$locale] . ': ' . $money . ' (' . itemCountLabel($locale, $itemCount) . ')';
}
echo orderSummary('fr_FR', 'EUR', 129950, 3) . PHP_EOL;
echo orderSummary('unknown', 'GBP', 2499, 1) . PHP_EOL;
// Prints:
// Total de la commande: 1 299,50 € (3 articles)
// Order total: £24.99 (1 item)
The stored total remains an integer number of minor units. Locale handling happens at the output boundary, where the formatter, label, and plural text can match the user's supported locale.