What is CLDR (Common Locale Data Repository)?

CLDR, the Common Locale Data Repository, is a dataset maintained by the Unicode Consortium that describes locale conventions for the world's languages: plural rules, date and time patterns, number and currency formats, sort orders, and translated names for languages, regions, and time zones. The data is compiled from contributions by native speakers and member organizations, and it is released on a regular cadence.

Its importance is that almost everything consumes it. The ICU library ships CLDR data, and through ICU it reaches operating systems, browsers, databases, and language runtimes. When PHP's intl extension formats a date for de_DE or picks the right plural category for Polish, the answer ultimately comes from CLDR.

For developers, the practical takeaway is: do not hand-roll locale conventions. If you find yourself writing a switch statement over language codes to decide decimal separators or plural forms, there is CLDR-backed machinery (NumberFormatter, MessageFormatter, IntlDateFormatter) that already knows the answer and gets updated as conventions evolve.

What CLDR data looks like from PHP

Every class in PHP's intl extension is a window onto CLDR. NumberFormatter knows each locale's separators and grouping, including systems that do not group by thousands. IntlDatePatternGenerator knows the order of day and month and the punctuation between them. Locale::getDisplayName() returns language and region names translated into any language, Collator sorts strings the way a given language alphabetizes them, and IntlCalendar knows which day starts the week.

None of this needs configuration or data files of your own. The output below comes straight from the CLDR release bundled with the ICU library PHP was built against.

(new NumberFormatter('en_IN', NumberFormatter::DECIMAL))->format(12345678.9); // "1,23,45,678.9"
(new NumberFormatter('de_CH', NumberFormatter::DECIMAL))->format(12345678.9); // "12’345’678.9"

IntlDatePatternGenerator::create('en_US')->getBestPattern('MMMd'); // "MMM d"
IntlDatePatternGenerator::create('de_DE')->getBestPattern('MMMd'); // "d. MMM"

Locale::getDisplayName('pt_BR', 'fr');                        // "portugais (Brésil)"
IntlCalendar::createInstance(null, 'de_DE')->getFirstDayOfWeek(); // 2 (Monday)

$letters = ['z', 'ä', 'a', 'ö', 'å'];
(new Collator('sv'))->sort($letters); // ['a', 'z', 'å', 'ä', 'ö']: Swedish puts å, ä, ö after z

Your CLDR version comes from ICU

PHP does not download CLDR. The intl extension is compiled against an ICU library, and each ICU release bundles one CLDR release, so the locale data you get depends on how PHP was built. A Homebrew PHP on a laptop, a distribution package in CI and a container image in production can ship three different versions.

The differences are real. Since CLDR 42, English time formats put a narrow no-break space (U+202F) before AM and PM instead of a regular space. A test that compares a formatted time with 2:30 PM typed on a keyboard passes on an older ICU and fails on a newer one, and the two strings look identical in the failure message. Check the version in each environment, keep them aligned where you can, and compare formatted output against the same formatter instead of hardcoded strings.

echo INTL_ICU_VERSION; // e.g. "77.1"
echo ResourceBundle::create('supplementalData', 'ICUDATA', false)['cldrVersion']; // e.g. "47"

$time = (new IntlDateFormatter('en_US', IntlDateFormatter::NONE, IntlDateFormatter::SHORT, 'UTC'))
    ->format(new DateTime('2026-07-09 14:30', new DateTimeZone('UTC')));

bin2hex($time); // "323a3330e280af504d": e280af is U+202F, not a space

Where Laravel and Carbon get their locale rules

Not everything in a Laravel app reads CLDR at runtime. The Number helper does: it is a thin wrapper around NumberFormatter, so Number::format() and Number::currency() follow CLDR exactly. trans_choice() does not: its plural rules are a hand-written switch over locale codes inside Laravel's MessageSelector, and a locale missing from that list, or written with a hyphen, always gets the first form.

Carbon ships its own translation files per locale instead of reading ICU, so Carbon and IntlDateFormatter can disagree on details for the same locale. Carbon's English LT format, for instance, uses an ordinary space before PM where ICU uses U+202F. When two sources disagree, CLDR through intl is the one to check against; browsers expose the same data through the Intl APIs, such as Intl.PluralRules for plural categories.

Common mistakes

Storing formatted values is the most lasting one. A price or date saved as display text is frozen in one locale and one CLDR version; store the raw value and format it on output. The second is treating locale data as constant: CLDR adjusts separators, abbreviations and spacing between releases, so snapshot tests of formatted output need regenerating after an ICU upgrade, and a failure there is a prompt to look rather than proof of a bug. The third is using a language-only locale where the region matters, such as de instead of de_CH for Swiss number formatting, where the grouping separator is an apostrophe rather than a period.

Put the theory to work

LangSyncer manages your Laravel translations live: import lang files, translate with AI and publish without a deploy.

We'd like to use analytics and marketing cookies to understand how the site is used. They stay off unless you accept them. Cookie Policy