DEV Community

Cover image for A solid way to add multi-language support to your Laravel app
boolfalse
boolfalse

Posted on

A solid way to add multi-language support to your Laravel app

Using custom middleware to intercept each request and validate URI prefix as a current app locale.

There are often cases when devs need to solve the same problem within different projects.
In the course of my many years of experience, one of those problems has been a localization in Laravel projects. For various projects, I have used some method that I have prepared according to the requirements.

You might think, then why didn’t I make a separate package to solve that problem?
Well, of course, it would be possible to create a package, but usually in the case of different projects, there are so many differences to each other, especially in the front-end part, that making a separate package for only one project becomes ineffective.

So, now I will share the old-but-gold way I have used in many of my Laravel projects, and I hope it may help you.

If you just want to check the ready project now, I will put here the ⭐ GitHub repo for the project we about to build.

For getting well whole lifecycle, let’s install a fresh Laravel app.
You can integrate this method in your existing Laravel project as well.

composer create-project laravel/laravel localization && cd localization
Enter fullscreen mode Exit fullscreen mode

In latest Laravel versions .env file will be created and setup automatically.

Now we have a default Laravel app installed on our machine, so we can quickly test that via built-in opportunity:

php artisan serve
Enter fullscreen mode Exit fullscreen mode

And we can see the result by opening http://localhost:8000 (by default, 8000 is the port for serving a Laravel app).

Often Laravel app has some routes for customers and some separated protected routes for admins/managers. For the second case we would have to have some URI prefix only for admins/managers, for example: “dashboard”.
Let’s assume we need to develop something like we described. For instance, we have

  • 3 views/pages/routes for customers: “home”, “about”, “contact”
  • and an admin view/page/route: “dashboard”

Our routes/web.php would like this:

<?php

use Illuminate\Support\Facades\Route;

Route::group([
    'prefix' => '{locale?}',
], function () {
    Route::get('/', function () { return view('pages.home'); })->name('home');
    Route::get('/about', function () { return view('pages.about'); })->name('about');
    Route::get('/contact', function () { return view('pages.contact'); })->name('contact');
});

Route::group([
    'prefix' => '{locale?}/dashboard',
], function () {
    Route::get('/', function () { return view('pages.dashboard'); })->name('dashboard');
});
Enter fullscreen mode Exit fullscreen mode

Setup appropriate blade views for testing.

  • overwrite resources/views/welcome.blade.php:
<!DOCTYPE html>
<html lang="{{ config('localization.locale_lang.' . app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel</title>

    <link href="{{ asset('styles.css') }}" rel="stylesheet">
</head>
<body>

<nav>
    <div class="centered-text">
        <h1>Laravel Localization</h1>
        <p>{{ __('page.hello_world') }}</p>
    </div>
    <ul class="drop-down closed">
        <li class="cursor-pointer">
            <p class="nav-button">Change Language</p>
        </li>
        @foreach(config('localization.locales') as $locale)
            <li data-locale="{{ $locale }}" class="ss-change-locale">
                <a href="javascript:void(0)">
                    {{ __('page.locales.name.' . $locale) }}
                </a>
            </li>
        @endforeach
    </ul>
    <div class="page-content">
        @yield('content')
    </div>
</nav>

<script src="{{ asset('scripts.js') }}"></script>
<script>
    // SWITCH LANGUAGE
    var currentUri = window.location.pathname;
    var locales = "{{ implode('|', config('localization.locales')) }}";
    var currentLocale = "{{ app()->getLocale() }}";
    var elements = document.querySelectorAll('.ss-change-locale');
    for (let i = 0; i < elements.length; i++) {
        elements[i].addEventListener('click', function () {
            let locale = this.getAttribute('data-locale');
            if (locale === currentLocale) {
                return;
            }
            let newUri = currentUri.replace(new RegExp('^\/(' + locales + ')'), '/' + locale);
            let paramsIndex = window.location.href.indexOf('?');
            let params = (paramsIndex !== -1 ? window.location.href.slice(paramsIndex) : '')
            let port = window.location.port ? ':' + window.location.port : '';
            window.location.href = window.location.protocol + '//' + window.location.hostname + port + newUri + params;
        });
    }
</script>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

As you can see, we’re iterating over all the languages (that we have setup in our config/localization.php configuration file), and list them with LI tags. And at the bottom some JavaScript snippet, which is responsible for picking up the language selected by the user and redirecting to the appropriate page with the appropriate language prefix.
You can easily modify the code snippet to suit your needs as it is just a way to show how it could be used.

Then create pages directory in the resources/views directory to have some partials.

  • resources/views/pages/home.blade.php default component for Home:
