How to build a localized app with Laravel – Part 3

Creating multi-language string maps

In the previous chapters, we looked at what an international application is and the different things to consider when building one. We got started with the tour guide application and made the backend of our application.

In this chapter, we will look at how to add support for multiple languages on our application.

Prerequisites

Read and followed the previous guides.

What we are going to achieve

The first thing we need to do, is to confirm where a user is coming from or the language the users browser is requesting. If a user requests a specific language URL, we need to give preference to that, else we give preference to the language a users browser is requesting. We will need to setup a middleware to do this check for us.

The next thing is to serve the user content based on this language. We can use a route group if we want to have separate pages for these content. If we want to maintain the same page structure, we can then pass the language as a parameter to our methods. With the language passed, we can retrieve the data from the database.

We can choose to store the data in JSON format or have database table fields for each language. JSON is ideal so we can always add new language without having to adjust our database. We finally need to map strings for things like error messages. We can display the appropriate string based on the language we set.

Making the content structure for destination description

We will store descriptions as JSON string. The language will serve as the object key while the content as the object value. It should look like this when stored:

'{
  "en" : "This is the English version",
  "fr" : "C'est la version française",
  "de" : "Dies ist die deutsche Version."
}'

To create a JSON object in PHP, we need to create an array and encode the array as JSON. This can be done using json_encode($array). Instead of using json_encode every time we want to store a destination record, we can define a mutator on the Destination model to automatically convert our array into objects.

Open ./app/Destination.php and add the following:

// app/Destination.php

[...]
class Destination extends Model
{
    protected $casts = [
        'description' => 'array'
    ];

    [...]

    public function setDescriptionAttribute($value)
    {
        $this->attributes['description'] = json_encode($value);
    }
}

We cast description field as an array, so that when we retrieve a destination record, we do not have to use json_decode on it to retrieve the data. PHP does not have a native way of working with JSON objects. To use a JSON object, we have to convert the object to an array first.

Now, let us store some destinations in the three languages we support. We are going to use a database seeder to store the content in the database.

Run the following command to create a database seeder:

$ php artisan make:seed DestinationTableSeeder

Then, open the database/seeds/DestinationTableSeeder.php file and replace with the following:

// database/seeds/DestinationTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use App\Destination;

class DestinationTableSeeder extends Seeder
{
    public function run()
    {
        $destinations = [
            [
              'name' => "New York City", 
              'image' => "https://images.pexels.com/photos/700974/pexels-photo-700974.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", 
              'location' => "United States", 
              'description' =>[
                   "en" => "This is New York City",
                   "fr" => "C'est New York",
                   "de" => "Dies ist New York"
                ]
            ],
            [
              'name' => "Eiffel Tower", 
              'image' => "https://images.pexels.com/photos/5463/people-eiffel-tower-france-landmark.jpg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", 
              'location' => "United States", 
              'description' => [
                   "en" => "This is the Eiffel Tower",
                   "fr" => "C'est la tour Eiffel.",
                   "de" => "Das ist der Eiffelturm."
                ]
            ]           
        ];

        foreach ($destinations as $destination) {
                Destination::create($destination);
        }
    }
}

To run the seeder, use the following command:

$ php artisan db:seed --class=DestinationTableSeeder

We have created the content needed for our destination page. We will look at how to retrieve the content shortly.

Setting the app language based on user’s browser language preference

Getting the preferred language from a user’s browser is easy. Laravel Request provides a method for getting it. You can pass an array of languages your application supports, and the method will return a match for it. For example, if the user wants en_US and your application only has general English en, the method will return en, which is convenient for us.

Another cool thing about this method is that, if the user’s preferred language does not exist in our array, the method returns the first item in our array. So you may want to order the languages you support with some preference.

We will make a middleware that will run with the web middleware group. It will set the language of our Laravel application based on the users browser preference. To create the middleware, run the following command:

$ php artisan make:middleware Localize

Now, open ./app/Http/Controllers/Middleware/Localize.php and replace with the following:

<?php

namespace App\Http\Middleware;

use Closure;

class Localize
{
    public function handle($request, Closure $next)
    {
        app()->setLocale($request->getPreferredLanguage(config('app.languages')));
        return $next($request);
    }
}

