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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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.