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.