Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

sboj2scz's avatar

Optional language for all routes

Hi,

I'm building multi-lingual website using Laravel 5.4 and:

  1. I'm looking for a way to add optional language prefix to all routes in "routes/web.php" without need to manually specify anything for any new route I add.
  2. Then somewhere in single place I want to change locale based on language from url (when present).

So far I've came up with this solution:

In the \App\Providers\RouteServiceProvider::mapWebRoutes method added ->prefix('{lang?}') to cover all routes at once (the ? marks parameter as optional):

Route::middleware('web')
     ->namespace($this->namespace)
     ->prefix('{lang?}')
     ->group(base_path('routes/web.php'));

In the \App\Providers\RouteServiceProvider::boot method restricted lang route parameter to known languages only and set locale when route is matched:

Route::pattern('lang', 'en|ru|lv');

Route::matched(function (RouteMatched $event) {
    app()->setLocale($event->route->parameter('lang'));
});

According to the docs it should work, but it doesn't and I'm getting HTTP 404 error because no route is matched.

While debugging route matching process I've found, that regexp for url matching is build as if lang parameter was required.

The /en/login url works, but /login doesn't.

P.S. Doubling routes count for language in url and without it isn't a way to go, because I won't be able to use named routes due route name collisions.

Thanks.

0 likes
11 replies
Exiax's avatar

Hi,

a while ago i developed a multilingual platform for a client and used the following way to implement any number of languages needed:

Routes file:

// German
Route::get('/musikfilter', 'ProductController@filter')->name('de.products.filter');

// English
Route::group(['prefix' => 'en'], function () {
    Route::get('/musicfilter', 'ProductController@filter')->name('en.products.filter');
});

To avoid conflicting named routes i prefix them with an abbreviation of the name of the language.

Next we need to find a way to make working with named routes simple again.

For that i implemented a new function "localizeRoute" and saved it in "app\Http\Helper\LocaleHelper.php"

/*
 * Used to localize a route based on what locale the application is running in.
 */
function localizeRoute($name, $locale = '') {
    return (empty($locale) ? App::getLocale().'.'.$name : $locale.'.'.$name);
}

To make that function available in every part of your application you need to register it. I did so in the "register"-method of "AppServiceProvider.php"

/**
 * Register any application services.
 *
 * @return void
 */
public function register()
{
    require_once base_path('app/Http/Helpers/LocaleHelper.php');

    // ...
}

Now we can use it very easily in every part of our application. Some examples:

// In controllers
return redirect()->route(localizeRoute('users.index'));

// In views
<a href="{{ route(localizeRoute('users.edit'), $user->id) }}"><i class="fa fa-edit fa-lg" aria-hidden="true"></i></a>

Finally we need to solve the problem of changing the applications locale based on the url. I wrote a middleware "LocaleMiddleware" for that

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\App;
use Carbon\Carbon;

class LocaleMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {   
        /*
         * We read the first segment of an URL (e.g. /en) and set it as the applications locale, 
         * if it's part of the allowed locales. Otherwise the fallback locale is used.
         */ 
        $locale = (in_array(request()->segment(1), config('app.locales')) ? request()->segment(1) : config('app.fallback_locale'));
        App::setLocale($locale);
    Carbon::setLocale($locale);

        return $next($request);
    }
}

Don't forget to include it in "App\Http\Kernel.php"

protected $middleware = [
    \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,

    \App\Http\Middleware\LocaleMiddleware::class,
    \App\Http\Middleware\RedirectMiddleware::class,
];

Hope that helps!

sboj2scz's avatar

Thanks, I've found similar solutions online as well, but each of them had drawbacks.

Since translating the actual routes isn't a requirement for me things could be simplified probably.

But I did like the {{ route(localizeRoute('users.edit'), $user->id) }} part in your solution. If only we can somehow set default route parameter values so user shouldn't care if he should or should not specify language for particular route.

From router component point of view having first parameter optional is a pain and I guess that's why it's not working, but if I've applied regexp restriction on that first optional parameter then it should work, but it doesn't.

Some discussions online explained that this might be underlying Symfony router limitation, but I haven't found that limitation in it's code.

Exiax's avatar

Yeah, i had to do a good amount of research and fiddling around with different approaches to find a solution which felt "clean".

What i like about having a middleware for setting the applications locale is how flexible it is.

You can very easily drop the url-based approach and replace it with a session-based variant or even solutions based on the HTTP Header "Accept-Language".

sboj2scz's avatar

What I'm worried about with approach involving creation of routes with/without language is performance during matching. Current matching code isn't aware of semantics in routes (fact that 1st parameter is always language) and therefore it will first match all routes without language and then all routes with language.

For example CMS website with 700 pages on 2 languages. That would be 1400 routes to match on every page visit compared to 700 routes to match if we manage to make 1st parameter optional.

I know that is a route cache, but only speeds up router configuration, but not actual matching. I have no idea if that thing can be improved.

