Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Intl Laravel Package

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.

Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Name collision with 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.
  • Non-breaking spaces in output. CLDR patterns use U+00A0 and U+202F (e.g. between symbol and amount in 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.
  • Fraction digits are not always 2. The formatter uses the currency's CLDR exponent: JPY = 0, USD = 2, BHD/KWD = 3. Never hardcode number_format($x, 2) for storage conversion — use getFractionDigits() from the Currency object when converting to/from minor units.
  • Locale identifier formats don't match. Laravel's 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.
  • Singletons, not transients, for repositories. Each repository parses a multi-hundred-KB JSON file on construction. Resolving new CurrencyRepository() inside a loop is the single most common performance mistake with this package.
  • Octane / queue workers. A singleton 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.
  • Catch ExceptionInterface. All package exceptions (UnknownCountryException, UnknownCurrencyException, UnknownLanguageException, UnknownLocaleException) implement CommerceGuys\Intl\Exception\ExceptionInterface and extend \InvalidArgumentException. Catching \Exception around formatting will swallow real bugs.
  • Percent style expects a fraction. With NumberFormatter::PERCENT, pass 0.75 for 75%, not 75. Verify against your version before trusting it in invoices.
  • Extensions: custom data path. Every repository accepts an alternate data directory. Copy 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.
  • Version bumps change data, not API. CLDR releases rename currencies and add locales; snapshot tests asserting exact symbol strings will break on a minor upgrade. Pin the version in composer.json if formatted output is contractual, and review the diff when upgrading.
  • Formatting is presentation-only. Don't store formatted strings; a locale switch in the UI would leave stale data. Keep the numeric amount and ISO code canonical, and format at render time so historical orders display correctly for each viewer's locale.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib