Localization in Laravel

I was recently working on a Laravel application that required the registration form to offer a Ukrainian translation. I have never worked on any applications that required localization, so I was a little unsure how to tackle this. As with many things in Laravel, localization is fortunately straightforward. This post will outline the steps I took to accomplish this task.

Setup

For the purposes of this post, I will be using a fresh install of Laravel 5.4 with the auth scaffolding built out. If you would like to follow along, spin up a new instance of Laravel and run the following artisan command:

php artisan make:auth

Configuration

The first items to consider when utilizing Laravel's localization features are:

  1. Default Locale - This is the locale your application will be initially set to. So if a user does not set his/her language the app will default to this value. In our case English.
  2. Fallback Locale - Sounds a bit like the default locale but actually means the locale that will be used if a language file for the locale that is set cannot be found.
  3. Locales - Array of locales your application will support. NOTE: you must add this value manually to the config/app.php file.

Here's what your configuration will look like.

// FILE: config/app.php

// default locale
'locale' => 'en',

// locale your app will use if there is not a language file available
'fallback_locale' => 'en',

// locales you want your app to support
'locales' => ['en' => 'English', 'uk' => 'Українська'], 

Language Files

Now that we have the languages configured we will add the language files containing the translated text. The translations that your app will use for localization are located in the resources/lang directory. Each language will have it's own directory named by the locale code.

Since we are translating the registration form, we will create a file named register.php for each language. The important thing to note here is that the files are named the same in each locale directory and use the same keys.

English

// FILE: resources/lang/en/register.php

return [
     'Register'         => 'Register',
     'Name'             => 'Name',
     'Email'            => 'Email',
     'Password'         => 'Password',
     'Confirm Password' => 'Confirm Password', 
];

Ukrainian

// FILE: resources/lang/uk/register.php

return [
    'Register'         => 'Pегістр',
    'Name'             => 'Ім\'я',
    'Email'            => 'Eлектронна Пошта',
    'Password'         => 'Пароль',
    'Confirm Password' => 'Підтвердити Пароль',
];

@lang() Blade Directive

The @lang() blade directive is where all the magic happens. Basically when you pass in the file name (register) and key (Register, Name, Email, etc.) to the @lang directive it will look to see if that combination exists in your language files. If it cannot find a language file for a particular locale, then it will use the fallback locale we configured in config/app.php.

So let's go ahead and update the labels in our registration form to use the @lang directive.


<!-- FILE: resources/views/auth/register.blade.php -->

@extends('layouts.app')

@section('content')
...
    <div class="panel-heading">@lang('register.Register')</div>
    ...
            <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
                <label for="name" class="col-md-4 control-label">@lang('register.Name')</label>
                ...
            </div>

            <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
                <label for="email" class="col-md-4 control-label">@lang('register.Email')</label>
                ...
            </div>

            <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
                <label for="password" class="col-md-4 control-label">@lang('register.Password')</label>
                ...
            </div>

            <div class="form-group">
                <label for="password-confirm" class="col-md-4 control-label">@lang('register.Confirm Password')</label>
                ...
            </div>

            <div class="form-group">
                <div class="col-md-6 col-md-offset-4">
                    <button type="submit" class="btn btn-primary">
                        @lang('register.Register')
                    </button>
                </div>
            </div>
            ...
    </div>
    ...
@endsection

At this point if you change your locale to uk in your configuration and visit the registration page, you will see your form labels translated to Ukrainian.

registration form

Route Defined Locale

We now need a way to set the locale dynamically. The simplest way to set locale is to pass it in as an argument on the route.

Route::get('welcome/{locale}', function ($locale) {
    App::setLocale($locale);

    //
});

In this example, if you visited the route /welcome/uk, the app's locale would be set to uk and the corresponding Ukrainian translations would be used.

This is a very common solution and there are reasons why you may want to include the locale string in your url. However, in my implementation I preferred not to have the locale string in the url and opted to save it to a session variable instead.

Session Defined Locale

Of course you could do some fancy thing like setting the user's locale based on where they are located but I'd rather have the user decide which language they prefer.

That being said, we need some way for users to select their preferred language. To do this we'll add a dropdown menu that contains the available locales supported by the app. When the user selects their preferred language, we will then set the locale in a session variable and use it to set locale in subsequent requests.

Note that the route and session defined locale solutions are not mutually exclusive, and you could combine them for your needs, but I will not cover that here.


<li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
        Language <span class="caret"></span>
    </a>
    <ul class="dropdown-menu">
        @foreach(config('app.locales') as $key => $locale)
            <li>
                <a href="{{route('locale', ['locale' => $key])}}">{{ $locale }}</a>
            </li>
        @endforeach
    </ul>
</li>

// FILE: routes/web.php

Route::get('/locale/{locale}', function (Request $request, $locale) {
    if (array_key_exists($locale, config('app.locales'))) {
        session(['locale' => $locale]);
    }
    return Redirect::back();
})->name('locale');

Locale Middleware

If you decide to use session defined locale, then you need some way of setting your app's locale based on the locale session variable early in the request cycle. This is a perfect job for a middleware.

To create any middleware simply run the artisan command:

php artisan make:middleware Locale

This will create a middleware named Locale in your app/Http/Middleware directory. After you generate your middleware you will then need to register it in the $middlewareGroups array in the app/Http/Middleware/Kernel.php file.

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\Locale::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

The middleware itself is very simple and simply reads the locale session variable, checks to see if this is a supported locale, and sets the app's locale if it is.

    // FILE: app/Http/Middleware/Locale.php
    public function handle($request, Closure $next)
    {
        $locale = $request->session()->get('locale');
        if ($locale !== null && array_key_exists($locale, config('app.locales'))) {
            \App::setLocale($locale);
        }
        return $next($request);
    }

Overview

All in all, localization like anything can get very complex very quickly. There are many items this post does not cover (e.g. error messages haven't been translated), and it barely scratches the surface. This post aims simply to give you a basic understanding on how localization is handled in Laravel.

Also, there are already a few very good packages out there to make working with localization in Laravel easier and tackle more complex issues like translated routes or storing translations in the database. I could have used any of these, but for such a simple requirement, I felt they would add more overhead than necessary.

If your app depends heavily on localization and has more complex requirements, then check out the following packages:

Categories: Web Development

Tags: Laravel