Line 11 sets the app language based on the famous Request method we have been talking about. We will define our languages inside ./config/app.php file.

Navigate to ./config/app.php and add the following:

// config/app.php
[...]

    'fallback_locale' => 'en',

    'languages' => ['fr','de','en'],

[...]

Finally, load the middleware in .app/Http/Kernel.php:

   // app/Http/Kernel.php

   [...]
    protected $middlewareGroups = [
        'web' => [
            [...]
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\Localize::class,
        ],
        [...]
    ];

Setting the app language based on the routes

Depending on how you intend to compose your routes, setting the app language should not be difficult. For this tour guide, we are going to use route prefixes. These prefixes will represent each of the languages we support. When a user visits the route, we will set the language based on what prefixes it.

Using route prefixes can present a little challenge. Since we want some specific routes to have different language versions, we may need to create different prefixes representing those languages and create the same routes under the prefix group. This is what I mean:

Route::prefix('en')->group(function() {
  Route::get('/', function () { return view('welcome'); });
});
Route::prefix('fr')->group(function() {
  Route::get('/', function () { return view('welcome'); });
});
Route::prefix('de')->group(function() {
  Route::get('/', function () { return view('welcome'); });
});

That approach does not look good. Instead, We might want to use wildcards on the routes like

Route::get('{lang}/', function () { return view('welcome'); });

and have our controller check the value of the wildcard, but that is equally not efficient.

So, we will use a wildcard prefix instead. Open your routes/web.php and add the following:

// routes/web.php
[...]
Route::prefix('{lang}')->group(function() {
  // routes here
});

Then, we are going to bind this wildcard in our RouteServiceProvider so we can pick the value of the wildcard. We will also throw a 404 not found if the wildcard is not part of the languages we support.

Open ./app/Providers/RouteServiceProdiver.php and add the following:

// app/Providers/RouteServiceProdiver.php
[...]
    public function boot()
    {   
      parent::boot();
      Route::bind('lang',function ($name){
        in_array($name,config('app.languages'))? app()->setlocale($name) : abort(404);  
      });
    }
[...]

Because we used this approach, any route a user visits will always set the app language. This way, the users have control over the content they see.

For pages with static content, we can use a string map for it. We can then use the app language to determine which content to show our users.

Returning content based on app language

We have looked at the two ways in which we can set the language of our application. Now, let us return the description of our destinations based on the app language.

We can define an accessor on the Destination model that will return the description we want. However, we cannot define the accessor on the description attribute itself. If we do, we will no longer be able to retrieve the original JSON string we saved once we choose to update it.

We will define an appended attribute on the model and define an accessor on it. Open the Destination model and add the following:

[...]

class Destination extends Model
{
    [...]

    protected $appends = ['translated_description'];

    [...]

    public function getTranslatedDescriptionAttribute()
    {    
        return $this->description[app()->getLocale()];
    }
}

Now, whenever we want to use the translated content (like in our views), we use translated_description.

Creating a string map for English, French and German

Laravel provides a localization feature that makes retrieving strings in various languages easy. To create string maps, we need to create a JSON file named after the language we want to map. The JSON object key will be the primary language version of the string/phrase (in our case, English) and the value will be in the language we want to map.

In our case, we will create fr.json and de.json. The string map below will work with the authentication pages generated when we use artisan to make the auth scaffolding.

Next, we will create two files in the resources/lang to store our string maps

$ touch resources/lang/fr.json
$ touch resources/lang/de.json

Insert the following content into fr.json file located at resources/lang/fr.json:

{
    "Login" : "Se connecter",
    "Password" : "Mot de passe",
    "E-Mail Address" : "La Adresse électronique",
    "Forgot Your Password?" : "Mot de passe Oublié?",
    "Remember Me" : "Souviens-toi de Moi",
    "Register" : "Enregistrer",
    "Confirm Password" : "Confirmer Mot de passe",
    "Name" : "Nom",
    "Welcome to the TourGuide" : "Bienvenue au TourGuide",
    "View destinations" : "Voir les destinations",
    "View More" : "Afficher plus",
    "Book Now" : "Reserve maintenant",
    "Phone" : "Téléphone",
    "Country" : "Votre Pays",
    "You do not have any reservations" : "Vous n'avez aucune réservation",
    "How many people are coming?" : "Combien de personnes viennent?",
    "e.g" : "par exemple",
    "When would you like to visit?" : "Quand voudriez vous visiter?",
    "Make a booking" : "Faire une réservation",
    "Number of tourists" : "Nombre de touristes",
    "Tour date" : "Date de la tournée"
}

