Home » Php » php – How to create multilingual translated routes in Laravel

php – How to create multilingual translated routes in Laravel

Posted by: admin April 23, 2020 Leave a comment

Questions:

I would like to create application with many translated routes depending on selected language. I’ve once described it at 3 methods of creating URLs in multilingual websites.

In this case it should be the first method from mentioned topic so:

  1. I have one default language
  2. I can have many other languages
  3. Current language should be calculated only by URL (without cookies/sessions) to make it really friendly also for search engines
  4. For default language there should be no prefix in URL, for other languages should be language prefix after domain
  5. Each part of url should be translated according to the current language.

Let’s assume I have set default language pl and 2 other languages en and fr. I have only 3 pages – mainpage, contact page and about page.

Urls for site should look then this way:

/
/[about]
/[contact]
/en
/en/[about]
/en/[contact]
/fr
/fr/[about]
/fr/[contact]

whereas [about] and [contact] should be translated according to selected language, for example in English it should be left contact but for Polish it should be kontakt and so on.

How can it be done as simple as possible?

How to&Answers:

First step:

Go to app/lang directory and create here translations for your routes for each language. You need to create 3 routes.php files – each in separate language directory (pl/en/fr) because you want to use 3 languages

For Polish:

<?php

// app/lang/pl/routes.php

return array(

    'contact' => 'kontakt',
    'about'   => 'o-nas'
);

For English:

<?php

// app/lang/en/routes.php

return array(
    'contact' => 'contact',
    'about'   => 'about-us'
);

For French:

<?php

// app/lang/fr/routes.php

return array(
    'contact' => 'contact-fr',
    'about'   => 'about-fr'
);

Second step:

Go to app/config/app.php file.

You should find line:

'locale' => 'en',

and change it into language that should be your primary site language (in your case Polish):

'locale' => 'pl',

You also need to put into this file the following lines:

/**
 * List of alternative languages (not including the one specified as 'locale')
 */
'alt_langs' => array ('en', 'fr'),

/**
 *  Prefix of selected locale  - leave empty (set in runtime)
 */
'locale_prefix' => '',

In alt_langs config you set alternative languages (in your case en and fr) – they should be the same as file names from first step where you created files with translations.

And locale_prefix is the prefix for your locale. You wanted no prefix for your default locale so it’s set to empty string. This config will be modified in runtime if other language than default will be selected.

Third step

Go to your app/routes.php file and put their content (that’s the whole content of app/routes.php file):

<?php

// app/routes.php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the Closure to execute when that URI is requested.
|
*/


/*
 *  Set up locale and locale_prefix if other language is selected
 */
if (in_array(Request::segment(1), Config::get('app.alt_langs'))) {

    App::setLocale(Request::segment(1));
    Config::set('app.locale_prefix', Request::segment(1));
}


/*
 * Set up route patterns - patterns will have to be the same as in translated route for current language
 */
foreach(Lang::get('routes') as $k => $v) {
    Route::pattern($k, $v);
}


Route::group(array('prefix' => Config::get('app.locale_prefix')), function()
{
    Route::get(
        '/',
        function () {
            return "main page - ".App::getLocale();
        }
    );


    Route::get(
        '/{contact}/',
        function () {
            return "contact page ".App::getLocale();
        }
    );



    Route::get(
        '/{about}/',
        function () {
            return "about page ".App::getLocale();

        }
    );

});

As you see first you check if the first segment of url matches name of your languages – if yes, you change locale and current language prefix.

Then in tiny loop, you set requirements for your all route names (you mentioned that you want have about and contact translated in URL) so here you set them as the same as defined in routes.php file for current language.

At last you create Route group that will have prefix as the same as your language (for default language it will be empty) and inside group you simply create paths but those parameters about and contact you treat as variables so you use {about} and {contact} syntax for them.

You need to remember that in that case {contact} in all routes will be checked if it’s the same as you defined it in first step for current language. If you don’t want this effect and want to set up routes manually for each route using where, there’s alternative app\routes.php file without loop where you set contact and about separately for each route:

<?php

// app/routes.php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It's a breeze. Simply tell Laravel the URIs it should respond to
| and give it the Closure to execute when that URI is requested.
|
*/

/*
 *  Set up locale and locale_prefix if other language is selected
 */
if (in_array(Request::segment(1), Config::get('app.alt_langs'))) {

    App::setLocale(Request::segment(1));
    Config::set('app.locale_prefix', Request::segment(1));
}


