Tech Verse Logo
Enable dark mode
Laravel 12 Localization: Prefixes, Files, and Rules

Laravel 12 Localization: Prefixes, Files, and Rules

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

5 min read

Localization in Laravel 12

Handling multiple languages across an application isn't just about wrapping string literals inside a __('Save') helper. It's about designing URL structures that search engines can crawl, maintaining translation keys across big codebases, and handling weird edge cases in human language pluralization. If you've ever shipped a site where changing the locale breaks URL generation across half your routes, you know how fragile this setup can get.

Laravel 12 paired with PHP 8.3 gives us clean hooks to handle locale state. Here is how to configure route prefixes, decide between PHP and JSON translation stores, and manage non-standard pluralization rules without creating a mess in your codebase.

Explicit Locale Prefixing via Routing

The cleanest architectural choice for localized web applications is prefixing the URL path with the locale code—for example, /en/dashboard or /fr/dashboard. Avoid setting locale state purely from cookie values or session variables without changing the URL path. If the URL doesn't reflect the language, search engines struggle to index your content, and users can't share links to specific language versions.

In Laravel 12, route definitions and middleware registration are organized inside bootstrap/app.php and route files. We start by creating a custom middleware that inspects the first segment of the incoming request path, validates it against supported locales, and assigns the application locale dynamically.

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpFoundation\Response;

class SetLocale
{
    protected array $supportedLocales = ['en', 'fr', 'es', 'de'];

    public function handle(Request $request, Closure $next): Response
    {
        $locale = $request->segment(1);

        if (! in_array($locale, $this->supportedLocales, true)) {
            $locale = config('app.fallback_locale', 'en');
        }

        App::setLocale($locale);
        URL::defaults(['locale' => $locale]);

        return $next($request);
    }
}

Pay attention to URL::defaults(['locale' => $locale]) in that middleware. That single call solves one of the most annoying bugs in Laravel routing. Without setting a URL default parameter, every time you call route('dashboard', ['locale' => $locale]) in your blade templates or controllers, you have to manually pass the locale key. By setting the default parameter inside middleware early in the lifecycle, standard call patterns like route('dashboard') automatically inject the active route prefix.

Defining the Route Groups

Next, structure your routes/web.php file using a localized route prefix group. Keep unlocalized routes like webhooks or external API callbacks outside this group.

use App\Http\Middleware\SetLocale;
use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route;

Route::prefix('{locale}')
    ->whereIn('locale', ['en', 'fr', 'es', 'de'])
    ->middleware([SetLocale::class])
    ->group(function () { 
        Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
        Route::get('/settings', [DashboardController::class, 'settings'])->name('settings');
    });

Route::get('/', function () {
    return redirect('/' . config('app.fallback_locale', 'en') . '/dashboard');
});

Using whereIn('locale', [...]) at the route level prevents unmatched path prefixes from entering your middleware logic, returning clean 404 responses immediately for invalid locales instead of running full middleware execution cycles.

Choosing Between PHP Array and JSON Files

Laravel supports two formats for language files inside the lang/ directory: nested PHP array files (e.g., lang/en/auth.php) and flat JSON files (e.g., lang/fr.json). Pick the right tool for the specific job rather than sticking dogmatically to one.

PHP Array Translation Files

PHP files excel when dealing with structured domain categories or when translations contain nested metadata. They return standard PHP arrays and support comments, variable scoping, and logical grouping.

lang/en/checkout.php
return [
    'title' => 'Complete your order',
    'buttons' => [
        'pay' => 'Pay :amount now',
        'cancel' => 'Cancel order',
    ],
];

You access these using short-key dot notation: __('checkout.buttons.pay', ['amount' => '$50']). This approach reduces memory overhead in huge translation files because Laravel loads lang files on demand when keys inside that file namespace are referenced.

JSON Translation Files

JSON translation files use the full default language text as the translation key rather than dot-notation short keys.

lang/fr.json
{
    "Complete your order": "Finalisez votre commande",
    "Save changes": "Enregistrer les modifications"
}

