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

tempest/intl

Internationalization toolkit for Laravel: locale-aware formatting for dates, numbers, currencies, and translations. Ships multilingual apps with consistent output across locales, wrapping PHP's intl extension in a tidy, framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: tempest/intl is a read-only sub-split of the Tempest framework's Intl component. As a Laravel developer you are not the target audience — it does not auto-discover, it does not touch the Lang facade, it does not register a service provider, and its config is read from Tempest's config layer, not config/app.php. Any assessment that starts with composer require tempest/intl and expects it to "just work" is wrong about day-to-day reality.

Minimal steps (honest version)

  1. Confirm the floor before anything else:
    php -m | grep -i intl          # ext-intl is a hard requirement
    php -r 'echo INTL_ICU_VERSION;' # record the ICU version; it drives formatting output
    
  2. Read upstream, not the split. The split repo is a publication artifact — its src/ is generated on release and its issue tracker is empty by design. Open the Tempest monorepo (tempestphp/tempest-framework) and read the Intl component directory there. That is where the Locale value object, the translator, and the formatter wrapper actually live, and where you will find the docs, changelog, and open issues.
  3. Check the transitive set — this is the step that usually ends the evaluation:
    composer why tempest/support        # container/support primitives are the usual surprise
    composer show tempest/intl --tree
    
    If it pulls Tempest container or support primitives, you are committing to maintaining a hand-written Laravel service provider forever to keep those seams alive.
  4. Do the first use case in a throwaway branch, behind your own interface, not in app/. The realistic first case is not "translate the UI" — that is Laravel's Lang job. It is locale-aware formatting of money, dates, and numbers in a bounded module (invoicing, reporting, exports), where you want a typed value object instead of sprinkling NumberFormatter calls.

Where to look first (in priority order)