Route::group(array('prefix' => Config::get('app.locale_prefix')), function()
{
    Route::get(
        '/',
        function () {
            return "main page - ".App::getLocale();
        }
    );


    Route::get(
        '/{contact}/',
        function () {
            return "contact page ".App::getLocale();
        }
    )->where('contact', Lang::get('routes.contact'));



    Route::get(
        '/{about}/',
        function () {
            return "about page ".App::getLocale();

        }
    )->where('about', Lang::get('routes.about'));


});

Fourth step:

You haven’t mentioned about it, but there’s one extra thing you could consider. If someone will use url /en/something where something isn’t correct Route, I think the best solution to make redirection. But you should make redirection not to / because it’s default language but to /en.

So now you can open app/start/global.php file and create here 301 redirection for unknown urls:

// app/start/global.php

App::missing(function()
{
   return Redirect::to(Config::get('app.locale_prefix'),301);
});

Answer:

What Marcin Nabiałek provided us with in his initial answer is a solid solution to the route localization problem.

The Minor Bugbear:

The only real downside with his solution is that we cannot use cached routes, which can sometimes be of great benefit as per Laravel's docs:

If your application is exclusively using controller based routes, you
should take advantage of Laravel’s route cache. Using the route cache
will drastically decrease the amount of time it takes to register all
of your application’s routes. In some cases, your route registration
may even be up to 100x faster. To generate a route cache, just execute
the route:cache Artisan command.


Why can we not cache our routes?

Because Marcin Nabiałek’s method generates new routes based on the locale_prefix dynamically, caching them would result in a 404 error upon visiting any prefix not stored in the locale_prefix variable at the time of caching.


What do we keep?

The foundation seems really solid and we can keep most of it!

We can certainly keep the various localization-specific route files:

<?php

// app/lang/pl/routes.php

return array(

    'contact' => 'kontakt',
    'about'   => 'o-nas'
);

We can also keep all the app/config/app.php variables:

/**
* Default locale 
*/
'locale' => 'pl'

/**
 * List of alternative languages (not including the one specified as 'locale')
 */
'alt_langs' => array ('en', 'fr'),

/**
 *  Prefix of selected locale  - leave empty (set in runtime)
 */
'locale_prefix' => '',

 /**
 * Let's also add a all_langs array
 */
'all_langs' => array ('en', 'fr', 'pl'),

We will also need the bit of code that checks the route segments. But since the point of this is to utilize the cache we need to move it outside the routes.php file. That one will not be used anymore once we cache the routes. We can for the time being move it to app/Providers/AppServiceProver.php for example:

public function boot(){
  /*
   *  Set up locale and locale_prefix if other language is selected
   */
   if (in_array(Request::segment(1), config('app.alt_langs'))) {
       App::setLocale(Request::segment(1));
       config([ 'app.locale_prefix' => Request::segment(1) ]);
   }
}

Don’t forget:

use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\App;

Setting up our routes:

Several changes will occur within our app/Http/routes.php file.

Firstly we have to make a new array contain all of the alt_langs as well as the default locale_prefix, which would most likely be '':

$all_langs = config('app.all_langs');

In order to be able to cache all the various lang prefixes with translated route parameters we need to register them all. How can we do that?

*** Laravel aside 1: ***

Let’s take a look at the definition of Lang::get(..):

public static function get($key, $replace = array(), $locale = null, $fallback = true){
      return \Illuminate\Translation\Translator::get($key, $replace, $locale, $fallback);
}

The third parameter of that function is a $locale variable! Great – we can certainly use that to our advantage! This function actually let’s us choose which locale we want to obtain the translation from!

The next thing we are going to do is iterate over the $all_langs array and create a new Route group for each language prefix. Not only that, but we are also going to get rid of the where chains and patterns that we previously needed, and only register the routes with their proper translations (others will throw 404 without having to check for it anymore):

/**
* Iterate over each language prefix 
*/
foreach( $all_langs as $prefix ){

   if ($prefix == 'pl') $prefix = '';

   /**
   * Register new route group with current prefix
   */
   Route::group(['prefix' => $prefix], function() use ($prefix) {

         // Now we need to make sure the default prefix points to default  lang folder.
         if ($prefix == '') $prefix = 'pl';

         /**
         * The following line will register:
         *
         * example.com/
         * example.com/en/
         */
         Route::get('/', '[email protected]')->name('home');

         /**
         * The following line will register:
         *
         * example.com/kontakt
         * example.com/en/contact
         */
         Route::get(Lang::get('routes.contact',[], $prefix) , '[email protected]')->name('contact');

         /**
         * “In another moment down went Alice after it, never once 
         * considering how in the world she was to get out again.”
         */
         Route::group(['prefix' => 'admin', 'middleware' => 'admin'], function () use ($prefix){

            /**
            * The following line will register:
            *
            * example.com/admin/uzivatelia
            * example.com/en/admin/users
            */
            Route::get(Lang::get('routes.admin.users',[], $prefix), '[email protected]')
            ->name('admin-users');

         });
   });
}