@extends('welcome')
@section('content')
    <p class="centered-text">{{ __('page.home') }}</p>
    <div class="centered-text">
        <a href="{{ route('about') }}" class="button">{{ __('page.about') }}</a>
        <a href="{{ route('contact') }}" class="button">{{ __('page.contact') }}</a>
        <a href="{{ route('dashboard') }}" class="button">{{ __('page.dashboard') }}</a>
    </div>
@endsection
Enter fullscreen mode Exit fullscreen mode
  • resources/views/pages/about.blade.php component for About:
@extends('welcome')
@section('content')
    <p class="centered-text">{{ __('page.about') }}</p>
    <div class="centered-text">
        <a href="{{ route('home') }}" class="button">{{ __('page.home') }}</a>
        <a href="{{ route('contact') }}" class="button">{{ __('page.contact') }}</a>
        <a href="{{ route('dashboard') }}" class="button">{{ __('page.dashboard') }}</a>
    </div>
@endsection
Enter fullscreen mode Exit fullscreen mode
  • resources/views/pages/contact.blade.php component for Contact:
@extends('welcome')
@section('content')
    <p class="centered-text">{{ __('page.contact') }}</p>
    <div class="centered-text">
        <a href="{{ route('home') }}" class="button">{{ __('page.home') }}</a>
        <a href="{{ route('about') }}" class="button">{{ __('page.about') }}</a>
        <a href="{{ route('dashboard') }}" class="button">{{ __('page.dashboard') }}</a>
    </div>
@endsection
Enter fullscreen mode Exit fullscreen mode
  • resources/views/pages/dashboard.blade.php one more component for Dashboard. Later you can add some blades like this:
@extends('welcome')
@section('content')
    <p class="centered-text">{{ __('page.dashboard') }}</p>
    <div class="centered-text">
        <a href="{{ route('home') }}" class="button">{{ __('page.home') }}</a>
        <a href="{{ route('about') }}" class="button">{{ __('page.about') }}</a>
        <a href="{{ route('contact') }}" class="button">{{ __('page.contact') }}</a>
    </div>
@endsection
Enter fullscreen mode Exit fullscreen mode

Now, for having a little bit of UI, we can use some ready styles from CodePen.
I will modify that assets a bit, so you can get those styles.css and scripts.js files from the public folder here.

At this point, as we have all the routes and views setup let’s create a config file specifically for the localizations-related stuff, create a custom middleware, register that middleware in the kernel to intercept all the web requests, and add some translations.

  • Create app/Http/Middleware/Localization.php:
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;

