niels-numbers/laravel-localizer
Locale-aware routing for Laravel with static, route:cache-ready localized routes. Auto-detects language, redirects to prefixed URLs, and resolves route() to the correct locale. Successor to mcamara/laravel-localization.
Installation:
composer require niels-numbers/laravel-localizer
Publish the config (optional):
php artisan vendor:publish --provider="NielsNumbers\Localizer\LocalizerServiceProvider" --tag="config"
Configure Locales:
Edit config/localizer.php to define supported locales (e.g., ['en', 'de', 'fr']) and set a default.
First Route:
Route::localize(function () {
Route::get('/about', [AboutController::class, 'index'])->name('about');
});
This generates:
/about (auto-detects locale)/de/about, /fr/about (explicit locales)Middleware:
Add to app/Http/Kernel.php:
protected $middlewareGroups = [
'web' => [
// ... other middleware
\NielsNumbers\Localizer\Middleware\SetLocale::class,
\NielsNumbers\Localizer\Middleware\RedirectLocale::class,
],
];
Test:
/about → Redirects to /en/about (or detected locale)./de/about → Stays on /de/about.Add a Blade snippet to switch locales:
@foreach(config('localizer.locales') as $locale)
<a href="{{ route('about', [], ['locale' => $locale]) }}">
{{ $locale }}
</a>
@endforeach
Or use the built-in helper:
<a href="{{ route('about')->localizedSwitcherUrl('de') }}">Deutsch</a>
Localized Routes with Naming Enforcement:
Route::localize(function () {
Route::get('/about', [AboutController::class, 'index'])->name('about'); // Named route
Route::get('/contact', [ContactController::class, 'index']); // Unnamed route (now stays unnamed)
});
Route::localize() no longer inherit a default name (with_locale. prefix). They remain unnamed but are still localized.Route::translate() now requires all routes to be named. If a route lacks a name, it throws UnnamedTranslatedRouteException at registration.Mixed Routes: Combine with standard routes:
Route::get('/static-page', [StaticController::class, 'index'])->name('static');
Route::localize(function () {
Route::get('/dynamic-page', [DynamicController::class, 'index'])->name('dynamic');
});
AppServiceProvider:
public function register()
{
$this->app->bind(
\Tighten\Ziggy\BladeRouteGenerator::class,
\NielsNumbers\Localizer\Ziggy\LocalizerBladeRouteGeneratorV2::class
);
}
@routes
<script>
// Ziggy's `route()` helper now respects locales
const url = route('about'); // Resolves to `/en/about` or current locale
</script>
app/Http/Middleware/HandleInertiaRequests.php:
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'locale' => app(\NielsNumbers\Localizer\Facades\Localizer::class)->getCurrentLocale(),
]);
}
// Vue example
const { locale } = window.__INERTIA__.shared;
Extend the detector chain in config/localizer.php:
'detectors' => [
\NielsNumbers\Localizer\Detectors\AcceptLanguageDetector::class,
\NielsNumbers\Localizer\Detectors\UserDetector::class,
\NielsNumbers\Localizer\Detectors\SessionDetector::class,
\App\Detectors\CustomDetector::class, // Your custom logic
],
Use Redirect::localized():
return Redirect::localized()->route('dashboard');
public function handle(Request $request, Closure $next)
{
$locale = app(\NielsNumbers\Localizer\Facades\Localizer::class)->getCurrentLocale();
if ($locale === 'de') {
// German-specific logic
}
return $next($request);
}
Unnamed Routes in Route::localize():
Route::localize() now stay unnamed (previously inherited a default name like with_locale.).localizedSwitcherUrl() or route()->localizedSwitcherUrl().Route::localize(function () {
Route::get('/contact', [ContactController::class, 'index'])->name('contact');
});
Route::translate() Naming Requirement:
Route::translate() now throws UnnamedTranslatedRouteException if any route inside it lacks a name.Route::translate() are named:
Route::translate(['en' => 'about', 'de' => 'ueber'], function () {
Route::get('/', [HomeController::class, 'index'])->name('home'); // Named
});
Case-Sensitive URLs:
/EN/about (wrong case) will 404 unless hide_default_locale is true.Route::localizedUrl() helpers or enforce lowercase in middleware.Route Caching:
php artisan route:cache, clear it if locales or routes are modified:
php artisan route:clear
Middleware Order:
SetLocale must run after StartSession and before SubstituteBindings.web(append: [SetLocale, RedirectLocale]) in Kernel.php.Query String Loss:
/posts?page=2 → /de/posts).localizedSwitcherUrl() with preserved query params:
{{ route('posts.index')->localizedSwitcherUrl('de', [], true) }}
Ziggy Version Mismatch:
LocalizerBladeRouteGeneratorV2 (not V1).composer.json for Ziggy version and update bindings.Check Current Locale:
dd(app(\NielsNumbers\Localizer\Facades\Localizer::class)->getCurrentLocale());
Inspect Route Names:
with_locale.about vs. without_locale.about:
dd(Route::current()->getName());
Disable Redirects Temporarily:
Set hide_default_locale: true in config/localizer.php to test unprefixed URLs.
Log Detector Chain: Add a custom detector to log the resolution process:
class DebugDetector implements DetectorInterface {
public function detect(Request $request): ?string {
\Log::info('Locale detection:', [
'accept_language' => $request->header('Accept-Language'),
'session' => $request->session()->get('locale'),
]);
return null; // Let other detectors handle it
}
}
Catch UnnamedTranslatedRouteException:
Wrap Route::translate() in a try-catch to handle missing names gracefully:
try {
Route::translate(['en' => 'home', 'de' => 'start'], function () {
Route::get('/', [HomeController::class, 'index']); // Unnamed (will throw)
});
} catch (\NielsNumbers\Localizer\Exceptions\UnnamedTranslatedRouteException $e) {
\Log::error('Missing route name in translated group: ' . $e->getMessage());
}
DetectorInterface:How can I help you explore Laravel packages today?