/**
* There might be routes that we want to exclude from our language setup.
* For example these pesky ajax routes! Well let's just move them out of the `foreach` loop.
* I will get back to this later.
*/
Route::group(['middleware' => 'ajax', 'prefix' => 'api'], function () {
    /**
    * This will only register example.com/api/login
    */
    Route::post('login', '[email protected]')->name('ajax-login');
});

Houston, we have a problem!

As you can see I prefer using named routes (most people do probably):

Route::get('/', '[email protected]')->name('home');

They can be very easily used inside your blade templates:

{{route('home')}}

But there is an issue with my solution so far: Route names override each other. The foreach loop above would only register the last prefixed routes with their names.

In other words only example.com/ would be bound to the home route as locale_perfix was the last item in the $all_langs array.

We can get around this by prefixing route names with the language $prefix. For example:

Route::get('/', '[email protected]')->name($prefix.'_home');

We will have to do this for each of the routes within our loop. This creates another small obstacle.


But my massive project is almost finished!

Well as you probably guessed you now have to go back to all of your files and prefix each route helper function call with the current locale_prefix loaded from the app config.

Except you don’t!

*** Laravel aside 2: ***

Let’s take a look at how Laravel implements it’s route helper method.

if (! function_exists('route')) {
    /**
     * Generate a URL to a named route.
     *
     * @param  string  $name
     * @param  array   $parameters
     * @param  bool    $absolute
     * @return string
     */
    function route($name, $parameters = [], $absolute = true)
    {
        return app('url')->route($name, $parameters, $absolute);
    }
}

As you can see Laravel will first check if a route function exists already. It will register its route function only if another one does not exist yet!

Which means we can get around our problem very easily without having to rewrite every single route call made so far in our Blade templates.

Let’s make a app/helpers.php file real quick.

Let’s make sure Laravel loads the file before it loads its helpers.php by putting the following line in bootstrap/autoload.php

//Put this line here
require __DIR__ . '/../app/helpers.php';
//Right before this original line
require __DIR__.'/../vendor/autoload.php';

All we now have to do is make our own route function within our app/helpers.php file. We will use the original implementation as the basis:

<?php
//Same parameters and a new $lang parameter
use Illuminate\Support\Str;

function route($name, $parameters = [], $absolute = true, $lang = null)
{
    /*
    * Remember the ajax routes we wanted to exclude from our lang system?
    * Check if the name provided to the function is the one you want to
    * exclude. If it is we will just use the original implementation.
    **/
    if (Str::contains($name, ['ajax', 'autocomplete'])){
        return app('url')->route($name, $parameters, $absolute);
    }

   //Check if $lang is valid and make a route to chosen lang
   if ( $lang && in_array($lang, config('app.alt_langs')) ){
       return app('url')->route($lang . '_' . $name, $parameters, $absolute);
   }

    /**
    * For all other routes get the current locale_prefix and prefix the name.
    */
    $locale_prefix = config('app.locale_prefix');
    if ($locale_prefix == '') $locale_prefix = 'pl';
    return app('url')->route($locale_prefix . '_' . $name, $parameters, $absolute);
}

That’s it!

So what we have done essentially is registered all of the prefix groups available. Created each route translated and with it’s name also prefixed. And then sort of overriden the Laravel route function to prefix all the route names (except some) with the current locale_prefix so that appropriate urls are created in our blade templates without having to type config('app.locale_prefix') every single time.

Oh yeah:

php artisan route:cache

Caching routes should only really be done once you deploy your project as it is likely you will mess with them during devlopement. But you can always clear the cache:

php artisan route:clear

Thanks again to Marcin Nabiałek for his original answer. It was really helpful to me.

Answer:

The same results can be applied with a simpler approach.. not perfect, but does offer a quick and easy solution. In that scenario, you do, however, have to write each routes so it might not do it for large websites.

Route::get('/contact-us', function () {
    return view('contactus');
})->name('rte_contact'); // DEFAULT

Route::get('/contactez-nous', function () {
    return view('contactus');
})->name('rte_contact_fr');

just define the route names in the localization file as so:

# app/resources/lang/en.json
{ "rte_contact": "rte_contact" } //DEFAULT

// app/resources/lang/fr.json
{ "rte_contact": "rte_contact_fr" }

You can then use them in your blade templates using generated locale variables like so:

<a class="nav-link" href="{{ route(__('rte_contact')) }}"> {{ __('nav_contact') }}</a>