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

Laravel Localization Laravel Package

mcamara/laravel-localization

Easy i18n for Laravel: detect browser language, redirect with locale saved in session or cookie, define localized and translated routes once, hide the default locale in URLs, and use helpers like a language selector. Note: dynamic routes mean route:cache needs extra setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require mcamara/laravel-localization
php artisan vendor:publish --provider="Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider"

This creates config/laravellocalization.php — the first file to open. The only thing most projects touch is supportedLocales (defaults to en and es), plus hideDefaultLocaleInURL and useAcceptLanguageHeader. Each locale entry carries name, native, regional, script, and dir, which the helper methods read back out for you.

Register the middleware aliases. On Laravel ≤10 this goes in app/Http/Kernel.php $middlewareAliases; on Laravel 11+ in bootstrap/app.php inside ->withMiddleware(...) using $middleware->alias([...]):

'localize'              => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class,
'localizationRedirect'  => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class,
'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class,
'localeCookieRedirect'  => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class,
'localeViewPath'        => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class,

Wrap your routes once. This is the core mental model: you define each route a single time, and the package generates a prefixed copy per supported locale.

Route::group([
    'prefix'     => LaravelLocalization::setLocale(),
    'middleware' => ['localeSessionRedirect', 'localizationRedirect', 'localeViewPath'],
], function () {
    Route::get('/', fn () => view('hello'));
    Route::get('test', fn () => view('test'));
});

// Everything outside the group is locale-free: API, webhooks, health checks, assets.

Visiting /en/test, /es/test, and /test (which resolves the browser locale and redirects) all work from that one definition. App::getLocale() is set from the URL, so Laravel's own __() / @lang machinery works unchanged.

Check version compatibility before you start. The 2.0.x line requires PHP ≥ 8.2 and targets Laravel 10–13. If you're on an older stack, you'll want 1.8.x — don't let Composer pick a major that your PHP can't run.

First use case: a bilingual marketing site or admin panel where you want /en/about and /es/about without duplicating route files. Get the route group and localeSessionRedirect working against two locales before adding translated route slugs — the slug work is a second, independent layer.

Implementation Patterns

Separate localized from non-localized routes at the file level. The pattern that scales is routes/web.php containing the group, and everything else (routes/api.php, auth callbacks, Stripe webhooks, sitemap, asset routes) outside it. Prefixing an API route with a locale is almost never what you want and creates confusing client contracts.

Build a language switcher from getLocalesOrder() + getLocalizedURL(). Using getLocalizedURL($localeKey) returns the current page translated into the target locale, so switching languages keeps the user in place rather than dumping them on the homepage:

@foreach (LaravelLocalization::getLocalesOrder() as $code => $locale)
    <a href="{{ LaravelLocalization::getLocalizedURL($code) }}"
       hreflang="{{ $code }}"
       @class(['active' => $code === LaravelLocalization::getCurrentLocale()])>
        {{ $locale['native'] }}
    </a>
@endforeach

getLocalesOrder() respects localesOrder in config; use it for display, not getSupportedLocales(), if you've configured a custom ordering.

Never hardcode href in Blade — route everything through localizeUrl(). This is the single highest-leverage habit with this package:

<a href="{{ LaravelLocalization::localizeUrl('/about') }}">About</a>
<form method="POST" action="{{ LaravelLocalization::localizeUrl('/contact') }}">

localizeUrl() resolves route model binding and honors hideDefaultLocaleInURL and translated routes, so /about becomes /es/acerca when that translation exists. Forgetting this on a single form turns a POST into a GET via the redirect middleware — see Gotchas.

Adopt translated routes for public-facing URLs. Put a routes.php lang file per locale (lang/en/routes.php, lang/es/routes.php) with keys like 'about' => 'about' / 'about' => 'acerca'. Then LaravelLocalization::getURLFromRouteNameTranslated('es', 'routes.about') yields /es/acerca. Note the method returns false when the key is missing in that locale — guard it rather than emitting an empty href. It accepts a parameter array for dynamic segments, e.g. ['category' => 'ghosts'] → /fr/archive/fantômes.

Pick one persistence strategy: session or cookie. localeSessionRedirect writes the locale to the session on every localized request; localeCookieRedirect does the same via cookie. Session gives you per-tab-ish behavior but dies with the session and doesn't survive for anonymous long-tail visitors well; cookie is what you want for a marketing site where returning visitors should land in their language. Don't register both — you'll get competing redirect decisions.

