Courses

Multi-Language Laravel 11: All You Need to Know

Language Switcher from URL

Summary of this lesson:
- Configure available locales in app configuration
- Create SetLocale middleware to handle language switching
- Modify routes to include locale segment
- Add language selector to navigation views
- Handle redirects and authentication with locales

Adding a language selector is crucial to any multilingual application. In this practical example, we'll add a language selector to our navigation bar:

In this first example, we will define the language from the URL segment, like /en/about or /es/register.


Setup

Here's our plan to set up a language selector:

  • Configuration: add a language list to the config - this will be used to display the language selector
  • Middleware: Add a Middleware to handle language change and URL redirection
  • Routes: Modify Routes to use the Middleware
  • Views: Add a language selector to our Views
  • Redirects: Modify our redirects for authentication

Let's start with the configuration:

config/app.php

// ...
'locale' => 'en', // <-- Locate this line and add `available_locales` below it
 
'available_locales' => [
'en',
'es',
],
// ...

Next, we can create our middleware:

php artisan make:middleware SetLocale

app/Http/Middleware/SetLocale.php

use URL;
use Carbon\Carbon;
 
// ...
 
public function handle(Request $request, Closure $next): Response
{
app()->setLocale($request->segment(1)); // <-- Set the application locale
Carbon::setLocale($request->segment(1)); // <-- Set the Carbon locale
 
URL::defaults(['locale' => $request->segment(1)]); // <-- Set the URL defaults
// (for named routes we won't have to specify the locale each time!)
 
return $next($request);
}

Now we need to modify Routes to use the new Middleware and add the locale segment to the URL:

routes/web.php

Route::get('/', function () {
return redirect(app()->getLocale()); // <-- Handles redirect with no locale to the current locale
});
 
Route::prefix('{locale}') // <-- Add the locale segment to the URL
->where(['locale' => '[a-zA-Z]{2}']) // <-- Add a regex to validate the locale
->middleware(\App\Http\Middleware\SetLocale::class) // <-- Add the middleware
->group(function () {
Route::get('/', function () {
return view('welcome');
});
 
Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
 
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
 
// ...
});
 
require __DIR__ . '/auth.php';
});

Now we can add the language selector to our views:

resources/views/layouts/navigation.blade.php

{{-- ... --}}
@foreach(config('app.available_locales') as $locale)
<x-nav-link
:href="route(\Illuminate\Support\Facades\Route::currentRouteName(), array_merge(Route::current()->parameters(),['locale' => $locale]))"
:active="app()->getLocale() == $locale">
{{ strtoupper($locale) }}
</x-nav-link>
@endforeach
<x-dropdown align="right" width="48">
{{-- ... --}}

And fix one issue with our Dashboard link:

resources/views/welcome.blade.php

Replace: {{ url('/dashboard') }} with {{ route('dashboard') }}

Finally, we need to modify our redirects for authentication:

Redirect after login:

app/Http/Controllers/Auth/AuthenticatedSessionController.php

public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
 
$request->session()->regenerate();
 
return redirect()->intended(route('dashboard', ['locale' => app()->getLocale()]));
}

Redirect after the user session expired:

bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__ . '/../routes/web.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->redirectGuestsTo(fn() => route('login', ['locale' => app()->getLocale()]));
})

Lastly, there is an issue mentioned where registrations do not redirect correctly. For this, we need to modify the RegisteredUserController:

app/Http/Controllers/Auth/RegisteredUserController.php

// ...
 
public function store(Request $request): RedirectResponse
{
// ...
 
return redirect(route('dashboard', absolute: false));
return redirect(route('dashboard', ['locale' => app()->getLocale()], absolute: false));
}

That is it, we are done! Once the page is loaded - we should see that we were redirected from / to /en and once the user logs in - we should be redirected to /en/dashboard:

And switching between languages should work as expected:


Repository: https://github.com/LaravelDaily/laravel11-localization-course/tree/lesson/ui-switching

Previous: Localizing Dates and Currencies
avatar

In app/Http/Controllers/RegisteredUserController

after user register should redirect to right locale

Should change this:

return redirect(RouteServiceProvider::HOME);

To:

return redirect(app()->getLocale() .RouteServiceProvider::HOME);

avatar

Sorry for late reply, just checked and it indeed sometimes causes this issue. Updating the article!

