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

ozankurt/laravel-intl

A Laravel package for internationalization built on PHP's intl extension, providing locale-aware helpers for formatting dates, numbers, and currencies, along with middleware and Blade directives to manage per-request locales in your application.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package.

    composer require ozankurt/laravel-intl
    

    Laravel's package auto-discovery should register the service provider and the Intl facade alias. Confirm both appear in php artisan about / bootstrap/cache/packages.php before writing code.

  2. Confirm ext-intl is enabled — this is the #1 first-run failure.

    php -m | grep intl
    

    If it's missing, install it (apt install php-intl, brew install php, or uncomment extension=intl in php.ini). The package is a thin wrapper around ICU; nothing works without the extension, and failures are often silent (fallback rather than exception).

  3. Publish the config (only if you need to change defaults).

    php artisan vendor:publish --tag=intl-config
    

    You get a place to set the default locale, default currency, and fallbacks. If you are happy with config('app.locale'), skip this step entirely.

  4. First use case — currency formatting.

    use OzanKurt\Intl\Facades\Intl;
    
    Intl::currency(1234.56, 'USD');              // $1,234.56
    Intl::currency(1234.56, 'EUR', 'de-DE');    // 1.234,56 €
    

    In Blade:

    {{ Intl::currency($order->total, $order->currency) }}
    

    Once that renders correctly you have verified ext-intl, the provider, and the facade are all healthy.

  5. Where to look first. The canonical API surface is the Intl class and facade docblocks in vendor/ozankurt/laravel-intl/src. The README is thin and the 2.1.0 "changelog" is only a compare link, so read the source before assuming a method signature, argument order, or option key. Note whether locale is read from app()->getLocale(), config('intl.locale'), or passed explicitly — set both if unsure.

  6. Decide early whether this is the right tool. This package formats — it does not carry country/currency/locale reference datasets, validation rules, or translations. If your roadmap is "country pickers and phone validation," that is a different package. If it is "render prices and dates correctly per user locale," you are in the right place.

Implementation Patterns

Format at the edge, never in the domain. Keep minor-unit integers and DateTimeInterface objects in your models; run them through Intl only in resources, controllers, and views. This keeps locale changes out of business logic and makes snapshots stable.

// app/Http/Resources/OrderResource.php
return [
    'total'     => Intl::currency($this->total_cents / 100, $this->currency),
    'placed_at' => Intl::date($this->created_at, 'long'),
];

Negotiate locale once, in middleware. Don't thread $locale through every call — resolve it per request and bind it.

// app/Http/Middleware/SetIntlLocale.php
public function handle($request, Closure $next)
{
    $locale = $request->user()?->locale
        ?? $request->getPreferredLanguage(['en-US', 'fr-FR', 'de-DE'])
        ?? config('app.locale');

    app()->setLocale($locale);
    config(['intl.locale' => $locale]);

    return $next($request);
}

Set both app()->setLocale() and the package config; the wrapper may read either, and this avoids a silent fallback to en-US.

Expose one Formatter service to the app. Call the service, not the facade, everywhere. One place to swap implementations, one place to fake in tests.

final class Formatter
{
    public function money(int $minor, string $currency, ?string $locale = null): string
    {
        return Intl::currency($minor / 100, $currency, $locale);
    }

    public function when(\DateTimeInterface $dt): string
    {
        return Intl::date($dt, 'medium');
    }
}

Bind it in a service provider and inject it. Tests then fake Formatter rather than fighting ICU output strings.

Use per-call overrides for edge rendering. Show the product's price in the shop's currency but the user's locale — that is a per-call locale, not the request default.

Intl::currency($price, 'JPY', $request->user()->locale);

Common dashboard formats.

Intl::number(0.1234, ['style' => 'percent', 'maximum_fraction_digits' => 1]); // 12.3%
Intl::number(1500000, ['notation' => 'compact']);                            // 1.5M
Intl::percentage(37.5, 1, 'de-DE');                                          // 37,5 %

Dates: store UTC, format in the user's zone. Pass a DateTimeInterface; convert timezone at the boundary.

Intl::date($order->created_at->setTimezone($user->timezone), 'long', $user->locale);

Testing. Pin the locale and travel time so assertions are deterministic, and assert on structure, not exact strings (ICU output varies by version).

protected function setUp(): void
{
    parent::setUp();
    app()->setLocale('en-US');
    config(['intl.locale' => 'en-US']);
    $this->travelTo(Carbon::parse('2024-01-15 12:00:00'));
}

Gotchas and Tips

  • ICU version drift is the #1 "works on my machine." Output for the same input differs between macOS ICU, Linux CI, and production — Windows PHP builds lag badly. Pin the same PHP base image across environments, and assert on structure (symbol present, digits grouped) rather than full strings.
  • Locale tags are BCP-47, not POSIX. en_US is invalid in ICU and silently falls back to the default. Store and pass en-US, pt-BR, zh-Hant-TW.
  • currency() formats, it doesn't convert. Intl::currency(100, 'JPY') renders 100 yen; it will not turn dollars into yen. Do FX conversion in your domain layer and format the result.
  • Silent fallback hides bad input. An unknown locale or currency code usually falls back to the default instead of throwing — you ship wrong-looking prices instead of an error. Validate user-supplied locale/currency against a whitelist before it reaches the formatter.
  • php artisan config:cache freezes your defaults. Runtime config(['intl.locale' => ...]) in middleware still works, but published-file values won't pick up env changes until you re-run config:cache. Re-run it in every deploy that changes intl.*.
  • Facade alias collisions. Intl is a popular alias — other packages may register it too. Prefer use OzanKurt\Intl\Facades\Intl; (class import) over relying on the global alias.
  • Formatter construction is expensive. NumberFormatter, IntlDateFormatter, and phone parsers are costly. If you format hundreds of rows in a report, memoize per (locale, style) or reuse an underlying intl object rather than calling the facade in a tight loop.
  • Octane quirk. Long-running workers cache container and facade state — after upgrading, php artisan octane:reload before concluding the package is broken.
  • CI images often lack ext-intl. Add php-intl to your Dockerfile and GitHub Actions extensions, and add a fast startup check so you fail on install rather than in the first formatting test.
  • Read the diff, not just the changelog. 2.1.0 ships only a compare link (2.0.0...2.1.0) with no prose. Before bumping:
    composer update ozankurt/laravel-intl -W --dry-run
    git log 2.0.0..2.1.0 --stat   # inside the package's vendor dir, if it's a git checkout
    
    Minor bumps can still change default arguments or add required config keys.
  • Adoption risk is real. 3 GitHub stars, no dependents, and one author means low bus-factor and thin docs. Budget for reading source, and if you go to production, consider wrapping the facade behind your own Formatter interface so you can swap to raw NumberFormatter/IntlDateFormatter in an afternoon if you need to.
  • Extension points — don't fork. If the facade lacks a style you need (units, scientific notation, custom skeleton patterns), bind your own formatter in a service provider and delegate to the underlying intl classes. The wrapper is thin, so composing is cheap and keeps you on the upgrade path.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource