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

Laravel Intl Laravel Package

propaganistas/laravel-intl

Laravel Intl adds convenient internationalization helpers to Laravel: format numbers, currencies, percentages, dates and phone numbers using the intl extension, with Blade directives, validation rules and locale-aware formatting throughout your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify the substrate first. Every helper in this package is a wrapper around PHP's intl extension (NumberFormatter, IntlDateFormatter, IntlDateFormatter-adjacent list/unit formatters). So the two-step is:

composer require propaganistas/laravel-intl
php -m | grep -i intl   # must print "intl"

If that second command is empty, install php-intl before anything else — the package will boot fine and then fail at call time with Class "NumberFormatter" not found, which is a confusing error to chase in production.

Publish the config. The service provider is auto-discovered; the config file (config/intl.php) carries the default locale used when you don't pass one explicitly. Publish it and point it at your app locale rather than leaving the shipped default:

php artisan vendor:publish --provider="Propaganistas\LaravelIntl\IntlServiceProvider"

Where to look first. In this order:

  1. README.md — the façade list and method signatures.
  2. src/IntlServiceProvider.php — what gets bound into the container and under which keys (this tells you what you can type-hint and what you can swap).
  3. src/Intl.php (the abstract base) — how locale resolution and setLocale() actually work, which determines whether you need to sync it with App::setLocale() at all.
  4. One concrete class, e.g. src/Number.php — to see the argument order and whether options arrays are passed through to the underlying formatter.

First use case: localized money in a Blade view. This is the 80% case — a price that reads correctly for a Dutch user (€ 1.234,56) and an American one ($1,234.56):

{{ Currency::format($order->total, $order->currency) }}
use Propaganistas\LaravelIntl\Facades\Number;
use Propaganistas\LaravelIntl\Facades\Currency;

Number::format(1234.56);         // "1,234.56"
Number::format(1234.56, 'de');   // "1.234,56"
Currency::format(1234.56, 'EUR'); // "€1,234.56"

Version note: the façade surface was reorganised across majors (an early single Intl façade gave way to per-domain facades: Number, Currency, Date, Duration, List/Lists, Unit, Language, Country, Script). Confirm the exact names and namespaces against the README for the version you install — snippets from old blog posts frequently target the wrong major.

Implementation Patterns

Keep locale in one place and propagate it deliberately. The package has a notion of a current locale that is separate from Laravel's App::setLocale(). Decide early which one is the source of truth, then bridge them in a single middleware rather than sprinkling setLocale() through controllers:

public function handle($request, Closure $next)
{
    $locale = $request->user()?->locale
        ?? $request->getPreferredLanguage(config('app.supported_locales'));

    app()->setLocale($locale);

    // Only needed if the package does not read app()->getLocale() itself —
    // check src/Intl.php first; if it does, drop these lines.
    Number::setLocale($locale);
    Currency::setLocale($locale);
    Date::setLocale($locale);

    return $next($request);
}

Blade components for presentation. Wrap formatting in anonymous or class components so views never contain locale logic and so you can change the formatting rule in one place:

class Money extends Component
{
    public function __construct(
        public float $amount,
        public string $currency = 'EUR',
        public ?string $locale = null,
    ) {}

    public function render(): string
    {
        return Currency::format($this->amount, $this->currency, $this->locale);
    }
}

Format at the edge, never in the model. Keep raw values (1234.56, 7200 seconds, BE) in the database and API payloads; apply the package inside API Resources, view models, or notification builders. Consumers that don't need display formatting (mobile clients doing their own i18n) should get numbers, not "€1.234,56" strings.

public function toArray($request): array
{
    return [
        'total' => $this->total,                       // machine-readable
        'total_display' => Currency::format($this->total, $this->currency),
        'placed_at' => $this->created_at->toIso8601String(),
        'placed_at_display' => Date::format($this->created_at, 'd MMMM yyyy'),
    ];
}

Localized input parsing is the sleeper feature. A Dutch user typing 1.234,56 into a form is not submitting a float. Parse with the submitting user's locale before validation:

$value = Number::parse($request->input('amount'), app()->getLocale());

if ($value === false) {
    throw ValidationException::withMessages([
        'amount' => __('validation.numeric', ['attribute' => 'amount']),
    ]);
}

Note parse() returns false rather than throwing — do not let that silently coerce to 0 in a financial path.

Human-readable durations instead of raw seconds. For "last synced", "trial expires in", job runtimes:

Duration::format($seconds);   // "1 hour, 30 minutes"

This is one of the strongest reasons to pull the package in, since Laravel core has no equivalent.

Natural-language lists for summaries and notifications. "Apples, Oranges, and Pears" versus "Apples, Oranges en Peren" is a real localization detail teams forget:

Lists::format($names);           // Oxford comma in en, different conjunction in nl/de
Lists::format($names, 'nl');

Unit formatting for shipping/logistics UI. Unit::format(1.5, 'kilometer') → "1.5 kilometers" / "1,5 Kilometer". Also handles byte-ish and measurement units where you'd otherwise hand-roll pluralization.

