Laravel I18n Frontend Best Practices

When our apps need to be internationalized to accommodate multiple locales, Laravel gives us some help out of the box. Yet it's good to know a few additional Laravel i18n best practices that help us to keep our apps scalable, and our code clean and fun to work with.

While Laravel is great for many kinds of applications, it is a general purpose framework. This means that we may need to build our own layers on top of Laravel to best suit our custom web apps. For internationalization (i18n) it provides some help out of the box. However, we’d like to provide you with some additional Laravel i18n tips and tricks.

The Laravel PHP framework needs no introduction. It has soared to the top of the PHP MVC pack, and for good reason: Laravel is one of the best-designed frameworks out there. It’s simple, layered, and well documented. Laravel is just a joy to use.

I will assume that you know the basics of Laravel and have read its documentation on localization. Given that, we can jump into some insights that will help you with your localized Laravel app.

At time of writing, I’m using PHP 7.1, Laravel 5.5, and MySQL 5.6.

Laying the Ground: A Simple Locale Library

Laravel gives us some locale configuration in config/app.php and its app()->getLocale() and app()->setLocale() methods. These are fine for simple apps, but the moment our app grows past a few pages, we can find ourselves repeating code if we don’t put some basic internationalization architecture into place.

Extend the Basic Locale Configuration

Out of the box, Laravel is locale-aware, giving us the app.locale and app.fallback_locale configuration options. We can add an app.supported_locales option to give us a single source of truth for the locales our app handles.

This setup will pair with a Locale class that we’ll build and contains everything we need to display a locale on the frontend: Its language code, its name in the default locale, and its directionality to account for right-to-left languages.

Create A Locale Class

Now we can write a more robust, sexy localization API using this configuration. A basic Locale class can get us started.

Our Locale class uses an instance of the underlying Laravel app to expose a library of helper methods that make working with locales much more pleasant. It can also keep our code nice and DRY (Don’t Repeat Yourself) by being our go-to API for dealing with locales.

Use Service Providers for Reusability and Clarity

Currently, we have to instantiate a new instance of the Locale class and inject our app instance every time we need one of its methods.

1

2

3