Access these with direct string translation: __('Save changes'). This format works well if you pass strings directly to frontend components in React 19 or Next.js 16 application layers, where writing abstract translation keys like ui.buttons.save_changes makes JSX messy.

Here is the big gotcha with JSON translation files: keys are exact string matches, case-sensitive, and sensitive to whitespaces. If a developer alters the English template text from "Save changes" to "Save Changes", the translation lookup fails completely in non-English environments and silently drops back to returning the key itself. For core domain models, prefer PHP files with strict short keys.

Advanced Pluralization and Custom Pluralizers

Standard language strings handle basic counts easily using the pipe character inside standard translation files. Laravel's trans_choice helper evaluates integer parameters against pipe-delimited ranges.

// lang/en/messages.php
return [
    'items_count' => '{0} No items selected|[1,19] :count items selected|[20,*] A large batch of :count items selected',
];

You trigger this in your application code using:

echo trans_choice('messages.items_count', $quantity, ['count' => $quantity]);

Handling Complex Slavic or Asian Language Pluralization

The standard interval matching rule breaks down when you localise into languages with complex grammar systems like Russian, Polish, or Arabic. Russian, for instance, has three distinct plural forms depending on the last digit of the number (e.g., 1 item = item, 2 items = items, 5 items = items-plural, 21 items = item).

Laravel's translation layer builds on top of Symfony's Translator component. If standard range intervals don't solve your grammar requirements, use the standard ICU MessageFormat standard by interfacing directly with Symfony's message formatter or registering dynamic custom pluralizers through PHP 8.3 native logic.

use Symfony\Component\Translation\Formatter\IntlFormatter;
use Illuminate\Support\Facades\App;

// Configuring standard ICU formatting when passing strings to frontend client tools
$formatter = new IntlFormatter();
$pattern = '{count, plural, =0 {No apples} one {# apple} few {# apples} many {# apples} other {# apples}}';

$translatedText = $formatter->format(
    $pattern, 
    'ru',
    ['count' => 21]
);

Integrating Localized APIs with React 19 and Next.js 16

If you run Laravel 12 as a headless API backend supplying translations to a Next.js 16 frontend app using React 19 Server Components, don't re-implement translation parsing logic on the node server. Return pre-translated payload structures directly from JSON API routes or expose locale files through dedicated endpoint dictionaries.

When returning payloads from Laravel controllers to React frontend apps, assign the locale per request using an Accept-Language header or route parameter middleware, and wrap API responses using API Resources:

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => __("products.{$this->slug}.name"),
            'price_formatted' => $this->formatted_price,
            'stock_message' => trans_choice('products.stock_status', $this->stock_count, ['count' => $this->stock_count]),
        ];
    }
}

This keeps heavy localization computations inside PHP 8.3, allowing React Server Components in Next.js 16 to consume light static text props without client-side bundle bloat caused by massive i18n frontend dependencies.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Laravel Signed URLs and One-Time Download Links

    Laravel Signed URLs and One-Time Download Links

    Laravel Timezone Handling: UTC, Users, and DST Bugs

    Laravel Timezone Handling: UTC, Users, and DST Bugs

    Laravel Login Throttle: Rate Limiting and Credential Defense

    Laravel Login Throttle: Rate Limiting and Credential Defense

    Building Honest Health Check Endpoints in Laravel

    Building Honest Health Check Endpoints in Laravel

    Writing Production-Ready Laravel Artisan Commands

    Writing Production-Ready Laravel Artisan Commands

    Testing Mail in Laravel: Mailables and Assertions

    Testing Mail in Laravel: Mailables and Assertions

    Solving Low-Priority Queue Starvation in Laravel

    Solving Low-Priority Queue Starvation in Laravel

    Realistic Laravel Seeders with States and Relations

    Realistic Laravel Seeders with States and Relations

    Fixing Laravel Broadcasting Auth and 403 Errors

    Fixing Laravel Broadcasting Auth and 403 Errors

    Laravel Multi Tenancy: Single vs Multi Database

    Laravel Multi Tenancy: Single vs Multi Database