Skip to content
← Back to projects

Laravel Locale Cookie

A Laravel middleware that resolves the application locale from a cookie, validating the requested value against a configurable list of supported locales and falling back to a sensible default when the cookie is missing or unknown. Cookie name, supported locales, and fallback locale are all driven by config.

Laravel Locale Cookie

#Laravel Locale Cookie

Buy Me A Coffee

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

A Laravel middleware that resolves the application locale from a cookie. The requested value is validated against a configurable list of supported locales and ignored when it is missing or unknown, falling back to a sensible default. Cookie name, supported locales, and fallback locale are all driven by config.

#Installation

You can install the package via composer:

composer require jeffersongoncalves/laravel-locale-cookie

You can publish the config file with:

php artisan vendor:publish --tag="locale-cookie-config"

This is the contents of the published config file:

return [
    'cookie' => 'locale',
    'supported' => ['en'],
    'fallback' => null,
];
  • cookie — the name of the cookie the middleware reads the visitor's locale from (default locale).
  • supported — the whitelist of accepted locales. A cookie value that is not in this list is ignored.
  • fallback — the locale applied when the cookie is missing or unsupported. When null, the middleware falls back to the framework's config('app.fallback_locale').

#Usage

Register the middleware on the route group that should be locale-aware. The middleware reads the cookie, validates it against your supported list, and applies the matching locale (or the fallback) for the rest of the request:

use JeffersonGoncalves\LocaleCookie\Middleware\SetLocale;

Route::middleware(SetLocale::class)->group(function () {
    // ...locale-aware routes
});

For convenience, the package also registers a locale middleware alias, so you can reference it by name:

Route::middleware('locale')->group(function () {
    // ...locale-aware routes
});

You can also alias it in bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \JeffersonGoncalves\LocaleCookie\Middleware\SetLocale::class,
    ]);
})

#Setting the cookie

This package only reads the locale cookie — it never writes it. Your application is responsible for setting the cookie when the visitor chooses a locale. You can do that from PHP:

return redirect()->back()->withCookie(cookie()->forever('locale', 'pt_BR'));

…or from the client with JavaScript:

document.cookie = 'locale=pt_BR; path=/; max-age=31536000';

#Encrypted cookies

By default Laravel's EncryptCookies middleware encrypts every cookie, and it runs before this middleware. To keep the read side (the SetLocale middleware, which reads the raw value via $request->cookies->get()) and the write side (the bundled switch route) consistent — and so that client-set cookies (e.g. the JavaScript example above, which are never encrypted) keep working — the package automatically registers the configured cookie name in EncryptCookies' exception list (via EncryptCookies::except()). The cookie is therefore always written and read as a plaintext value; you do not need to add it to $except yourself.

If you change locale-cookie.cookie, the exclusion follows the new name automatically.

#Using with Livewire

If your locale-aware pages use Livewire, register the middleware as a Livewire persistent middleware as well, so subsequent /livewire/update requests keep the visitor's locale instead of falling back to the default. Register both the route-group middleware and the persistent middleware (pattern taken from the source project this package was extracted from):

use Livewire\Livewire;
use JeffersonGoncalves\LocaleCookie\Middleware\SetLocale;

// In a service provider's boot() method
Livewire::addPersistentMiddleware([
    SetLocale::class,
]);

#Locale switch route

The package registers a ready-made route that persists the chosen locale in the cookie and redirects back to a same-host Referer (open-redirect safe). Point your language switcher at it:

@foreach (config('locale-cookie.supported') as $code)
    <a href="{{ route('locale.switch', ['locale' => $code]) }}">{{ strtoupper($code) }}</a>
@endforeach

The {locale} param is constrained to config('locale-cookie.supported'). Configure (or disable) it under locale-cookie.switch:

'switch' => [
    'enabled' => true,
    'path' => 'locale/{locale}',
    'name' => 'locale.switch',
    'lifetime' => 60 * 24 * 365, // cookie lifetime in minutes
    'middleware' => ['web'],     // attaches the queued cookie; the locale cookie is excluded from encryption
],

#Shortening a locale code

The LocaleCookie::short() helper reduces a locale to its short language code — handy for <html lang="…">, flag icons, or any place you only need the base language:

use JeffersonGoncalves\LocaleCookie\LocaleCookie;

LocaleCookie::short('pt_BR'); // 'pt'
LocaleCookie::short('pt-BR'); // 'pt'
LocaleCookie::short('EN');    // 'en'
LocaleCookie::short();        // current app locale, shortened
LocaleCookie::short('');      // 'en' (fallback when there is no usable prefix)
<html lang="{{ \JeffersonGoncalves\LocaleCookie\LocaleCookie::short() }}">

#URL-prefix mode (SEO / crawlable per-language URLs)

Cookie-only locale resolution is a dead end for SEO: crawlers never send a locale cookie, so they only ever see your fallback locale — there is no distinct, crawlable URL for a French version of a page to exist at, so there's nothing to point hreflang at.

Opt-in URL-prefix mode fixes that by giving every non-default locale a real path prefix (/en/..., /fr/...), while the default locale stays unprefixed at root (no mass redirect needed for your existing indexed URLs). Enable it in the config:

'url_prefix' => [
    'enabled' => env('LOCALE_COOKIE_URL_PREFIX', false),
    'default_locale' => null, // falls back to `fallback` above when null
    'segments' => [
        'fr' => 'fr',
        'de' => 'de',
        'es' => 'es',
    ],
],

Wrap your route definitions in LocaleCookie::routes() instead of registering them directly. It registers each route once unprefixed for the default locale, and once more per configured segment — a Laravel route can't collapse a leading optional segment followed by more literal segments into both forms on its own, so this registers the group twice under the hood:

use JeffersonGoncalves\LocaleCookie\LocaleCookie;

LocaleCookie::routes(function () {
    Route::middleware(['web', 'locale'])->get('projects', ProjectsController::class)->name('projects.show');
});

This yields projects.show (/projects, default locale) plus fr.projects.show (/fr/projects) and es.projects.show (/es/projects). With url_prefix.enabled, the locale/SetLocale middleware resolves the locale from the matched route's prefix instead of the cookie — the same URL always renders the same locale for anyone, bots included — and still keeps the cookie in sync for code that reads it directly.

Use LocaleCookie::localizedRoute() for a language switcher, so "this same page, in French" links to /fr/projects rather than swapping the cookie and reloading:

LocaleCookie::localizedRoute('projects.show', [], 'fr'); // '.../fr/projects'

And LocaleCookie::alternates() to emit hreflang tags without reimplementing the locale-to-URL mapping yourself:

@foreach (\JeffersonGoncalves\LocaleCookie\LocaleCookie::alternates() as $locale => $url)
    <link rel="alternate" hreflang="{{ $locale }}" href="{{ $url }}">
@endforeach

#Bringing your own dynamic {locale} routing

LocaleCookie::routes() isn't the only way to shape locale-prefixed routes — if your app registers every locale (default included) under a single dynamic group instead:

Route::prefix('{locale}')->where('locale', 'en|fr|es')->group(function () {
    Route::middleware(['web', 'locale'])->get('projects', ProjectsController::class)->name('projects.show');
});

SetLocale still resolves it: with url_prefix.enabled, it checks the matched route's locale action first (for LocaleCookie::routes()'s groups), then falls back to a {locale} route parameter. Either source also fills route()/link generation with the resolved locale via URL::defaults(), so calls like route('projects.show') don't need locale passed explicitly.

#Testing

composer test

#Changelog

Please see CHANGELOG for more information on what has changed recently.

#Contributing

Please see CONTRIBUTING for details.

#Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

#Credits

#License

The MIT License (MIT). Please see License File for more information.

New version available.