Easy add localization to your Laravel app

Posted on March 5, 2018

When I started to build this website, I wanted to be able to write blog posts in English, but also in French, my native language. My website had to be multilingual.

There are several packages out there to handle localization in your app. The most well-known is probably mcamara/laravel-localization. I used it at first, but then I had several troubles with it. One of the most critical to me is the impossibility to unit test localized routes.

On another hand, Laravel already provides some built-in features to deal with localization. So, the remanining part was to build a dynamic system to change locale depending on the route:

GET /en/about # Displays the about page in english
GET /fr/a-propos # Displays the about page in french

Let's review how I implemented this route-based localization mecanism.

The Laravel basics

The Laravel documentation has a dedicated page for localization. Here are the main features.

Locale

First, in config/app.php, we can set the application locale:

/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by the translation service provider. You are free to set this value
| to any of the locales which will be supported by the application.
|
*/

'locale' => 'en',

This locale can be changed at runtime:

app()->setLocale('fr');

// And we can get it
app()->getLocale(); // 'fr'
app()->isLocale('fr'); // true

Translations

There are also some translation features.

We can define strings in resources/lang:

// resources/lang/en/general.php
return [
    'about' => 'About'
];

// resources/lang/fr/general.php
return [
    'about' => 'Γ€ propos'
];

Then we can get them, based on the locale value:

app()->setLocale('en');
trans('general.about') // About
trans('general.about', 'fr') // Γ€ propos

app()->setLocale('fr');
trans('general.about') // Γ€ propos

Build the dynamic localization system

Based on these features, we're now able to build our logic.

Dynamicaly set the locale value

We want to define the locale depending on the current URL:

  • /en/about should set the locale to en
  • /fr/about should set the locale to fr
  • ...

A good way to do this is using a middleware:

// app/Http/Middlewares/SetLocale.php

public function handle(Request $request, Closure $next): Response
{
    $locale = $request->segment(1, ''); // `en` or `fr`

    app()->setLocale($locale);

    return $next($request);
}

Define the routes

Now, we can write our routes:

// routes/web.php

Route::get('/en/about', function() {
    return trans('general.about');
});

Route::get('/fr/about', function() {
    return trans('general.about');
});

We can easily refactor this:

// routes/web.php

foreach(['en', 'fr'] as $locale) {

    Route::prefix($locale)->group(function() {
        Route::get('about', function() {
            return trans('general.about');
        });
    })

}

Localize the URL

Now, it would be nice to localize the URL, ie to use /en/about, fr/a-propos, etc. It's more user friendly, and by extension more SEO friendly. Let's add some logic to our route file:

// routes/web.php

// We're storing the current locale temporarily
$currentLocale = app()->getLocale();

foreach(['en', 'fr'] as $locale) {

    app()->setLocale($locale);
    
    Route::prefix($locale)->group(function() {
        Route::get(trans('routes.about'), function() {
            //
        });
    })

}

// We restore the locale
app()->setLocale($currentLocale); 

Then, add our route translations in resources/lang:

// resources/lang/en/routes.php
return [
    'about' => 'about'
];

// resources/lang/fr/routes.php
return [
    'about' => 'a-propos'
];

Now, we've got:

  • /en/about should set the locale to en
  • /fr/a-propos should set the locale to fr

Neat.

Some improvements

The above is the skeleton for our route-based localization system. We can easily think of improvements.

In your app, you'll probably want to build a navigation. These links will be dynamic, depending on the current locale.

Maybe we could have this helper:

localeRoute('about'); // '/en/about'
app()->setLocale('fr');
localeRoute('about'); // '/fr/a-propos'

Another common need will be to switch the locale from the current page. This could be another method:

// Current URL is '/en/about'
switchRoute(); // '/fr/a-propos'

Depending on your needs, you may also want to hide the default locale in the URL:

  • /about should set the locale to en
  • /fr/a-propos should set the locale to fr
  • ...

And so on. I think we got enough to write a dedicated package πŸ˜‰

Introducing alexjoffroy/laravel-localization

We saw how to build a basic route-based localization system in Laravel. As I wrote it for this blog, I thought it could be a good idea to extract it in a package to reuse in my projects. And maybe in your's !

Here are some cool features.

Easily register your routes with the locales macro

// routes/web.php

Route::locales(function() {
    Route::get(
        trans('routes.about'), 
        'App\Http\Controllers\AboutController@index'
    )->name('about');
});

Read more in the 'Add your routes' section from the documentation.

The Localization instance

The \AlexJoffroy\Localization\Localization class provides a set of methods which could be helpful to use in your app. The object is registered as a singleton and can be accessed from the app container, the L10n facade or the l10n() helper.

$l10n = app('localization');
// or
$l10n = L10n::getFacadeRoot();
// or
$l10n = l10n();

There are useful check methods available such as:

  • isCurrentLocale
  • isDefaultLocale
  • isSupportedLocale
  • getSupportedLocales

Read more in the Localization instance section from the documentation.

Generate routes

You can easily generate a locale version of route with the Localization::locale method:

l10n()->route('about', [], true, 'fr'); // `https://yourapp.com/fr/a-propos`

// Shortcut will fallback to current locale
l10n()->route('about'); // `https://yourapp.com/en/about` 

Also, you can switch the locale for the current route:

// Given the current app url is `https://yourapp.com/en/about`

$l10n->currentRoute('fr'); // `https://yourapp.com/fr/a-propos`

Read more in the Generate routes section from the documentation.

Render a switch view

You'll probably want to display a selector allowing your users to choose their language. This can be easily done in a view:

{{ l10n()->renderSwitch() }}

Read more in the Render switch section from the documentation.

And many more

Middleware, helpers, and more are available to make it fit your needs. This package is probably not the most exhaustive one, it's a little bit opinionated and it has limitations. But it's doing it right for me, so why not for you ?

Please check the full documentation on Github to know about installation, configuration and usage.

I hope you like it ! Feel free to send me any feedback or question on Twitter πŸ˜‰