Language/country pickers from the CLDR data, not a hardcoded array. For a locale switcher or country dropdown, generate labels from the package so they're translated for free:

// Locale switcher: names of languages, in the current UI language
collect(['en', 'nl', 'de', 'fr'])
    ->map(fn ($code) => ['code' => $code, 'label' => Language::name($code)]);

// Country dropdown: ISO 3166-1 alpha-2 in, localized name out
Country::name('BE');           // "Belgium"
Country::name('BE', 'nl');     // "België"

Inject the classes, don't only use facades. In services and actions, type-hint the underlying class — it's bound in the container and makes tests far easier:

public function __construct(private \Propaganistas\LaravelIntl\Number $number) {}

public function summarize(Collection $rows): string
{
    return $this->number->format($rows->sum('amount'), null, [
        'minimum_fraction_digits' => 2,
    ]);
}

Testing: swap the façade or fake the binding. For unit tests of business logic, assert on the number, not the string. Where you must assert the formatted output, swap the façade with a deterministic stub so the assertion doesn't depend on the container's ICU version.

Gotchas and Tips

ICU data version is the #1 source of "works on my machine". intl ships CLDR data compiled into the extension; your Debian dev container, your Alpine CI runner, and your Amazon Linux production box can produce byte-different output for the same input — most often the separator inside "€ 1,234.56" flipping between a normal space, U+00A0 (non-breaking), and U+202F (narrow no-break). Never snapshot-assert raw formatted strings across environments. Normalize first:

$normalized = preg_replace('/[\x{00A0}\x{202F}\x{2009}]/u', ' ', $actual);

ext-intl is missing from most slim images. On Alpine you need icu-dev at build time plus docker-php-ext-install intl; on Debian, apt install php-intl. Pin it in the image and add a smoke test that calls Number::format(1) so a broken build fails at CI, not at checkout.

Facade name collision with Laravel 10+. Illuminate\Support\Number now covers format(), currency(), abbreviate(), ordinal(), fileSize() — real overlap with this package's number/currency surface. If you alias this package's façade as Number in config/app.php, you'll shadow or be shadowed depending on load order, and IDE auto-import will pick the wrong one. Either import the package façade by full namespace everywhere, or standardize on the core Number for numbers/currency and reserve this package for what core doesn't do: Duration, Lists, Unit, Language, Country, Script, and locale-aware parsing.

Singleton + mutable locale = request bleed under Octane. The formatter objects are container singletons; setLocale() mutates shared state. Under RoadRunner/Swoole/Octane, a request that sets nl and doesn't reset can leak Dutch formatting into the next request. Reset in a terminating callback, or prefer a withLocale()-style call (if your version exposes one) over mutating the singleton.

Default precision is not what finance wants. Locale defaults drive fraction digits, so Number::format(2.0) may render "2" where your invoice needs "2.00", and Currency::format() may drop decimals for zero-decimal currencies (JPY) — which is correct, but surprising when you expect two places. Pass explicit options rather than relying on the locale default, and write a test per currency you support.

Argument order and code formats bite. Currency::format($amount, $currency) — swapping them produces a confusing exception or a nonsense string. Currency must be ISO 4217 (EUR, not eur or €); countries are ISO 3166-1 alpha-2 (BE, not BEL); languages are BCP-47-ish (en, en_GB, pt_BR) — and Language::name('en_GB') answers a different question than Language::name('en'). Validate inputs at the boundary; these helpers are happy to return garbage or throw for unknown codes.

Parsing is locale-sensitive in a dangerous direction. Number::parse('1,5') under en is 15, not 1.5. If you parse form input with the app default locale instead of the user's locale, you will silently multiply European decimal inputs by ten. Parse with the same locale that rendered the form, and reject on false.

Timezones for Date. IntlDateFormatter needs a timezone; if you pass a Carbon instance that carries one, you're fine, but Date::format(now(), ...) will use whatever the formatter's default is. Set the timezone explicitly at bootstrap (date_default_timezone_set / config('app.timezone')) and prefer passing Carbon objects over strtotime() integers.

Per-row formatting is slow in loops. Building a NumberFormatter per call is non-trivial. For a 5,000-row export, format once per distinct (locale, currency, options) combination — memoize the formatter or pre-group rows — rather than calling Currency::format() inside the inner loop.

Extension points are just the container. Every formatter is bound by IntlServiceProvider. To customize behaviour (e.g., inject a company-wide default currency, cache formatters, or add a missing unit), bind your own subclass over it in a service provider:

$this->app->bind(
    \Propaganistas\LaravelIntl\Number::class,
    \App\Support\Intl\Number::class
);

In tests, Number::swap($fake) gives you the same leverage from the façade side.

Pin the major version and read the changelog on upgrade. This package is a thin, stable wrapper with modest adoption and no runtime dependencies beyond ext-intl — which is a feature (nothing to break) and a risk (nobody's watching it). composer require "propaganistas/laravel-intl:^3.0" with a lockfile is the sane posture; a minor-version surprise in a package this small is unlikely but the façade reorganisation between majors means an unpinned composer update can turn every Intl::formatNumber() call in your codebase red at once.

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels