Posted 7 min to read

Adding multilanguage support to Laravel application requires focusing on three main aspects: routing, user interface translation and database structure modification. In this article I will describe each of them and draw attention to good practices that are worth following.
photo by maxxyustas (elements.envato.com)

Routing preparation

Each subpage should have a unique URL for a given language (you will find out why this is important later in this article), for example:

  • /en/contact - English version,
  • /es/contact - Spanish version,
  • /pl/contact - Polish version.

In a more SEO-friendly version, we can translate the routes:

  • /en/contact,
  • /es/contactenos,
  • /pl/kontakt.

Although both variants can be obtained directly through routing rules, I recommend using the mcamara/laravel-localization package. It not only allows you to easily use both of the above variants, but also offers a lot of other useful features (such as localized URLs, URL helpers or language detection based on browser settings). After completing all installation steps, put all multilingual routes into a special group:

// routes/web.php

Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['localeSessionRedirect']], function()
{
    Route::get('/', 'PagesController@home')->name('pages.home');
    Route::get('contact', 'PagesController@contact')->name('pages.contact');
});

This gives us the ability to use prefixes in URLs for the languages listed in the config/laravellocalization.php file in the supportedLocales array:

// config/laravellocalization.php

'supportedLocales' => [
  'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'],
  'es' => ['name' => 'Spanish', 'script' => 'Latn', 'native' => 'español', 'regional' => 'es_ES'],
  'pl' => ['name' => 'Polish', 'script' => 'Latn', 'native' => 'polski', 'regional' => 'pl_PL']
]

In the above example of routing group definition, it is worth paying attention to the presence of the localeSessionRedirect middleware, thanks to which, if the current (written in the session) language is e.g. es, then after entering https://example.com/contact you will be redirected to https://example.com/es/contact. However, if no language is saved in the session, it will be determined by the HTTP Accept-Language header (if useAcceptLanguageHeader in config/laravellocalization.php is set to true).

If you want to translate routes, create a resources/lang/**/routes.php file with their translations for each language, for example:

// resources/lang/pl/routes.php

return [
  'contact' => 'kontakt',
  'posts' => 'posty/{slug}
];

Then use them in routes/web.php:

// routes/web.php

Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => [ 'localize' ]], function ()
{
  Route::get(LaravelLocalization::transRoute('routes.contact'), 'ContactController@index');
  Route::get(LaravelLocalization::transRoute('routes.posts'), 'PostsController@show');
});

Note that there must be localize middleware in the Route::group().

User interface translation

This is an easy task as Laravel offers the possibility to translate strings in two ways. The first one is to create files with associative arrays containing "short" keys:

// resources/lang/en/contact.php

return [
    'header' => [
        'title' => 'Contact',
        'subtitle' => 'Hello :name, feel free to contact us!'
    ],

    'button_text' => 'Contact form'
];

The strings from this file can be extracted with the __() helper or the @lang directive (in Blade templates) as follows:

// resources/views/contact.blade.php

<header>
  <h1>{{ __('contact.header.title') }}</h1>
  <p>{{ __('contact.header.subtitle', ['name' => 'John']) }}</p>
</header>

<a href="#">@lang('contact.button.text')</a>

If the key is not found in the file for the currently used language, it will be searched in the file for the language indicated in fallback_locale inside config/app.php.

The second way is to use the whole string as keys inside the JSON file. For example, for the German language version you can create a file resources/lang/de.json with the following content:

// resources/lang/de.json

{
  "Your message has been sent successfully.": "Ihre Nachricht wurde erfolgreich gesendet."
}

The translation can be displayed in the same way:

echo __('Your message has been sent successfully.');

Personally, I prefer the first way (short keys in many files), because a reasonable division into files and the possibility of nesting arrays allows not to lose the context when making translations. Moreover, there are no conflicts. For example, the English word "contact" in Polish may mean not only "kontakt", but also the verb "kontaktować".

You can read more details about string translations in the official docummentation.

Database structure (MySQL)

There are several possibilities to create a multilingual data structure. Each of them has its advantages and disadvantages. I will show you a way that I have successfully implemented in dozens of small and large projects.

It is a good idea to use the spatie/laravel-translatable package, which includes a trait for convenient translation management in Eloquent models.

On the database side, data in multilingual table fields are kept in JSON format. An example table of "products" with such fields looks like this:

id | name | description | price
---
1 | {"pl": "Czarna bluza", "en": "Black hoodie"} | {"pl": "Wykonana w 100% z bawełny.", "en": "Made of 100% cotton."} | 89.00

Initially, I was very skeptical about this solution. I always adhered to the principle "one language = one record" or I created a separate table with translations (product_translations). Both of these solutions turned out to be burdensome in fast-growing applications, but it seemed to me that keeping the data in JSON will make it even more difficult to retrieve data from the database and there will be performance problems. Nothing could be further from the truth. The laravel-translatable package has a number of convenient methods to handle translations. In addition, MySQL from version 5.7 already supports JSON format natively, so you can very easily make queries on your JSON fields. Take a look at the examples below.

Model preparation

Add Spatie\Translatable\HasTranslations trait to the model class and then define multilingual table fields in the $translatable property:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;

class Product extends Model
{
    use HasTranslations;

    public $translatable = ['name', 'description'];
}

Creating and updating translations

This can be done in three ways. The easiest way is to simply set the value of the object properties for the currently used language:

$product->name = 'New name';
$product->save();

Saving translations for multiple languages at once:

$product->name = ['en' => 'New name', 'pl' => 'Nowa nazwa'];
$product->save();

You can also use the setTranslation method:

$product->setTranslation('name', 'en', 'Other name');
$product->save();

Getting translations

When you want to get a value for currently used language, you can just do:

$product->name;

To get a translation in a specific language, you can use the getTranslation method:

$product->getTranslation('name', 'pl');

An array with all translations can be obtained in this way:

$product->getTranslations('name');

What is worth knowing when implementing multilanguage support?

  1. Do not display different language versions of the page at the same URL. If you save the language chosen by the user (in cookie, localStorage, etc.) just to know in which language the content should be loaded and you do not create different URLs for each language version, crawlers will not index all language versions of that page or worse - they will index different language version every time! This is why you should use language prefix in URLs.

  2. Make sure that the application layout is not created with pixel perfect precision for one language. Consider that a text that can fit into a 400x50 px box in English will not necessarily fit its German or Spanish translation. Make sure each content container can contain text of varying lengths.

  3. Be prepared for possible differences in the display of dates, time and currencies. The most common date format in the US is month/day/year, while in Europe and the UK you use day/month/year. This can lead to many misunderstandings. Similar differences also exist in the time display (12- or 24-hour clock) and of course currencies. Therefore, these values should not be hard coded.

  4. Remember about the lang attribute in HTML. It will allow search engines to generate search results more precisely and various screen reader applications will be able, for example, to pronounce the displayed text correctly.

  5. Link alternate language versions of the site using <link rel="alternate" hreflang="LANGUAGE" url="URL"> tag. This will allow crawlers not only to see additional content to be indexed, but also allow browsers to suggest the user to switch to a language version appropriate to their regional settings.