avatar
You can use Markdown
avatar

While the url works, I find it not working for routes like blog detail where I pass a dynamic slug. I keep having the error where it says route paramter for {slug} not provided. To solve this problem, you can create a helper function in app dir called helpers.php and add in this function:

function getLocaleUrlPath(string $locale)
{
    $currentPath = URL::full();
    return Str::replace('/'. strtolower(app()->getLocale()), '/' . $locale, $currentPath);
}

And in my navigation links with pure blade, I am able to use this:

<a href="{{URL(getLocaleUrlpath($locale))}}" class="login-button"> {{ strtoupper($locale) }} </a>

If there are any issues then let me know.

avatar

Hm, not sure I understand from where the issue comes. Could you add a bit more details? Would love to update the lesson if there's an issue!

avatar

From the language switcher, if you're on the route for example https://localhost:8000/en/blog/first-post and you click on the language switcher, it will break the app because the only param that is supplied is the $locale whereas we also need to pass the slug of the post to reload that current route.

avatar

Hmm, could you try to use this:

route(\Illuminate\Support\Facades\Route::currentRouteName(), array_merge(Route::current()->parameters(),['locale' => $locale]))

in your resources/views/layouts/navigation.blade.php language selection and let me know if the problem persists?

avatar

It works now like magic!!!

avatar

Awesome to hear! Updating the article, thank you!

avatar

Thank you too. Now I got to learn about the Route::current()->parameters() function. Pretty neat!

avatar
You can use Markdown
avatar

I'm having problems with this technique. The default language of my application is Brazilian Portuguese (pt_BR) and when I apply this technique it would only have to be pt and when I call the base URL the system calls it as pt_BR and gives an 404 error. How to resolve this issue?

avatar

Hi, can you add what error you have? Or where does the 404 come from?

Or better yet, add the configuration changes you made for pt_BR and I'll try to reproduce it and help you out

avatar

It gives 404 because the system transforms the URL with /pt_BR since the correct configuration for Portuguese (Brazil) is this and not pt

avatar

I change locate to pt_BR in app.php in config folder

avatar

ops locale = 'pt_BR', sorry. Thanks

avatar

Okay, got it. Looked into the code and it is because of the Route::prefix() in your web.php file.

To fix your issue, you can use a different regex (by default we expect 2 characters only):

Route::prefix('{locale}')
    ->where(['locale' => '[a-zA-Z]{2}_[a-zA-Z]{2}'])

This should allow you to use pt_BR as a locale

avatar
You can use Markdown
avatar

Hello, I'm using "laravel/breeze": "^1.24" and I'm facing the problem that breeze no longer publishes auth controllers to the app directory

app/Http/Controllers/Auth/RegisteredUserController.php

rather it is now located in the vendor

vendor/laravel/breeze/stubs/default/app/Http/Controllers/Auth/RegisteredUserController.php

Is there a way I can have this back int the app directory? so I can modify the return redirect with the app()->getLocale:

return redirect(app()->getLocale() . RouteServiceProvider::HOME);

If I modify it in vendor it might be overwritten if I run composer update

avatar

Did you run php artisan breeze:install? Because that file that's located in the vendor is a stub (template) file and should not be used/modified.

ps. Never modify vendor files!

avatar

Thx for your reply.

Yes I did, seems to be a Livewire thing. Seems the authentication is implemented differently with the Breeze Livewire Stack. For now I was not able to make it completly multilingual.

I 've created two new projects for testing. During the process of php artisan breeze:install the installer asks Which Breeze stack would you like to install?

It works if you choose

  • Blade with Alpine

Mutiple Controllers including RegisteredUserController.php are in the app/Http/Controllers/Auth/-Directory

If you choose

  • Livewire (Volt Class API) with Alpine
  • Livewire (Volt Functional API) with Alpine

There is only VerifyEmailController.php in the app/Http/Controllers/Auth/-Directory

avatar

Oh... Now that makes sense!