class Localization
{
    public function handle(Request $request, Closure $next)
    {
        $first_segment = $request->segment(1);
        if (in_array($first_segment, config('localization.locales'))) {
            App::setLocale($first_segment);
            URL::defaults(['locale' => $first_segment]);

            return $next($request);
        } else {
            $fallback_locale = config('app.fallback_locale');
            $segments = $request->segments();
            array_unshift($segments, $fallback_locale);

            return Redirect::to(implode('/', $segments));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This custom middleware, called Localization, is responsible for handling and setting the application’s locale (language) based on the first segment of the URL.

  1. It extracts the first segment from the URL. For example, “en” in “example.com/en/page”.
  2. It checks if this first segment corresponds to a supported locale as defined in the configuration config('localization.locales').
  3. If the first segment is a valid locale, it sets the application’s locale using App::setLocale($first_segment), ensuring that the rest of the application operates in that language.
  4. It also updates the URL to include the selected locale as a default for future generated URLs using URL::defaults(['locale' => $first_segment]).
  5. If the first segment is not a valid locale, it redirects the user to the same URL but with the default (fallback) locale by prepending the fallback locale to the URL segments, as defined in config/app.php fallback_locale field. Of course, don’t forget to clear caches after each configuration change, running php artisan optimize.

Shortly, this custom middleware determines the application’s current language (locale) based on the first URL segment, setting it to a valid locale or redirecting to the default locale if the segment is not recognized.

So if we want to change default locale (language) in our app, we need to change the fallback locale in config/app.php:
'fallback_locale' => 'en' to 'fallback_locale' => ''.

Register the middleware in the web middleware group by adding the Localization class as a last array element of “web” sub-array of $middlewareGroups array:

protected $middlewareGroups = [
    'web' => [
        // EXISTING ONES
        \App\Http\Middleware\Localization::class, // APPEND THIS HERE
    ],
    'api' => [
        // EXISTING ONES
    ],
];
Enter fullscreen mode Exit fullscreen mode

The reason we did that only just for web middleware group is that we don’t want to affect other existing routes, such as API endpoints which could be presented in our app.

Now, for having dynamic code in our app, we need to have some configuration file, where we can save all our locales (languages). We will create some config/localization.php specifically for the localizations-related stuff:

<?php

return [
    'locales' => [
        'en',
        'cn',
    ],
    'locale_lang' => [
        'en' => 'en_US',
        'cn' => 'zh_CN',
    ],
];
Enter fullscreen mode Exit fullscreen mode

As you may already notice, in our app we will have English as a default (fallback) language, and Chinese as a secondary language. And we just use “locales” field in our dropdown HTML, to iterate and list all the locales.

Let’s create some translations as well, which we can use in our application.

  • Create lang/en/page.php (for Laravel 9 and old versions create resources/lang/en/page.php):
<?php

return [
    'locales' => [
        'code' => [
            'en' => "EN",
            'cn' => "中国人",
        ],
        'name' => [
            'en' => "English",
            'cn' => "中国人",
        ],
    ],

    'hello_world' => "Hello World",
    'change_language' => "Change Language",

    'home' => "Home",
    'about' => "About",
    'contact' => "Contact",
    'dashboard' => "Dashboard",
];
Enter fullscreen mode Exit fullscreen mode
  • Create lang/cn/page.php (for Laravel 9 and old versions create resources/lang/cn/page.php).
<?php

return [
    'locales' => [
        'code' => [
            'en' => "中文",
            'cn' => "CN",
        ],
        'name' => [
            'en' => "English",
            'cn' => "中文",
        ],
    ],

    'hello_world' => "你好世界",
    'change_language' => "更改语言",

    'home' => "家",
    'about' => "关于",
    'contact' => "联系",
    'dashboard' => "仪表板",
];
Enter fullscreen mode Exit fullscreen mode

At this point we have all the necessary things setup to have our app working. Just last time clear the caches and run the server:

# make sure always to refresh all the caches after each config file changes
php artisan optimize

# quick way to run and test our app
php artisan serve
Enter fullscreen mode Exit fullscreen mode

We can now check out our app in the browser on http://localhost:8000

Make sure you have disabled automatic translation extensions (like Google Translate extension as in the picture):

That’s all for the setup.
Later, if you want to add some more translations, the one thing you just need to, to add your stuff in the translation files located in the lang directory as we have done already.

Sometimes things happen, when we need to add a completely new language to our existing app that will be supported as well as other languages.
The good part of this article is that we have developed a way to dynamically add new locales/languages.
In our test case we will add Armenian as a third language.

The only two steps you need to do to add a new language, that are:

  • Add a language code in the localization-specific configuration file (config/localization.php in our case):
<?php

return [
    'locales' => [
        'en',
        'cn',
        'am', // NEW LANGUAGE
    ],
    'locale_lang' => [ // OPTIONAL STUFF
        'en' => 'en_US',
        'cn' => 'zh_CN',
        'am' => 'hy_AM', // NEW LANGUAGE
    ],
];
Enter fullscreen mode Exit fullscreen mode
  • Add the appropriate lang file like we did before. In our case we will create lang/am/page.php (for Laravel 9 and old versions create resources/lang/am/page.php)
<?php

return [
    'locales' => [
        'code' => [
            'en' => "中文",
            'cn' => "CN",
            'am' => "ՀԱՅ",
        ],
        'name' => [
            'en' => "English",
            'cn' => "中文",
            'am' => "Հայերեն",
        ],
    ],

    'hello_world' => "Բարև աշխարհ",
    'change_language' => "Փոխել լեզուն",

    'home' => "Տուն",
    'about' => "Մասին",
    'contact' => "Կապ",
    'dashboard' => "Վահանակ",
];
Enter fullscreen mode Exit fullscreen mode

You optionally may modify your existing lang files as I need to do in my case as you can notice in the GitHub repo, but it depends on your case.
In my case I will do some small additions in ****/en/page.php* and in ****/cn/page.php*:

<?php

return [
    'locales' => [
        'code' => [
            // EXISTING CODE
            'am' => "ՀԱՅ", // ADDING THIS
        ],
        'name' => [
            // EXISTING CODE
            'am' => "Հայերեն", // ADDING THIS
        ],
    ],
    // EXISTING CODE
];
Enter fullscreen mode Exit fullscreen mode

Finally… again, don’t forget to clear the caches by the artisan command.

That’s it.

Now we have integrated a custom and solid way to have Laravel multi-language support, as well as the way to add new languages dynamically.

In the end, I will put here the GitHub repo ⭐ for the project we built.


If you liked this article, feel free to follow me here. 😇

To explore projects working with various modern technologies, you can follow me on GitHub, where I actively publicize much of my work.

For more info you can visit my website boolfalse.com

Thank you !!!

Top comments (0)