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
In app/Http/Controllers/RegisteredUserController
after user register should redirect to right locale
Should change this:
To:
Sorry for late reply, just checked and it indeed sometimes causes this issue. Updating the article!
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 calledhelpers.php
and add in this function:And in my navigation links with pure blade, I am able to use this:
If there are any issues then let me know.
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!
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 theslug
of the post to reload that current route.Hmm, could you try to use this:
in your
resources/views/layouts/navigation.blade.php
language selection and let me know if the problem persists?It works now like magic!!!
Awesome to hear! Updating the article, thank you!
Thank you too. Now I got to learn about the
Route::current()->parameters()
function. Pretty neat!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?
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
It gives 404 because the system transforms the URL with /pt_BR since the correct configuration for Portuguese (Brazil) is this and not pt
I change locate to pt_BR in app.php in config folder
ops locale = 'pt_BR', sorry. Thanks
Okay, got it. Looked into the code and it is because of the
Route::prefix()
in yourweb.php
file.To fix your issue, you can use a different regex (by default we expect 2 characters only):
This should allow you to use pt_BR as a locale
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
rather it is now located in the vendor
Is there a way I can have this back int the app directory? so I can modify the return redirect with the app()->getLocale:
If I modify it in vendor it might be overwritten if I run composer update
Did you run
php artisan breeze:install
? Because that file that's located in the vendor is astub
(template) file and should not be used/modified.ps. Never modify vendor files!
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 asksWhich Breeze stack would you like to install?
It works if you choose
Mutiple Controllers including
RegisteredUserController.php
are in theapp/Http/Controllers/Auth/
-DirectoryIf you choose
There is only
VerifyEmailController.php
in theapp/Http/Controllers/Auth/
-DirectoryOh... 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...).
Maybe this video would help you:
https://www.youtube.com/watch?v=nzD0Uy7VMPE
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:So the locale needs to be prepended there.
Thank you for pointing me in the right direction!
Glad it helped! And thanks for the example.
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
Take a look at url example for create button - all routes have to have a parent parameter passed. otherwise this will be the issue
aha so I always need to add the locale something like this.
return redirect()->route('my-route', ['locale' => app()->getLocale()]);
yep! it does need parent always
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
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
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)
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
Yes, you are correct about this. You do have to always include this:
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?
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 loadpage
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
Thank you