Priority What Why
1 Laravel's own Illuminate\Support\Number, NumberFormatter, Carbon, Pluralizer This is your baseline. Measure against it before adopting anything.
2 symfony/intl Decade-old, Symfony-backed, and already a transitive dep of most Laravel installs via symfony/*.
3 Upstream Tempest Intl source Only if (1) and (2) are genuinely short for your case.

First use case template. Before writing a line, write the interface you want to consume. If this package earns its place, it is because this interface is cleaner than calling intl directly:

interface MoneyFormatter
{
    public function format(int $minorUnits, string $currency, string $locale): string;
}

interface LocaleResolver
{
    public function forRequest(Request $request): string;
}

Bind your implementation in AppServiceProvider. The vendor type must never appear in a controller or domain signature. That is the whole adoption strategy in two methods.


Implementation Patterns

The adapter pattern is not optional

Treat tempest/intl as a leaf library, not a platform dependency. You will be wrapping it. The pattern:

// app/Support/Intl/TempestMoneyFormatter.php
namespace App\Support\Intl;

use App\Contracts\MoneyFormatter;
use Tempest\Intl\...; // exact namespace per the version you pin

final class TempestMoneyFormatter implements MoneyFormatter
{
    public function format(int $minorUnits, string $currency, string $locale): string
    {
        // delegate, then normalize/round-trip here so the rest of the app
        // is insulated from any upstream signature change
    }
}

Register a container binding, not a facade alias, and add an architecture test that fails the build if Tempest\Intl\* is imported outside App\Support\Intl:

// tests/Architecture/IntlBoundaryTest.php
arch('intl vendor types stay contained')
    ->expect('Tempest\Intl')
    ->toOnlyBeUsedIn('App\Support\Intl');

That test is the difference between a spike and a liability. Without it, the import spreads into controllers within a sprint and your exit cost becomes a refactor.

Locale negotiation belongs in middleware

The package offers request-aware locale detection, but in Laravel the negotiation hook is your middleware slotting into Laravel's own locale pipeline — do not fork into two locale sources of truth:

// app/Http/Middleware/NegotiateLocale.php
public function handle(Request $request, Closure $next)
{
    $locale = $this->resolver->forRequest($request); // your adapter
    app()->setLocale($locale);                     // keep Laravel's Lang in sync
    $request->attributes->set('intl.locale', $locale);
    return $next($request);
}

The single most dangerous pattern here is letting Laravel's app()->getLocale() and the Intl component's locale drift apart. Whatever you do, keep one function that sets both, and call it from one place.

Inject the locale into Blade, don't re-resolve it

Format in the controller or a view composer, pass a formatted string to the view. Do not call the Intl API from Blade — you will lose the adapter boundary and you cannot test the locale matrix through views.

// controller
return view('invoices.show', [
    'total'  => $this->money->format($invoice->totalMinor, $invoice->currency, app()->getLocale()),
    'issued' => $this->dates->formatDate($invoice->issuedAt, app()->getLocale()),
]);

Testing pattern: a locale matrix, not per-test locale

Write one data provider that runs a fixed set of locales (en_GB, en_US, de_DE, fr_FR, ja_JP) and asserts against hardcoded expected strings, captured from the current ICU version. This is what catches the insidious divergence case — where the Tempest formatter and Laravel's Number produce different grouping or currency placement for the same locale:

public static function locales(): array
{
    return [
        ['en_GB', '1234567', '£1,234,567.00'],
        ['de_DE',  '1234567', '1.234.567,00 £'],
        ['fr_FR',  '1234567', '1 234 567,00 £'],
    ];
}

Run the same provider against your existing Number::currency() output to get a comparison report. That report is the actual output of the spike.

If Tempest is in the stack

Then this is the natural default and the adapter pattern is optional within Tempest code (the container wiring is already done). But even then, keep the boundary at the module edge, because the package is pre-1.0 and versioned in lockstep with the framework.


Gotchas and Tips

The split lags upstream. A bug you file against the split will not be seen; a fix upstream will not reach you until the next release regenerates the split. When debugging, read the monorepo source at the tag that produced your installed version, not the installed vendor/ copy — they can differ by commits.

ext-intl missing is silent, not loud. In a container without it, the code does not necessarily throw — locale resolution can fall back to en and you ship wrong currency formatting to German customers with green tests. Pin the extension in your Dockerfile and assert it in a health check. Also pin INTL_ICU_VERSION awareness: ICU upgrades change formatting output (clustering, currency symbols, plural rules) with no deprecation notice, and your locale-matrix tests are the only warning you get.

There is no illuminate/* integration, and that cuts both ways. It installs cleanly because it does not depend on Laravel; it integrates with nothing because it does not depend on Laravel. No facade, no auto-discovery, no config publishing, no Lang:: interop, no @choice plural support. If your translation workflow runs through lang/*.php files or a TMS that generates them, this package will not participate in it.

Pluralization is a trap. The package's pluralization and Laravel's Pluralizer / trans_choice are two separate rule engines. Using both in one app means the same count can pluralize differently in different screens. Pick one, for all counts.

Read-only means no local patch. composer patches and forks are the only emergency fix path, and a private fork is a permanent maintenance tax. Get governance sign-off on this specific point before adoption, not after the first incident.

No semver guarantees below 1.0. A minor bump can rename a method. Mitigations, in order of preference: pin the exact version in composer.lock, pin in composer.json with a ~ constraint that does not cross a minor, and read the upstream monorepo changelog on every Renovate PR — because the split repo's changelog will be empty or unhelpful.

Config lives in Tempest's layer. Published config does not exist for Laravel, and the component does not read config/app.php. If you need locale defaults or fallback chains, they go in your own config and get passed into the adapter — do not try to make the package read Laravel config.

Extension points are upstream, not here. Anything you want to customize (new formatters, locale resolution strategy) is either available in the upstream component's public surface or is not available at all. There is no plugin registry to hook into from the split. Verify a specific extension point exists in source before designing around it.

Abandonment exit plan, one paragraph in the repo. Document: why the dependency exists, which adapter implements it, what replaces it (symfony/intl or native intl), and who makes the fork decision. This is the cheapest possible insurance and it is the step everyone skips.

Default recommendation for a Laravel team: don't adopt. Run the one-sprint spike in parallel against Illuminate\Support\Number / Carbon / native intl and symfony/intl; adopt only if the adapter measurably reduces code and the divergence matrix is clean. If you are not on Tempest, the integration and governance overhead almost always exceeds the ergonomic gain.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource