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.
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.
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).
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.
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.
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.
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.
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'));
}
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.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.*.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.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.php artisan octane:reload before concluding the package is broken.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.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.Formatter interface so you can swap to raw NumberFormatter/IntlDateFormatter in an afternoon if you need to.How can I help you explore Laravel packages today?