Latest Posts

Setting up locale based routing in Laravel with middleware

Tutorials

Laravel is an excellent framework that supports multilingual functionality out of the box - but it doesn't go as far as setting up the routing for you. Here's how you can achieve it with simple routing and middleware.


A common strategy when it comes to multilingual routing is to use the locale prefix as the first 'segment' of your URL. An example of this would be https://christalks.dev/en/my-blog-post - note that I have defined the locale to use for the system by definining en within my URL structure.

That's great, but in practice this won't actually achieve anything. Firstly, lets setup our routing structure in our routes/web.php file:

Determining the users locale

Firstly, when the user hits your main website route, e.g. https://christalks.dev, we want to determine the users preferred language. Laravel offers this functionality out of the box by utilising the users language preference.

Route::get('/', function (\Illuminate\Http\Request $request) {
    $locale = $request->getPreferredLanguage(['en', 'es']);

    return redirect($locale);
});

The above will work out the users preferred language out of the available options (English or Spanish) - based on the users keyboard preferences - and simply redirect them on to the locale based routes.

Define the locale based routes

With that done, we now need to define the routes themselves. Using a simple example below, we'll setup a route for the home page, and a route for a blog post. In practice, everything that you want to be prefixed with a locale should go inside this route.

Route::prefix('{locale}')->middleware(App\Http\Middleware\LocaleMiddleware::class)->group(function () {

    Route::get('/', fn () => view('welcome'));

    Route::get('/{post}', fn (App\Models\BlogPost $post) => view('my-blog-post', $post));

})->where(['locale' => '[a-z]{2}']);

All routes within this group should now be prefixed with the locale, as defined on the first highlighted line. To limit the possible matches here, we'll make sure that our locale is always a two letter string, consisting of letters only - this is achieved by the where method on the last line.

You'll notice on the first line, we're setting up some locale middleware. Now this is where the magic happens. Lets have a look at the middleware we've built...

The middleware

Middleware is one of the most powerful tools in Laravel's arsenal and it can handle anything from security and authorisation to managing preferences. In this case, we'll set up some preferences, telling Laravel how to process this locale based request:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\View;

class LocaleMiddleware
{
    public function handle(Request $request, Closure $next): mixed
    {
        $locale = $request->segment(1);

        App::setLocale($locale); 
        View::share('locale', $locale);
        URL::defaults(['locale' => $locale]);

        $request->setLocale($locale);

        return $next($request);
    }
}

And that's our middleware done! It's time to give your routing a test!

What is the middleware actually doing?

We're calling on a few of Laravel's core services, just to let it know that the locale has been updated. This gives us consistent functionality throughout the rest of our application.

On line 16 we grab the locale, we know it'll be the first segment in the URL because of the routes we've defined, and we know it'll be a two letter locale string.

On line 18, we tell the application that we're changing the apps language to the defined locale.

On line 19, we're sharing with our views the locale we have set. This way you can access {{ $locale }} within your blade files.

On line 20, we're telling the URL builder - perhaps the most critical part - that any route that contains the {locale} binding should be swapped out for the locale we're setting.

On line 22, we're setting the locale for the remainder of the current request - just in case its used anywhere else.

Finally, on line 24, we pass the request on to the next bit of middlware.

Additional considerations

As with any tutorial, this is just an example and there are a number of cases this doesn't consider...

Security: One particular case that springs to mind is that it doesn't offer any validation to check whether the locale actually exists. To achieve this, you might want to setup an enum that contains a definition of all your applications locales.

Third-party packages: If you're using a package like Livewire, you may find it beneficial to update how the routing works so that any update requests are sent with a locale prefix via the locale middleware too. You can achieve this by adding the below to the boot method of your AppServiceProvider as per Livewire's documentation

Livewire\Livewire::setUpdateRoute(function ($handle) {
    return Route::post('{locale}/livewire', $handle)
        ->middleware(['web', App\Http\Middleware\LocaleMiddleware::class]);
});

Persistence: When a users language preference is set, you might want to store that against the session or in a cookie, so that when the user returns, rather than adopting their keyboard preference you are adopting their previously chosen preference - but I'll leave that to you!

I think that's about it - if you enjoyed this, please consider subscribing to my blog!