Number::withLocale

Runs a callback with Number's locale temporarily set, restoring the previous locale afterwards.

Signature
static Number::withLocale(string $locale, callable $callback): TReturn
  • Added in Laravel 10.33.0

Parameters

ParameterTypeDescription
$$localestringThe locale to install for the duration of the callback, such as "de", "fr_CA" or "en_IN".
$$callbackcallableThe callback to invoke while that locale is active. It is called with no arguments and its return value becomes the return value of the call.

Return value

TReturnWhatever the callback returns, passed straight through unchanged.

Errors

Nothing of its own. Exceptions raised inside the callback propagate to the caller, and from Laravel 12.20.0 the previous locale is restored on the way out.

Changelog

  • PHP 10.33.0Introduced with the Illuminate\Support\Number class.
  • PHP 12.20.0Restoration moved into a finally block, so the previous locale is put back even when the callback throws. Earlier versions leaked the temporary locale into the rest of the process on an exception.

Every Number method that formats or parses accepts a $locale argument, but threading that argument through a block of code gets tedious fast. Number::withLocale() sets the class's default locale once, runs your callback, and puts the old value back in a finally block, returning whatever the callback returned.

When to reach for it

Reach for it when several formatting calls in a row share a locale that is not the application's: rendering one PDF invoice in the recipient's language inside a job that processes many recipients, building a localized notification, or writing a test that asserts formatting under a fixed locale regardless of the environment.

What trips people up

Its reach is narrower than the name suggests. It writes one protected static property on Illuminate\Support\Number — nothing else. App::setLocale(), translation strings, Carbon's ->translatedFormat() and any NumberFormatter you build yourself are all unaffected. If you want dates and copy localized too, change those separately.

The callback is invoked with no arguments, so a closure declaring parameters simply receives nothing. And because the underlying state is a static property, the swap applies process-wide for the duration of the call — fine for a normal request, worth remembering inside long-lived Octane or queue workers where the callback should stay short and must not yield.

Example

Example · php
<?php

use Illuminate\Support\Number;

// The default locale is "en".
echo Number::format(1234.56), PHP_EOL;

$german = Number::withLocale('de', function () {
    return Number::format(1234.56);
});

echo $german, PHP_EOL;

// The previous locale is back, even after an exception.
try {
    Number::withLocale('fr', fn () => throw new RuntimeException('boom'));
} catch (RuntimeException $e) {
    echo 'caught: ', $e->getMessage(), PHP_EOL;
}

echo Number::format(1234.56), PHP_EOL;

// Output:
// 1,234.56
// 1.234,56
// caught: boom
// 1,234.56

When to use it

  • Rendering one customer's invoice totals in their locale inside a job that loops over many customers.
  • Asserting formatted output in a test without depending on the machine's or app's configured locale.
  • Building a comparison table that shows the same figure formatted for several markets.

Frequently asked questions

What is the difference between Number::withLocale and Number::useLocale?

useLocale() changes the default locale permanently for the rest of the process, while withLocale() changes it only for the duration of a callback and then restores the previous value. Use useLocale() once at boot in a service provider, and withLocale() for a scoped exception to that default.

Does Number::withLocale change the application locale?

No. It writes only the protected static $locale property on Illuminate\Support\Number, so translation strings, App::currentLocale(), Carbon's translated dates and any NumberFormatter you construct yourself are all unaffected. Call App::setLocale() as well if you need the surrounding text localized too.

Is the locale restored if the callback throws an exception?

Yes, on Laravel 12.20.0 and later, where the restore happens in a finally block. On earlier versions an exception escaped before the previous locale was put back, leaving the temporary locale installed for every later request handled by that process, which is a real hazard under Octane and queue workers.

More examples

Formatting one figure for several markets

Three formatting calls share a locale per iteration without repeating the argument, and the closure's array return value comes straight back out. The final line shows the default "en" locale intact after five swaps. Note that abbreviate() keeps its English suffix in every locale — withLocale changes separators and symbols, not Laravel's own abbreviation letters.

Example · php
<?php

use Illuminate\Support\Number;

$revenue = 12500000;

foreach (['en', 'de', 'fr', 'en_IN', 'ja'] as $locale) {
    $row = Number::withLocale($locale, fn () => [
        Number::format($revenue),
        Number::abbreviate($revenue),
        Number::percentage(12.5, precision: 1),
    ]);

    printf("%-6s %-14s %-6s %s%s", $locale, $row[0], $row[1], $row[2], PHP_EOL);
}

echo Number::format($revenue), PHP_EOL;

// Output:
// en     12,500,000     12M    12.5%
// de     12.500.000     12M    12,5 %
// fr     12 500 000     12M    12,5 %
// en_IN  1,25,00,000    12M    12.5%
// ja     12,500,000     12M    12.5%
// 12,500,000

Rendering a notification in the recipient's locale

Two things are being localized and they need separate calls: withLocale handles the number separators and currency placement, while ->locale() on the MailMessage handles the translated sentences around them. Dropping either one produces a half-translated email — German prose with US-style decimal points, or the reverse.

Example · php
<?php

namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Number;

class InvoiceIssued extends Notification
{
    public function __construct(private readonly Invoice $invoice)
    {
    }

    public function toMail(object $notifiable): MailMessage
    {
        [$total, $tax] = Number::withLocale($notifiable->locale, fn () => [
            Number::currency($this->invoice->total, $this->invoice->currency),
            Number::currency($this->invoice->tax, $this->invoice->currency),
        ]);

        return (new MailMessage)
            ->locale($notifiable->locale)
            ->line(__('Total due: :total', ['total' => $total]))
            ->line(__('Including tax: :tax', ['tax' => $tax]));
    }
}

Pinning the locale in a test

Wrapping the assertion means the test does not depend on Number::useLocale() having been called in a service provider, and it cannot leak a changed locale into the tests that run after it. The escape in the expectation is deliberate: ICU separates the amount from the euro sign with a non-breaking space, which is the most common reason this kind of assertion fails on a first run.

Example · php
<?php

namespace Tests\Unit;

use Illuminate\Support\Number;
use PHPUnit\Framework\TestCase;

class PriceFormattingTest extends TestCase
{
    public function test_it_formats_prices_for_german_customers(): void
    {
        $formatted = Number::withLocale('de', fn () => Number::currency(1999.5, 'EUR'));

        $this->assertSame("1.999,50\u{a0}€", $formatted);
    }
}

Discussion

  • Be the first to comment on this lesson.