Insert the following content into de.json file located at resources/lang/de.json:

{
    "Login" : "Anmeldung",
    "Password" : "Passwort",
    "E-Mail Address" : "E-Mail-Addresse",
    "Forgot Your Password?" : "Haben Sie Ihr Passwort vergessen?",
    "Remember Me" : "Erinnere dich an mich",
    "Register" : "Registrieren",
    "Confirm Password" : "Bestätige das Passwort",
    "Name" : "Name",
    "Welcome to the TourGuide" : "Willkommen beim TourGuide",
    "View destinations" : "Ziele anzeigen",
    "View More" : "Mehr sehen",
    "Book Now" : "buchen Sie jetzt",
    "Phone" : "Telefon",
    "Country" : "Land",
    "You do not have any reservations" : "Sie haben keine Reservierungen",
    "How many people are coming?" : "Wie viele Leute werden kommen?",
    "e.g" : "z.B.",
    "When would you like to visit?" : "Wann möchten Sie besuchen?",
    "Make a booking" : "Reservieren",
    "Number of tourists" : "Anzahl der Touristen",
    "Tour date" : "Tourdatum"
}

Now, let us make use of this in our view file. Open the resources/views/welcome.blade.php file and edit:

<!doctype html>
<html lang="{{ app()->getLocale() }}">
[...]
    <body>
        <div class="flex-center position-ref full-height">
            @if (Route::has('login'))
                <div class="top-right links">
                    @auth
                        <a href="{{ url('/home') }}">Home</a>
                    @else
                        <a href="{{ route('login') }}">{{ __('Login') }}</a>
                        <a href="{{ route('register') }}">{{ __('Register') }}</a>
                    @endauth
                </div>
            @endif

            <div class="content">
                <div class="title m-b-md">
                    {{ __('Welcome to the TourGuide') }}
                </div>

                <div class="links">
                    <a href="{{url(app()->getLocale().'/destinations')}}">{{__('View destinations')}}</a>
                </div>
            </div>
        </div>
    </body>
</html>

Using the mapped string is quite easy. In the blade file, use __('String to translate'). If Laravel cannot find the JSON file for the app local language, it will use the original string we wish to translate. So, the String to translate can be in the primary language of our application.

That is all we need do to use the string map.

Creating error and validation messages based on language

Laravel uses language strings for errors and validation. These language strings are stored in PHP files within the resources/lang directory. Within this directory, we need to create subdirectories for each language supported by the application.

Create the following subdirectories – fr and de inside resources/lang.

$ mkdir resources/lang/fr
$ mkdir resources/lang/de

In the two directories we just created, create the following files:

$ touch resources/lang/fr/auth.php
$ touch resources/lang/fr/pagination.php
$ touch resources/lang/fr/passwords.php
$ touch resources/lang/fr/validation.php
$ touch resources/lang/de/auth.php
$ touch resources/lang/de/pagination.php
$ touch resources/lang/de/passwords.php
$ touch resources/lang/de/validation.php

At this stage, head over to the GitHub repository for this project and copy out the required language files.

Now, you can try it out by checking out the login or register pages. Run php artisan serve then visit the url.

To see the pages in different language versions, change the language settings on your browser language settings. If you are on a chrome browser, read about how to change your language here.

Conclusion

We have come very far. We have understood what internationalization requires and we created a basic application setup for it. We looked at how to store content in our database when it has multiple language versions. We also looked at how to get the language preference of a user and how to use the app language to show the content we want.

We can say at this point that we fully understand what it takes to build an international application. In the next chapter, we will look at the frontend of the application and make pages to test everything we have built. We will address any accessibility issues that may arise and see how best to make the experience of our application seamless.

The source code to the application in this article is available on GitHub.

This post was originally posted on Pusher.