We have not looked into the Livewire stack yet, as that stack uses Volt API. These files are in different locations (can't tell where, except for functional API is in... views...).

avatar

Maybe this video would help you:

https://www.youtube.com/watch?v=nzD0Uy7VMPE

avatar

With the livewire stack the auth stuff is implemented in resources/views/livewire/pages/auth/ instead.

In the file login.blade.php for example there is the redirect:

$this->redirect(
        session('url.intended', RouteServiceProvider::HOME),
        navigate: true
    );

So the locale needs to be prepended there.

$this->redirect(
        session('url.intended', app()->getLocale() . RouteServiceProvider::HOME),
        navigate: true
    );

Thank you for pointing me in the right direction!

avatar

Glad it helped! And thanks for the example.

avatar
You can use Markdown
avatar

Hi

I really like the simplicity in this solution, but I'm facing an issue.

I cannot figure if it is a livewire issue or an issue with the solution. I followed the above and it worked quit well until I accessede a page wher I have a Livewire table component - nothing fancy just a simple pagination compnent and it has some actions for each row. The action calls a normal controller function, but all the sudden my model binding stopped working.

The error I'm getting is like this:

App\Http\Controllers\UserDocTemplateController::setupTemplateFields(): Argument #1 ($userDocTemplate) must be of type App\Models\UserDocTemplate, string given, called in /Users/rabol/code/web/sign/vendor/laravel/framework/src/Illuminate/Routing/Controller.php on line 54

another error I have seen is is like this:

Missing required parameter for [Route: unsubscribe] [URI: {locale}/unsubscribe/{id?}/{type?}] [Missing parameter: locale].

this is the code:

$listUnsubscribeHeader = $listUnsubscribeHeader.'<'.route('unsubscribe', ['id' => '', 'type' => $type]).'>';

as mentioned I don't know if it's related to Livewire or something else

avatar

Take a look at url example for create button - all routes have to have a parent parameter passed. otherwise this will be the issue

avatar

aha so I always need to add the locale something like this.

return redirect()->route('my-route', ['locale' => app()->getLocale()]);

avatar

yep! it does need parent always

avatar

hmm

this causes error

route('users.documents.tracking',['locale' => app()->getLocale(),'userDoc' => $userDoc->id])

App\Http\Controllers\UserDocController::tracking(): Argument #1 ($userDoc) must be of type App\Models\UserDoc, string given, called in /Users/rabol/code/web/sign/vendor/laravel/framework/src/Illuminate/Routing/Controller.php on line 54

the language is passed not the model

looks like all routes that have a parameter like this route_name/{varName} have a issue

avatar

Not sure what could be there. And it's not so easy to give a potential solution wihout diving deep in the code and debugging, sorry

avatar

if you have a controller method like this:

public fuction show(Post $post)

then you could have a route like this:

Route:get(/{post}/show, [PostController::class, 'show'])->name('show')

With the solution from this lesson you will have a problem as all routes is prefixed with 'locale' so you rcontroller method actually needs to look like this:

public fuction show(string $locale, Post $post)

avatar
You can use Markdown
avatar

Try this:

git clone git@github.com:LaravelDaily/laravel-localization-course.git demo

cd demo

composer install

copy .env.example .env

modify .env

php artisan key:generate

create a db

php artisan make:model Post --all

modify the post migration

php artisan migrate

update the post factory

create a some postes via tinker

Follow the lesson

add this to your routes/web.php

Route::get('/posts/{post}/show', [PostController::class,'show'])->name('post.show');

then open your browser (I use laravel Valet)

http://demo.test/en/posts/1/show

then you can see the error

App\Http\Controllers\PostController::show(): Argument #1 ($post) must be of type App\Models\Post, string given, called in /Users/rabol/code/web/demo/vendor/laravel/framework/src/Illuminate/Routing/Controller.php on line 54

avatar

Yes, you are correct about this. You do have to always include this:

public function show(string $locale, Post $post)
    {
        dd($post);
    }
		```
		
		Otherwise it will indeed throw the error you are mentioning. This is because the first parameter in our URL is the locale and not the model. That's how Laravel model binding works
avatar
You can use Markdown
avatar

Could you please tell me how to make the 'en' locale be used by default without adding it to the url, while the other locales are already added?

👍 2
avatar

To do this efficiently, I would recommend you to use database/cookie based token for it.

The problem is - our language key is the FIRST parameter on the URL and removing it, can cause the page to think that /page/post needs to load page as our language.

Of course, you can play around and cover the URL defaults/parameter checks, but that's a bigger topic than I can fit in a comment, sorry

avatar

Thank you

avatar
You can use Markdown
avatar
You can use Markdown