if((newApp\Locale(app())->isSupported('es')){

// Spanish is supported

}

This can be a pain, and we can reduce this pain a bit by using a Larvel Service Provider. These are just a way to register functionality with Laravel for reuse and inversion of control (IoC). They can make our code more testable and easier to reason about.

To create the provider, we can use the built-in Artisan command line tool.

1

php artisan make:provider LocaleServiceProvider

This will generate the boilerplate class at app/Providers/LocaleServiceProvider.php.

We can update this class’ register method to bind our Locale class in Laravel’s IoC container.

app/Providers/LocaleServiceProvider.php (excerpt)

1

2

3

4

5

6

publicfunctionregister()

{

$this->app->singleton(Locale::class,function($app){

returnnewLocale($app);

});

}

We bind a singleton to our class name so that we don’t build an instance multiple times during a request. We also create a single source of truth for the construction of the class by providing our app instance in one place.

Now we can register our new service provider.

In config/app.php (excerpt).

1

2

3

4

5

6

7

8

9

10

11

12

13

14

'providers'=>[

/*

* Application Service Providers...

*/

App\Providers\AppServiceProvider::class,

App\Providers\AuthServiceProvider::class,

// App\Providers\BroadcastServiceProvider::class,

App\Providers\EventServiceProvider::class,

App\Providers\RouteServiceProvider::class,

App\Providers\LocaleServiceProvider::class,

],

Laravel will now bind our singleton to the class’ name during every request. We can use this binding to access our class.

1

2

3

if(app(App\Locale::class)->isSupported('ar')){

// Arabic is supported

}

Use a Global Helper Function or a Facade for Clean Code

While this is better than writing (new App\Locale(app())->current()), manually calling the IoC container every time we want to work with our locale library is less than ideal. We can make our lives easier using by writing a global helper function.

First we create app/helpers.php. In it, we can put this simple function.

1

2

3

4

5

6

7

8

9

/**

* Retrieve our Locale instance

*

* @return App\Locale

*/

functionlocale()

{

returnapp()->make(App\Locale::class);

}

Next, we register our file to be autoloaded by updating our Composer configuration.

composer.json (excerpt).

1

2

3

4

5

6

7

8

9

10

"autoload":{

"classmap":[

"database/seeds",

"database/factories"

],

"psr-4":{

"App\\":"app/"

},

"files":["app/helpers.php"]

},

There should really only be a handful of global functions in our app, otherwise we risk polluting the global namespace and colliding with framework or third-party code.

We can then run composer dump-autoload from the command line to make our new function available to our code.

And Behold!

1

2

3

if(locale()->isSupported('ru')){

// Russian is supported

}

Much cleaner, right? Alternatively, we can use a facade to access our local library. This has the advantage of not polluting the global namespace. The choice of using a global function or a facade is largely a matter of personal pereference, however.

To use a facade, we write a little file at app/Facades/Loc.php.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

<?php

namespaceApp\Facades;

useApp\Locale;

useIlluminate\Support\Facades\Facade;

classLocextendsFacade

{

/**

* Get the registered name of the component.

*

* @return string

*/

protectedstaticfunctiongetFacadeAccessor(){returnLocale::class;}

}

We then register the alias in config/app.php (excerpt).

1

2

3

4

5

6

7

8

9

10

11

12

13

'aliases'=>[

'App'=>Illuminate\Support\Facades\App::class,

'Artisan'=>Illuminate\Support\Facades\Artisan::class,

'Auth'=>Illuminate\Support\Facades\Auth::class,

// ...

'URL'=>Illuminate\Support\Facades\URL::class,

'Validator'=>Illuminate\Support\Facades\Validator::class,

'View'=>Illuminate\Support\Facades\View::class,

'Loc'=>App\Facades\Loc::class,

],

Now we can use our library via static method calls.

1

2

3

4

5

Loc::current();

if(Loc::isSupported('hi')){

// Hindi is supported

}

To avoid collision with the built-in PHP Locale class, we can name the class Loc.

Routing

With our neat library in place, we can get to the meat of our internationalization work. Our app needs a way to determine the current user’s locale. We can do this via received headers, the user’s IP address, or the currently requested URI.

Determine the Locale from the URI

We’ll focus on the URI method here since it’s quite common and the simplest to implement.

Assuming we have a music store app and we have routes like /artists/1/albums that we want to internationalize, we can set up the locale as a parameter in our routes.

You may notice that we are repeating ourselves quite a bit here, and this means that our controllers will have to handle our locale setting. An ArtistContoller should be dealing with artists, not locales.

Use Route Prefixes and Groups to Keep Things DRY

We can use route groups to eliminate this repetition.

In routes/web.php (excerpt).

1

2

3

4

5

6

7

8

9

10

11

Route::redirect('/','/'.locale()->current(),301);

Route::prefix('{locale}')->group(function(){

Route::get('artists','ArtistsController@index');

Route::get('artists/{id}','ArtistsController@show');

Route::get('artists/{id}/albums','ArtistAlbumsController@index');

//...

});

That looks a lot better, doesn’t it? In order to make sure a locale is always set in our URI, notice that we redirect our root route to /en, given that English is our default locale.

Be Careful with Route Parameter Order

Continuing the example above, if you were to write the ArtistsController@index method like this…

1

2

3

4

publicfunctionindex($id)

{

// do something with $id -- broken ⚠

}

…you would find that the locale value is in the $id parameter. We can rewrite this method to get around this problem.

1

2

3

4

publicfunctionindex($locale,$id)

{

// do something with $id -- working ✅

}

Use Middleware to Separate Concerns and Document Behaviour

To actually set the locale, we need to tell our app about the language code we found in the URI. The cleanest way to do this is by using middleware.

We can create a new middleware class using Artisan on the command line.

1

php artisan make:middleware SetLocale

This will create a boilerplate at app/Http/Middleware/SetLocale.php. We can update the handle method of our new middleware to set the locale.

We just grab the languagecode from /languagecode/foo, check if we support it, and fallback to a configured locale if we don’t. In all cases, once this middleware handles the request, our locale will be set. First, though, we need to register it with Laravel.

We set our middleware to run right after we check for maintenance mode. Of course, if your maintenance mode page needs to be localized, you may want to move the SetLocale middleware up the stack.

Now our locale will be set on each request of the application and we can retrieve it using locale()->current(). Moreover, a dedicated file and class exist to document this behavior so that future developers (including ourselves) can easily reason about how our app is determining its local. This is better than burying that code in the routing file.

The above <html> tag will always have a supported locale and directionality (‘ltr’ or ‘rtl’). The latter is important for right-to-left languages like Arabic and Hebrew, since the whole page layout will change for those.

Instead, we can use the resources/lang/en.jsonJSON translation files to make our views look much more natural.

resources/views/artists/show.blade.php (excerpt).

1

2

3

<h1>@lang(":name's Profile",['name'=>$artist->name])</h1>

<pclass="lead">@lang('The bio. The albums. The singles. All you need in one place.')</p>

While this method can be a bit verbose, it makes our views much easier to read. It also saves us from having to write translations for our default locale. In the example above, we wouldn’t have an en.json file. If our app only supported Arabic and English, we could just have one translation file.

resources/lang/ar.json (excerpt)

1

2

3

4

5

{

":name's Profile":"إسم :name",

"The bio. The albums. The singles. All you need in one place.":"القصة. الأغاني. الألابيم. كل ما تحتاج من معلومات في مكان واحد."

...

You can mix and match array-based PHP and JSON translation files.

In Closing

Writing code to localize your app is one task, but working with translations is a completely different story. Many translations for multiple languages may quickly overwhelm you which will lead to the user’s confusion. Fortunately, PhraseApp can make your life as a developer easier! Feel free to learn more about PhraseApp, referring to the Getting Started guide.

I hope these tips have helped you on your journey with Laravel i18n. This is a big topic, and we could dive into localizing other layers of a Laravel app, such as the model and data layers. I’m hoping we can do just that in an upcoming article. Stay tuned! 🙂