定制 mikemenard/laravel-localization 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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:

  1. A single global middleware assigned to the web group that sets the locale and URL::defaults() for every request
  2. Session-based persistence that remembers the user's locale choice
  3. Optional route-specific middleware for routes that need locale prefixes

This architecture means:

  • route('profile') automatically generates /fr/profile when 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 switcher
  • language: 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}/about vs /{lang}/about
  • Route parameters: route('about', ['locale' => 'fr']) vs route('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/profile when 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_USen-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 DTOs
  • Localization::isCurrentLocale($locale) checks if the locale is currently active
  • Localization::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:

  1. Exact match: 'fr' matches 'fr' in config
  2. Prefix match: 'fr' matches 'fr-ca' in config (if no exact match)
  3. 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

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-11-05