mikemenard/laravel-localization
最新稳定版本:v1.0.0
Composer 安装命令:
composer require mikemenard/laravel-localization
包简介
A modern, flexible Laravel package for URL-based localization with seamless Livewire and third-party package support
README 文档
README
A modern, flexible Laravel package for URL-based localization with seamless Livewire and third-party package support.
Why This Package?
Most Laravel localization packages force you to wrap all your routes in localized groups, breaking compatibility with Livewire and third-party packages that generate their own URLs. This package takes a different approach.
The Solution:
Instead of requiring every route to be in a localized group, this package uses:
- A single global middleware assigned to the
webgroup that sets the locale andURL::defaults()for every request - Session-based persistence that remembers the user's locale choice
- Optional route-specific middleware for routes that need locale prefixes
This architecture means:
- ✅
route('profile')automatically generates/fr/profilewhen French is active - ✅ Livewire components work without modification
- ✅ Third-party packages (Filament, Nova, etc.) respect the current locale
- ✅ No need to wrap every single route in a localized group
- ✅ Session persistence works across the entire application
Features
- 🌍 URL-based localization with configurable route parameters
- 🔄 Automatic URL generation via
URL::defaults()- no manual locale passing needed - 💾 Session persistence - remembers user locale preference
- 🎯 Smart locale resolution - session → browser headers → app default
- 🔗 Livewire compatible - works seamlessly with Livewire navigation
- 📦 Third-party friendly - packages automatically use the current locale
- 🎨 Clean route macro -
Route::localized()for prefix-based routes - 🛡️ Strict enforcement - optional middleware for canonical URL redirects
- 🔧 Extensible Locale DTO - add custom properties via magic methods
- ⚡ Zero configuration - works out of the box with sensible defaults
Requirements
- PHP 8.4+
- Laravel 10+
Installation
Install the package via Composer:
composer require mikemenard/laravel-localization
The package will automatically register via Laravel's package auto-discovery.
Publish the configuration file (optional):
php artisan vendor:publish --tag=localization-config
Quick Start
1. Add the Global Middleware
Add the Localize middleware to your web middleware group in bootstrap/app.php:
use Mikemenard\Localization\Middleware\Localize; return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware): void { $middleware->web(append: [ Localize::class, ]); }) // ...
Important: Use $middleware->web() instead of $middleware->append().
2. Define Your Locales
Edit config/localization.php:
return [ 'supported_locales' => [ 'en' => ['label' => 'English', 'language' => 'en'], 'fr-ca' => ['label' => 'Français', 'language' => 'fr'], 'es' => ['label' => 'Español', 'language' => 'es'], ], 'route_parameter' => 'locale', 'locale_class' => \Mikemenard\Localization\Locale::class, ];
3. Create Localized Routes
Use the Route::localized() macro for routes that should have locale prefixes:
use Illuminate\Support\Facades\Route; // Root redirect to localized homepage Route::get('/', fn() => redirect('/'.Localization::resolveLocale())); // Localized routes Route::localized(function () { Route::get('/', fn() => view('welcome')); Route::get('/about', fn() => view('about')); Route::get('/contact', fn() => view('contact')); });
This generates:
/en/→ English homepage/fr/→ French homepage/en/about→ English about page/fr/about→ French about page
4. Add a Language Switcher (example)
Create a Blade component at resources/views/components/language-switcher.blade.php:
@use(\Mikemenard\Localization\Facades\Localization) <div class="language-switcher"> @foreach(Localization::getLocales() as $locale) @if(!Localization::isCurrentLocale($locale)) <a href="{{ Localization::localizeCurrentUrl($locale) }}"> {{ $locale->label }} </a> @break @endif @endforeach </div>
Use it in your layout:
<nav> <x-language-switcher /> </nav>
Configuration
The configuration file provides three main options:
Supported Locales
Define all locales your application supports:
'supported_locales' => [ 'en' => [ 'label' => 'English', 'language' => 'en', ], 'fr-ca' => [ 'label' => 'Français', 'language' => 'fr', ], 'es' => [ 'label' => 'Español', 'language' => 'es', ], ],
Each locale requires:
- Key: The locale code (e.g.,
'en','fr-ca') - normalized to lowercase with hyphens label: Display name for the language switcherlanguage: Language code for the locale
You can add custom properties that will be accessible via the Locale DTO:
'en' => [ 'label' => 'English', 'language' => 'en', 'flag' => '🇬🇧', 'direction' => 'ltr', 'currency' => 'USD', ],
Access custom properties:
@foreach(Localization::getLocales() as $locale) <span>{{ $locale->flag }}</span> {{ $locale->label }} @endforeach
Route Parameter
Customize the route parameter name used in URLs:
'route_parameter' => 'locale', // Default // Or use a different name: 'route_parameter' => 'lang',
Validation: Must be a single word (alphanumeric and underscores only) - no slashes or special characters.
This affects:
- URL structure:
/{locale}/aboutvs/{lang}/about - Route parameters:
route('about', ['locale' => 'fr'])vsroute('about', ['lang' => 'fr'])
Locale Class
Specify a custom Locale DTO class:
'locale_class' => \App\Localization\CustomLocale::class,
Your custom class must extend \Mikemenard\Localization\Locale:
namespace App\Localization; use Mikemenard\Localization\Locale; class CustomLocale extends Locale { public function displayName(): string { return strtoupper($this->label); } public function flagEmoji(): string { return $this->flag ?? '🏳️'; } }
How It Works: The Two-Middleware System
This package uses two middleware classes for different purposes:
1. Localize Middleware (Global)
Purpose: Sets the locale for every request and makes URL generation work automatically.
Added to: web middleware group in bootstrap/app.php
Behavior:
- Extracts locale from the first URL segment (e.g.,
/fr/about→'fr') - Falls back to session → browser headers → app default if no locale in URL
- Calls
App::setLocale()to set the application locale - Calls
URL::defaults(['locale' => 'fr'])so all route generation includes the locale - Saves the locale to session for persistence
Why this matters:
- Makes
route('profile')automatically generate/fr/profilewhen French is active - Livewire components automatically use the correct locale in their URLs
- Third-party packages respect the current locale without modification
- Session persistence means the locale "sticks" across requests
2. EnforceLocale Middleware (Route-Specific)
Purpose: Enforces locale prefixes and handles redirects for routes that must have a locale.
Added to: Specific route groups via Route::localized() macro or manual route groups
Behavior:
- Validates the locale in the URL is in the correct format
- Redirects to a valid locale if the format is invalid
- Returns 404 if an unsupported locale is requested
- Redirects to canonical locale format (e.g.,
en_US→en-us) - Ensures strict URL structure for SEO and consistency
Example:
Route::localized(function () { Route::get('/', HomeController::class); Route::get('/products', ProductController::class); });
URLs like /invalid/products get redirected to /en/products (or user's session locale).
Route Macro Usage
The Route::localized() macro simplifies creating localized route groups.
Basic Usage
Route::localized(function () { Route::get('/', fn() => view('home')); Route::get('/about', fn() => view('about')); });
Equivalent to:
Route::prefix('{locale}') ->middleware(\Mikemenard\Localization\Middleware\EnforceLocale::class) ->group(function () { Route::get('/', fn() => view('home')); Route::get('/about', fn() => view('about')); });
Chainable Middleware
You can chain additional middleware before calling group():
Route::localized() ->middleware('auth') ->group(function () { Route::get('/dashboard', DashboardController::class); Route::get('/profile', ProfileController::class); });
Multiple middleware:
Route::localized() ->middleware(['auth', 'verified']) ->group(function () { Route::resource('posts', PostController::class); });
Name Prefixing
Combine with Laravel's name prefixing:
Route::localized() ->name('admin.') ->prefix('admin') ->middleware('auth') ->group(function () { Route::get('/dashboard', DashboardController::class)->name('dashboard'); // Route name: admin.dashboard // URL: /en/admin/dashboard });
Manual Route Groups (Alternative)
If you prefer not to use the macro, define the group manually:
Route::prefix('{locale}') ->middleware(\Mikemenard\Localization\Middleware\EnforceLocale::class) ->group(function () { // Your routes });
Language Switcher Component
Create a reusable language switcher component at resources/views/components/language-switcher.blade.php:
@use(\Mikemenard\Localization\Facades\Localization) <div class="flex gap-4"> @foreach(Localization::getLocales() as $locale) @if(!Localization::isCurrentLocale($locale)) <a href="{{ Localization::localizeCurrentUrl($locale) }}" class="text-blue-600 hover:text-blue-800" > {{ $locale->label }} </a> @else <span class="font-bold text-gray-900"> {{ $locale->label }} </span> @endif @endforeach </div>
Explanation:
Localization::getLocales()returns all configured locales as Locale DTOsLocalization::isCurrentLocale($locale)checks if the locale is currently activeLocalization::localizeCurrentUrl($locale)generates the current URL with the locale switched
Advanced Language Switcher
With custom flags and dropdown:
@use(\Mikemenard\Localization\Facades\Localization) <div class="relative" x-data="{ open: false }"> <button @click="open = !open" class="flex items-center gap-2"> {{ Localization::currentLocale()->flag }} {{ Localization::currentLocale()->label }} </button> <div x-show="open" @click.away="open = false" class="absolute mt-2 bg-white shadow-lg rounded"> @foreach(Localization::getLocales() as $locale) <a href="{{ Localization::localizeCurrentUrl($locale) }}" class="block px-4 py-2 hover:bg-gray-100" > {{ $locale->flag }} {{ $locale->label }} </a> @endforeach </div> </div>
Livewire-Compatible Switcher
If using Livewire with wire:navigate:
@use(\Mikemenard\Localization\Facades\Localization) <div> @foreach(Localization::getLocales() as $locale) @if(!Localization::isCurrentLocale($locale)) <a wire:navigate href="{{ Localization::localizeCurrentUrl($locale) }}" > {{ $locale->label }} </a> @endif @endforeach </div>
Customizing the Locale DTO
The Locale class uses magic methods to allow custom properties.
Adding Custom Properties
In your config:
'supported_locales' => [ 'en' => [ 'label' => 'English', 'language' => 'en', 'flag' => '🇬🇧', 'direction' => 'ltr', 'currency' => 'USD', 'date_format' => 'Y-m-d', ], 'fr' => [ 'label' => 'Français', 'language' => 'fr', 'flag' => '🇫🇷', 'direction' => 'ltr', 'currency' => 'EUR', 'date_format' => 'd/m/Y', ], 'ar' => [ 'label' => 'العربية', 'language' => 'ar', 'flag' => '🇸🇦', 'direction' => 'rtl', 'currency' => 'SAR', 'date_format' => 'd/m/Y', ], ],
Access custom properties via magic __get():
@foreach(Localization::getLocales() as $locale) <div dir="{{ $locale->direction }}"> {{ $locale->flag }} {{ $locale->label }} </div> @endforeach <div> Currency: {{ Localization::currentLocale()->currency }} Date format: {{ Localization::currentLocale()->date_format }} </div>
Extending the Locale Class
Create a custom Locale class:
namespace App\Localization; use Mikemenard\Localization\Locale as BaseLocale; class CustomLocale extends BaseLocale { public function uppercaseLabel(): string { return strtoupper($this->label); } public function isRtl(): bool { return $this->direction === 'rtl'; } public function flagEmoji(): string { return $this->flag ?? '🏳️'; } public function formattedDate(\DateTime $date): string { return $date->format($this->date_format); } }
Register it in config:
'locale_class' => \App\Localization\CustomLocale::class,
Use custom methods:
@foreach(Localization::getLocales() as $locale) <div class="{{ $locale->isRtl() ? 'text-right' : 'text-left' }}"> {{ $locale->flagEmoji() }} {{ $locale->uppercaseLabel() }} </div> @endforeach
API Reference
Facade Methods
All methods are available via the Localization facade:
use Mikemenard\Localization\Facades\Localization;
supported(): array
Returns an array of all supported locale codes.
Localization::supported(); // ['en', 'fr', 'es']
getLocales(): Collection
Returns a Collection of Locale objects.
Localization::getLocales()->each(function ($locale) { echo $locale->code . ': ' . $locale->label; });
currentLocale(): Locale
Returns the Locale object for the current application locale.
$current = Localization::currentLocale(); echo $current->label; // 'English'
setLocale(string|Locale $locale): string
Sets the application locale, updates session, and sets URL defaults.
Localization::setLocale('fr'); // Returns 'fr' Localization::setLocale($localeObject); // Also accepts Locale objects
Throws: UnsupportedLocaleException if locale is not supported.
isSupported(string|Locale|null $locale): bool
Checks if a locale is supported.
Localization::isSupported('fr'); // true Localization::isSupported('de'); // false Localization::isSupported(null); // false
isCurrentLocale(string|Locale $locale): bool
Checks if a locale is the current application locale.
Localization::isCurrentLocale('en'); // true if current locale is 'en'
localizeCurrentUrl(string|Locale $locale): string
Returns the current URL with the locale switched.
// Current URL: /en/products?sort=price Localization::localizeCurrentUrl('fr'); // '/fr/products?sort=price'
resolveLocale(): string
Resolves the locale from session → browser headers → app default.
$locale = Localization::resolveLocale(); // 'fr' (from session or browser)
getRouteParameter(): string
Returns the configured route parameter name.
Localization::getRouteParameter(); // 'locale'
isCanonical(string $locale): bool
Checks if a locale is in canonical format (exact match in supported locales).
Localization::isCanonical('en'); // true Localization::isCanonical('en_US'); // false (should be 'en-us')
getLocaleFromUrl(): ?string
Extracts the locale from the current request's route parameter.
// URL: /fr/about Localization::getLocaleFromUrl(); // 'fr'
isLocale(string $locale): bool
Validates if a string matches the locale format pattern.
Localization::isLocale('en'); // true Localization::isLocale('en-us'); // true Localization::isLocale('invalid'); // false
Locale DTO Properties
Every Locale object has these properties:
$locale->code; // string - Locale code (e.g., 'en', 'fr-ca') $locale->label; // string - Display label (e.g., 'English') $locale->language; // string - Language code (e.g., 'en') // Plus any custom properties from config via __get(): $locale->flag; // Custom property $locale->direction; // Custom property $locale->currency; // Custom property
Advanced Usage
Programmatic Locale Switching
In controllers or middleware:
use Mikemenard\Localization\Facades\Localization; class LocaleController { public function switch(string $locale) { try { Localization::setLocale($locale); return redirect()->back(); } catch (\Mikemenard\Localization\Exceptions\UnsupportedLocaleException $e) { abort(404, $e->getMessage()); } } }
URL Generation
Thanks to URL::defaults() being set automatically, all route helpers work:
// Current locale: 'fr' route('about'); // '/fr/about' url('/contact'); // '/fr/contact' action([ProductController::class, 'index']); // '/fr/products'
Override for a specific locale:
route('about', ['locale' => 'en']); // '/en/about'
Accessing Session Directly
The package stores locale in session under key localization:locale:
session('localization:locale'); // 'fr' session(['localization:locale' => 'en']); // Manually set
Browser Locale Detection
The package automatically detects browser locale from Accept-Language headers:
Accept-Language: fr-FR,fr;q=0.9,en;q=0.8
Will resolve to 'fr' if no session locale exists.
Locale Fallback Logic
When matching locales, the package uses intelligent fallback:
- Exact match:
'fr'matches'fr'in config - Prefix match:
'fr'matches'fr-ca'in config (if no exact match) - Language code match:
'fr-ca'matches'fr'in config (if no exact or prefix match)
Example:
// Config: ['en', 'fr-ca'] Localization::isSupported('fr-ca'); // true (exact) Localization::isSupported('fr'); // true (matches 'fr-ca' via prefix) Localization::isSupported('en-us'); // true (matches 'en' via language code)
Testing
In tests, you can manually set the locale:
use Mikemenard\Localization\Facades\Localization; test('homepage shows French content', function () { Localization::setLocale('fr'); $response = $this->get(route('home', ['locale' => 'fr'])); $response->assertSee('Bienvenue'); });
Exception Handling
The package throws descriptive exceptions for common errors:
UnsupportedLocaleException
Thrown when trying to set an unsupported locale:
try { Localization::setLocale('de'); // Not in config } catch (\Mikemenard\Localization\Exceptions\UnsupportedLocaleException $e) { // "Locale 'de' is not supported. Supported locales: en, fr." }
InvalidRouteParameterException
Thrown when route parameter config is invalid:
// config/localization.php 'route_parameter' => 'locale/invalid', // Invalid! // Throws: "The route parameter 'locale/invalid' is invalid. // It must be a single word without slashes, optionals (?), or special characters."
InvalidLocaleClassException
Thrown when custom locale class is invalid:
// config/localization.php 'locale_class' => \App\Models\User::class, // Doesn't extend Locale! // Throws: "The locale class 'App\Models\User' must extend \Mikemenard\Localization\Locale."
FAQ
Does this replace Laravel's built-in localization?
No, it complements it. Laravel's trans(), __(), and language files still handle translations. This package manages locale selection, URL structure, and session persistence.
Can I use this with Livewire?
Yes! That's one of the main reasons this package exists. The global middleware ensures Livewire respects the current locale without modification.
What about SEO and duplicate content?
Use the EnforceLocale middleware on your localized routes to ensure canonical URLs and automatic redirects. This prevents duplicate content issues.
Can I have non-localized routes?
Absolutely. Only routes inside Route::localized() groups require locale prefixes. Other routes work normally:
Route::get('/health', fn() => 'OK'); // No locale prefix Route::get('/api/users', [UserController::class, 'index']); // No locale prefix Route::localized(function () { Route::get('/', fn() => view('home')); // Requires locale prefix });
How do I handle the root URL /?
Create a redirect to your default or detected locale:
use Mikemenard\Localization\Facades\Localization; Route::get('/', function () { return redirect('/' . Localization::resolveLocale()); });
Can I use subdomain-based locales?
Not currently. This package is designed for path-based locales (/en/about, /fr/about).
Does this work with Laravel Jetstream/Breeze?
Yes, but you'll need to wrap their routes in Route::localized() groups if you want localized URLs for auth pages.
License
This package is open-source software licensed under the MIT license.
Credits
Created by Mike Menard.
Built for Laravel 10+ with PHP 8.4+.
统计信息
- 总下载量: 43
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-11-05