Use localeViewPath only if you actually split view directories. It changes the view base path to resources/views/{locale}/, mirroring the lang structure. It's a nice fit for sites with genuinely divergent layouts per language, and a trap for sites that share one layout with translated strings — the moment a view is missing from one locale folder you get a 500 that's maddening to trace.

Handle validation and flash messages with the locale already set. Because the locale comes from the middleware/route prefix, validation errors rendered after a redirect back will be in the correct language provided the redirect target is localized. This is where localizeUrl() on form actions pays for itself twice.

In tests, be explicit about locale. Hit prefixed URLs ($this->get('/es/test')) for integration coverage, and use App::setLocale('es') when unit-testing translation output. Assert on LaravelLocalization::getCurrentLocale() rather than App::getLocale() if you're testing package behavior. Beware that redirect middleware will turn a bare /test request into a 302 — assert accordingly, or disable the redirect middleware in that test class.

SEO wiring: emit <link rel="alternate" hreflang="{{ $code }}" href="{{ LaravelLocalization::getLocalizedURL($code) }}"> for each locale plus an x-default, and canonicalize to the localized URL, not the unprefixed one. Without redirect middleware, robots can crawl /test repeatedly and receive different locale content — a duplicate-content problem the README explicitly calls out.

Gotchas and Tips

php artisan route:cache does not work out of the box. This is the package's defining architectural constraint: routes are generated dynamically per request via setLocale() inside the route file, which is fundamentally incompatible with serialized route caching. On a high-traffic app this is a real production consideration. The README now explicitly points to niels-numbers/laravel-localizer as the successor with statically registered routes and native route:cache support, and links a migration guide. If you're greenfield, evaluate the successor before committing; if you're staying, budget for the fact that route caching is off the table.

POST forms that aren't localized become GETs. A form posting to /contact (no locale) hits a redirect middleware on the way in, the 302 downgrades the method, the payload and CSRF token vanish, and you get a "method not allowed" or a silently empty request. Symptom: MethodNotAllowedHttpException or missing _token. Fix: always wrap form action in localizeUrl().

Validation messages stuck in the default locale. Usually caused by the locale not being set before the validator runs — e.g. the route lives outside the localized group, or you're validating in a FormRequest triggered from a non-localized AJAX endpoint. Verify the request actually passed through the localized group; a quick dd(App::getLocale()) in the controller confirms it.

Middleware order and MethodNotAllowedHttpException. localizationRedirect and localeSessionRedirect must run in the right sequence relative to SubstituteBindings and your auth middleware. If you see method-not-allowed errors only on some locales, suspect an ordering issue in the group's middleware array rather than your routes. The localize alias (LaravelLocalizationRoutes) is the legacy/explicit alternative to the prefix => setLocale() idiom — pick one approach per route group; mixing them produces double-prefixing or no prefix at all.

Redirect loops with hideDefaultLocaleInURL. With the default locale hidden, /en/test is redirected to /test, and localeSessionRedirect may then redirect /test back to a locale — check the combination against your session state. The safe configuration is: hidden default locale + localizationRedirect + one of the redirect middlewares, plus urlsIgnored for anything that must never be touched.

urlsIgnored is your escape hatch. Add patterns for health checks, Horizon/Telescope, sitemap.xml, and any vendor routes that break when a locale prefix is applied. Also remember to exclude locale prefixes from asset/CDN rewriting rules at the web-server level.

Custom keys via localesMapping and renamed locale keys. You can map en-GB to a friendlier uk URL segment, and you can rename locale keys outright — but keys must not collide, and any stored session/cookie value from before a rename becomes an unknown locale. Plan migrations of persisted locale values when you rename keys.

getURLFromRouteNameTranslated() returns false, not an empty string. If you drop it straight into href="{{ ... }}" you'll render href="", which navigates to the current page. Guard it and fall back to localizeUrl().

Console commands and queued jobs have no locale. The locale is a request-scoped artifact of the URL. In jobs and Artisan commands, App::getLocale() is whatever the default is — set it explicitly per recipient/record before rendering mailables or notifications.

Extension points are limited, so wrap rather than subclass. There's no first-class binding to swap the LaravelLocalization implementation. If you need to customize behavior, wrap the facade in your own helper/Blade component, add custom middleware before or after the package's, and keep config changes in config/laravellocalization.php (never edit the vendor copy). For anything genuinely structural — static routes, route caching — migration to the successor is the cleaner extension path.

Environment quirks: utf8suffix exists for CentOS-style systems where setlocale needs a different suffix; if locale-aware PHP functions (dates, number formatting via Intl) return English on a server that should be localized, that config value is the first thing to check.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource