commerceguys/intl
commerceguys/intl is a PHP internationalization library that provides country, currency, language, and locale data with locale-aware formatters. It helps commerce and multilingual apps display consistent, CLDR-based names and currency values.
Install the package directly — there is no Laravel service provider, facade, or config file, so you wire it yourself in one place:
composer require commerceguys/intl
// app/Providers/IntlServiceProvider.php
namespace App\Providers;
use CommerceGuys\Intl\Country\CountryRepository;
use CommerceGuys\Intl\Currency\CurrencyRepository;
use CommerceGuys\Intl\Formatter\CurrencyFormatter;
use CommerceGuys\Intl\Formatter\NumberFormatter;
use CommerceGuys\Intl\Language\LanguageRepository;
use Illuminate\Support\ServiceProvider;
class IntlServiceProvider extends ServiceProvider
{
public function register(): void
{
// Data repositories are expensive to construct: make them singletons.
$this->app->singleton(CurrencyRepository::class, fn () => new CurrencyRepository());
$this->app->singleton(CountryRepository::class, fn () => new CountryRepository());
$this->app->singleton(LanguageRepository::class, fn () => new LanguageRepository());
// Formatters are cheap and locale-bound: build them per binding, not per container.
$this->app->bind(CurrencyFormatter::class, function ($app) {
return new CurrencyFormatter(
$app->make(CurrencyRepository::class),
new NumberFormatter($app->getLocale(), NumberFormatter::DECIMAL)
);
});
}
}
First use case — render a price in a Blade view or API resource:
$formatter = app(CurrencyFormatter::class);
echo $formatter->format(1299.5, 'USD'); // "$1,299.50" (en_US)
Where to look first in the source: src/Repository/ for the four *Repository classes (get(), getAll(), getList() are the whole API surface), src/Formatter/ for NumberFormatter (decimal/percent) and CurrencyFormatter, and data/*.json for the CLDR/ISO payloads you can inspect or extend.
Repository + formatter composition. Every workflow follows the same shape: pull a definition object from a repository (Currency, Country, Language, Locale), then hand its data to a formatter or your own presenter. The definition objects are plain value objects — getCurrencyCode(), getSymbol(), getFractionDigits(), getNumericCode() — so they're ideal for serialising into API responses.
Custom validation rules. Wrap the exception-throwing lookups in Laravel rules so checkout forms validate ISO codes without extra dependencies:
class CountryCode implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
app(CountryRepository::class)->get($value);
} catch (\CommerceGuys\Intl\Exception\ExceptionInterface) {
$fail("The {$attribute} is not a valid country code.");
}
}
}
Do the same for CurrencyCode, LanguageCode, and locale codes. Catching the marker interface ExceptionInterface rather than \Exception keeps it narrow.
Localised select lists. For a country dropdown or a currency picker, skip your own table: CountryRepository::getList() and CurrencyRepository::getList() return code => name arrays ready for collect()->map() into a pluck() for forms. Cache them (Cache::rememberForever) — the data is static per package version.
Prefilling a default currency from a country. CountryRepository::get($countryCode)->getCurrencyCode() gives you the currency used in that country, which is the right default when a user picks a shipping address rather than a currency directly.
Money handling. Store raw amounts plus the ISO currency code in the database; format only at the edge (Blade component, JsonResource, mailable). If you need a cast, format in serialize()/toArray(), never in the raw attribute — you still want to sum and compare numbers in PHP.
Locale-aware formatter factory. In a multi-locale app, resolve formatters through a small factory keyed by locale rather than binding one per request in a container binding, so a single job handling several recipients doesn't rebuild the same formatter repeatedly:
class CurrencyFormatterFactory
{
private array $cache = [];
public function __construct(private CurrencyRepository $currencies) {}
public function for(string $locale): CurrencyFormatter
{
return $this->cache[$locale] ??= new CurrencyFormatter(
$this->currencies,
new NumberFormatter($locale, NumberFormatter::DECIMAL)
);
}
}
Pair with commerceguys/addressing. That package consumes these repositories for address formats and subdivisions; if you're building checkout, install both and let the country repository drive the address form fields.
ext-intl. This package defines CommerceGuys\Intl\Formatter\NumberFormatter, which is not PHP's \NumberFormatter. Never import both unaliased in one file — use CommerceGuys\Intl\Formatter\NumberFormatter as IntlNumberFormatter; saves hours of confusion.fr_FR). Assertions like assertSame('1 234,56 €', $out) fail on invisible bytes, and strings echoed into HTML can wrap oddly. Normalise with str_replace(["\u{00A0}", "\u{202F}"], ' ', $s) in tests, not in production output.number_format($x, 2) for storage conversion — use getFractionDigits() from the Currency object when converting to/from minor units.config('app.locale') is typically en, fr, de, but CLDR locales are en_US, fr_FR. Passing Laravel's value straight into new NumberFormatter(...) throws UnknownLocaleException for some values, and silently changes symbol choice for others ($ vs US$). Map explicitly, e.g. config('app.cldr_locale', 'en_US'), and keep a hard fallback.new CurrencyRepository() inside a loop is the single most common performance mistake with this package.CurrencyFormatter built from app()->getLocale() will leak the first request's locale into every subsequent one under Octane. Bind formatters as scoped/transient or go through the locale-keyed factory above.ExceptionInterface. All package exceptions (UnknownCountryException, UnknownCurrencyException, UnknownLanguageException, UnknownLocaleException) implement CommerceGuys\Intl\Exception\ExceptionInterface and extend \InvalidArgumentException. Catching \Exception around formatting will swallow real bugs.NumberFormatter::PERCENT, pass 0.75 for 75%, not 75. Verify against your version before trusting it in invoices.vendor/commerceguys/intl/data/currencies.json, append crypto or internal pseudo-currencies, and point a dedicated repository instance at it — cleaner than post-processing lists in your app, and it keeps validation rules consistent.composer.json if formatted output is contractual, and review the diff when upgrading.How can I help you explore Laravel packages today?