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>
Suppose I have 3 (or more languages) on site. English, Italian, French. English being the default.
I want the homepage url to be:
mysite.com/ - for english
mysite.com/it - for italian
mysite.com/fr - for french
My route is currently
Route::get('/{locale}', 'HomeController#index')->name('home');
This works for French, Italian but obviously not for English being just mysite.com/
I dont want another route like
Route::get('/', 'HomeController#index')
Because then I wouldn be able to simply call home in any language as
{{ route('home', $locale) }}
Whats the best solution?
One of my old solutions but still should work:
In the beginning of routes.php
$locale = Request::segment(1);
if(in_array($locale, ['en','fr','it'])){
app()->setLocale($locale);
}else{
app()->setLocale('en');
$locale = '';
}
Then
Route::group([
'prefix' => $locale
], function(){
Route::get('/demo', 'TestController#demo')->name('demo');
...
})
Important: You should always use named routes in this scenario. The generated URLs will then be for the current locale:
route('demo') will return /demo when app locale is english, and /it/demo when app locale is italian!
Laravel allows optional parameters in route definition, so you can set local route parameter as optional for your usage :
Route::get('/{locale?}', 'HomeController#index')->name('home');
And in your HomeController, check the parameter to know if locale is present
public function index(Request $request, $locale = null) {
if (empty($locale)) {
$locale = 'en'; // english by default
}
...
}
Hope it's helps you :)
Laravel 5.5
Using a group in RouteServiceProvider for {domain}
I want to be able to call Named Routes in blade without having to pass
public function pageName($domain){
return view('mypage', ['domain'=>$domain,'othervars'=>$domain)])
}
and avoid this mess in blade:
{{ route('nameOfRoute', ['domain'=>$domain]) }}
Instead i would love to simply in my route group set the route(['domain']) property to be $domain and be done with it.
No global way to set it, but instead, wrap the route('name') default helper function with my own helper function, and add the param to the array.
Example function:
function orgRoute($route, $params = [])
{
if (!is_array($params)){
$params = [$params];
}
// Set the domain value if not set, null, or jibberish
if (!isset($params['domain']) || $params['domain']=='{domain}' || $params['domain'] == '')
{
// Instance of App Domain is set in OrgBaseController __construct()
$domain = \App::make('current_domain');
$params['domain'] = $domain;
}
return route($route, $params);
}
I have my Laravel app running from a subdirectory, so in order to be able to use the URL helper like this: {{ url('users') }} instead of like this {{ url('subdirectory/users') }} I use the forceRouteUrl() method in my AppServiceProvider.php file. It looks like this:
public function boot()
{
URL::forceRootUrl(Config::get('app.url'));
...
And my .env: APP_URL=http://app.dev/subdirectory
So far so good, except when returning routes. For example I have a sorting method that does this:
function sort_schedule($column, $order)
{
return route('schedule', [
'sortBy' => $column,
'sortOrder' => $order,
]
);
}
But the link it generates is this: http://app.dev/subdirectory/subdirectory/...
The same thing happens when I use Kyslik's Column Sortable package.
How can I fix this?
It seems that while other helpers like url() or asset() don't have this issue, the route() helper tends to duplicate the subpath when laravel isn't accessible directly from the site root and forceRootUrl() is used.
I'm using laravel 5.4 but this problem is present on previous versions too.
To solve this issue i found three possible solutions:
Use url() instead of route() and manually create your urls
Everytime you use route() remember to pass false as the third parameter, it will use relative paths that don't have this issue
Override the default route() helper with a custom one
I opted for the third option and created a CustomHelper.php file that contains my new route() helper.
Here are the instructions on how to override default helpers:
https://laracasts.com/discuss/channels/general-discussion/override-functions-in-supporthelpersphp
Inside my CustomHelper.php file I added this
function route($name, $parameters = [], $absolute = true)
{
$appUrl = config('app.url'); // in your case: http://app.dev
$appUrlSuffix = config('app.url_suffix'); // in your case: subdirectory
// Additional check, do the workaround only when a suffix is present and only when urls are absolute
if ($appUrlSuffix && $absolute) {
// Add the relative path to the app root url
$relativePath = app('url')->route($name, $parameters, false);
$url = $appUrl.$relativePath;
} else {
// This is the default behavior of route() you can find in laravel\vendor\laravel\framework\src\Illuminate\Foundation\helpers.php
$url = app('url')->route($name, $parameters, $absolute);
}
return $url;
}
The app.url_suffix is a custom variable that i defined in config/app.php
'url_suffix' => env('APP_URL_SUFFIX', null),
and in the .env config file
APP_URL_SUFFIX=subdirectory
I am making a bilingual app. I am using same routes for each but I am using different views for both languages. Whenever I want to redirect to a route I want to pass {{ route('test.route', 'en') }}. Where I am passing en, I want to fetch the current locale value from the view and then pass it to the route. Please help.
try this. It will give the locale set in your application
Config::get('app.locale')
Edit:
To use this in blade, use like the following, to echo your current locale in blade.
{{ Config::get('app.locale') }}
If you want to do a if condition in blade around it, it will become,
#if ( Config::get('app.locale') == 'en')
{{ 'Current Language is English' }}
#elseif ( Config::get('app.locale') == 'ru' )
{{ 'Current Language is Russian' }}
#endif
To get current locale,
app()->getLocale()
At first create a locale route and controller :
Route::get('/locale/{lang}', 'LocaleController#setLocale')->name('locale');
class LocaleController extends Controller
{
public function setLocale($locale)
{
if (array_key_exists($locale, Config::get('languages')))
{
Session::put('app_locale', $locale);
}
return redirect()->back();
}
}
Now you can check easily in every page:
$locale = app()->getLocale();
$version = $locale == 'en' ? $locale . 'English' : 'Bangla';