How to create multilingual translated routes in Laravel
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:
- I have one default language
- I can have many other languages
- Current language should be calculated only by URL (without cookies/sessions) to make it really friendly also for search engines
- For default language there should be no prefix in URL, for other languages should be language prefix after domain
- 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?
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);
});
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('/', 'MainController@getHome')->name('home');
/**
* The following line will register:
*
* example.com/kontakt
* example.com/en/contact
*/
Route::get(Lang::get('routes.contact',[], $prefix) , 'MainController@getContact')->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), 'AdminController@getUsers')
->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', 'AjaxController@login')->name('ajax-login');
});
Houston, we have a problem!
As you can see I prefer using named routes (most people do probably):
Route::get('/', 'MainController@getHome')->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('/', 'MainController@getHome')->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';
UPDATE FOR LARAVEL 7+
The bootstrap/autoload.php
file doesn't exist anymore, you will have to add the code above in the public/index.php
file instead.
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.
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>