sboj2scz's avatar
sboj2scz
OP
Best Answer
Level 1

I've just spoke to our SEO guy and he told, that optional language in URL is actually a bad thing and if URL without language is accessed, then 301 redirect to URL with primary language should be made.

Here is code that reflects this:

In the \App\Providers\RouteServiceProvider::boot method:

// Specify available languages for routes.
Route::pattern('_locale', \implode('|', $this->app['config']['app.locales']));

Route::matched(function (RouteMatched $event) {
    // Get language from route.
    $locale = $event->route->parameter('_locale');

    // Ensure, that all built urls would have "_locale" parameter set from url.
    url()->defaults(array('_locale' => $locale));

    // Change application locale.
    app()->setLocale($locale);
});

In the \App\Providers\RouteServiceProvider::mapWebRoutes method:

// Added "->prefix(...` line to auto-prefix all routes with locale.
Route::middleware('web')
     ->namespace($this->namespace)
     ->prefix('{_locale}')
     ->group(base_path('routes/web.php'));

In the \App\Exceptions\Handler::render method:

// When we've got non-matched route resulting in "404 Not Found" response.
if ($exception instanceof NotFoundHttpException) {
    $config = app('config');
    $default_locale = $config['app.locale'];
    $locale = $request->segment(1);

    // See if locale in url is absent or isn't among known languages.
    if (!\in_array($locale, $config['app.locales'])) {
        // Redirect to same url with default locale prepended.
        $uri = $request->getUriForPath('/' . $default_locale . $request->getPathInfo());

        return redirect($uri, 301);
    }
}

The url()->defaults(array('_locale' => $locale)); was really helpful, because when calling redirect()->route(...) and similar methods that build urls to route I no longer need to manually specify locale provided in the url.

4 likes
george_fourkas's avatar

@aik099 While your solution is one of the most well documented that i have found accross the internet, to all of you who are adding dynamically languages be carefull when adding 301 HTTP response code, to redirects. Browser caches the redirects and causes 404. The error is happening when you are going from multilingual to single language. For example if you have 2 languages English and French with route prefixes /en and /fr when you delete the french language you might want to redirect to the route without the locale prefix since now you are only using 1 language. If provided a 301 response code the browser will cache and redirect to /en which now does not exist. Use 307 response code, or set an appropriate header like Cache-Control:no-cache to prevent this.

1 like
MohammadAlAni's avatar

@george_fourkas You are right. Even if you change the default local to another one, the browser cache will still redirect to the previous default one. I think Cache-Control:no-cache is the best solution regarding to seo

and btw I think we should change the Exception handler

// When we've got non-matched route resulting in "404 Not Found" response.
if ($exception instanceof NotFoundHttpException) {
    $config = app('config');
    $default_locale = $config['app.locale'];
    $locale = $request->segment(1);

    // See if locale in url is absent or isn't among known languages.
    if (!\in_array($locale, $config['app.locales'])) {
        // Redirect to same url with default locale prepended.
        $uri = $request->getUriForPath('/' . $default_locale . $request->getPathInfo());

        return redirect($uri, 301);
    }
}

to

// When we've got non-matched route resulting in "404 Not Found" response.
if ($exception instanceof NotFoundHttpException) {
    $config = app('config');
    $default_locale = $config['app.locale'];
    $locale = $request->segment(1);

    // See if locale in url is absent or isn't among known languages.
    if (strlen($locale) !==2 && !in_array($locale, $config['app.locales'])) { // <---- updated
        // Redirect to same url with default locale prepended.
        $uri = $request->getUriForPath('/' . $default_locale . $request->getPathInfo());

        return redirect($uri, 301);
    }
	return parent::render($request, $e); // <---- updated
}

to show 404 page for unsupported language code and undefined routes

abowlfazl's avatar

@AIK099 - hello, thank you for your answer. but i have a problem... some of my pages not show with this method, and when input a wrong locale its cant recognize that. and i dont know how to change locale with select box with this method...

thank you for your help :)

jmvdholst's avatar

I got something similar implemented but I have some difficulties testing it. Any tips? When I do this in my test:


   $this->followingRedirects()->get('/dossiers')->assertSee('Dossiers');

I get a 404 in return but in real life it is being redirected to /en/dossiers

shamaseen's avatar

The solution is much simpler than that, all you need to do is to go to your app/Providers/RouteServiceProvider.php and duplicate the web route booting, once with the locale and once without, like:

$this->routes(function () {
            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->group(base_path('routes/web.php'));

            Route::middleware('web')
                ->prefix('{lang}')
                ->group(base_path('routes/web.php'));
        });

that is it :)

1 like
JeffreyTP's avatar

@shamaseen Nice and clean solution! But how to achieve this in Laravel 11 since Laravel 11 does not have a RouteServiceProvider file anymore but it's stored in bootstrap/app.php ?

Please or to participate in this conversation.