Laravel framework has a built-in support for localization and give us all we need to create multi language websites like a breeze.
Localizing web apps is an essential aspect in web development. For this Laravel framework provides us all we need to implement localization and build a multi language website. Let’s describe how to achieve this in Laravel 10.
By default Laravel stores translation files in dedicated directory, on Laravel version 10 it’s the lang/ directory.
However on a fresh laravel installation this directory not exist until you publish laravel language files using the lang:publish artisan command:
php artisan lang:publish
Once executed this command you will see a new directory lang/ created in the project root with some built language files like validation, authentication, pagination, etc:
You can customize the translations in any of these files and also you can add your own translation files according to your app needs.
If you open any translation file eg. auth.php, you will see that it returns array of translations:
<?php return [ 'failed' => 'These credentials do not match our records.', 'password' => 'The provided password is incorrect.', .... .... ];
Let’s create a new directory ar/ inside of lang/ considering that our app will contain two languages Arabic and English. Then create the messages file in each lang directory:
en/messages.php
<?php return [ 'Welcome to our app' => 'Welcome to our app' ];
ar/messages.php
<?php return [ 'Welcome to our app' => 'أهلا بك فى تطبيقنا ' ];
As you see a typical translation file return an array of translations. To retrieve any translation string in laravel classes or Blade views, the __() function can be used like so:
echo __('messages.Welcome to our app');
{{ __('messages.Welcome to our app') }}
As you see to retrieve the translated string we are using dot notation by separating language filename and the string, like <language_file.message>. Using this mechanism we can retrieve nested deep translated messages from the language files. For example we can divide the language file into sections as shown:
<?php return [ 'home' => [ 'welcome text' => 'Welcome to home' ], 'store' => [ // store related messages ], 'single_post' => [ // single post page related messages ], 'forms' => [ // forms related messages 'contact_form' => [ 'header' => 'Contact us' ], 'comment_form' => [ 'header' => 'Add Comment ] ] ];
Now to retrieve any translated string as usual you can go deep into nested arrays:
__('messages.home.welcome text')
__('messages.forms.contact_form.header')
Language Configuration
At this point if you print this translation string it will show the default translation which is english by default as identified in config/app.php configuration file:
'locale' => 'en', 'fallback_locale' => 'en',
These two settings defines the default locale and fallback locale of the app. The default locale is ‘en’. You may change this to other language ‘ar’ for example. The fallback_locale setting defines the locale when the active language doesn’t contain the given translation string.
Many people usually set the locale and fallback_locale to the same value like ‘ar’. However it’s important to keep the fallback_locale to be the system locale in case no translation string found in the locale the fallback will be used.
Laravel provides some methods to detect, set, and get the locale, for example to determine the current locale you use App::isLocale($locale) method:
if(App::isLocale('ar')) { // }
Also there is App::currentLocale() method to get the current locale:
$currentLocale = App::currentLocale();
Another similar method to get the locale App::getLocale():
$currentLocale = App::getLocale();
To set locale to a new locale there is App::setLocale($locale) method:
App::setLocale('fr');
Let’s see an example language switcher:
routes/web.php
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('home'); }); Route::get('/lang/{locale}', function ($locale) { session(['lang' => $locale]); return redirect('/'); })->name('lang.switch');
Here i added routes for home page and another route for switching to another language using a $locale param. In the route closure i saved the $locale into session to be used later in middleware.
resources/views/home.blade.php
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Localization</title> </head> <body class="antialiased"> <div> <div> @if(app()->isLocale('en')) <a href="{{ route('lang.switch', 'ar') }}">العربية</a> @else <a href="{{ route('lang.switch', 'en') }}">English</a> @endif </div> <div class="max-w-7xl mx-auto p-6 lg:p-8"> {{ __('messages.Welcome to our app') }} </div> </div> </body> </html>
In this view i added two links to navigate to each locale, but one link is shown at a time by checking for app()->isLocale(). Then i displayed a simple translated message from messages.php files.
Now if you click in any of the links the language will be saved into session but you will not see translated message that’s because we don’t set the updated locale yet.
To set the updated locale i created a middleware:
app/Http/Middleware/LangSwitcher.php
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; use Symfony\Component\HttpFoundation\Response; class LangSwitcher { /** * Handle an incoming request. * * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next */ public function handle(Request $request, Closure $next): Response { App::setLocale(session()->get('lang') ?? 'en'); return $next($request); } }
In the handle() method i invoked App::setLocale() method passing the lang stored in the session if exist. The last thing to do is add this middleware to kernel.php
app/Http/Kernel.php
protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, .... .... \App\Http\Middleware\LangSwitcher::class ] ];
As shown i added the middleware to the $middlewareGroups array in the web section so that it can be applied to all web routes.
Translated Strings
So far i described how to add translated strings and how to retrieve them from language files. In the previous example i created a messages.php file to hold translation messages, and retrieved the messages using the __() function. There is also another function trans() which works the same as __() function.
{{ trans('messages.Welcome to our app') }}
Also there is a blade directive @lang which achieve the same purpose:
@lang('messages.Welcome to our app')
String Interpolation
We can add dynamic parameters in translated strings which can be replaced at runtime like so:
'Product Price' => 'Product :productname Costs :price',
Then you can pass in the parameters as second argument to __() like so:
__('messages.Product Price', ['productname' => 'Sample product', 'price' => '$20'])
String Pluralization
String pluralization is a way to display different translations for the same string based on if something is pluralized or single. In laravel this can be achieved using the | character:
'Cart Items' => 'There is one item in the cart|There are many items in the cart'
Here we provided two variations of the translated string, one is singular form and the other is plural form.
To retrieve translations the trans_choice() function can be used:
trans_choice('messages.Cart Items', 1) // There is one item in the cart trans_choice('messages.Cart Items', 5) // There are many items in the cart
The trans_choice() function accepts the string to translate and another argument which is count, if the count is greater than 1 then the plural form of the string is returned, otherwise the singular form is returned.
You can add complex variations of pluralization like so:
'selection' => '{0} There are none|[1,5] There are some|[6,*] There are many',
trans_choice('messages.selections', 0) // There are none trans_choice('messages.selections', 5) // There are some trans_choice('messages.selections', 7) // There are many
Also you can combine dynamic parameters with pluralized strings:
'users selected' => 'One user :username selected|Many users selected'
trans_choice('messages.users selected', 1, ['username' => 'john'])
Using Json Language Files
Laravel supports using Json files to hold language translations instead of PHP files, in that case each language will have a dedicated json files in the lang/ directory like so:
lang/
en.json
ar.json
fr.json
Example:
lang/en.json
{ "landing message": "Welcome" }
lang/ar.json
{ "landing message": "مرحبا بك!" }
To retrieve the translated message the same way using __() function:
__('landing message')
For nested strings you can specify nested keys like so:
{ "messages": { "home": { "welcome text": "Welcome to homepage" } } }
To retrieve nested strings in json files, this approach can be used:
__('messages')['home']['welcome text']
Best Practices For Localizations
- Use any approach for storing translation messages either PHP files or Json Files.
- For big projects with lot’s of translation strings it’s preferrable to use JSON files.
- Apply Pluralization techniques for text that has many variations to get different translation for singular and plural form using trans_choice() function.
- When storing translations in PHP files it’s better to use different translation files (domains) and not single file depending on your application parts or sections, i.e file for forms, file for email messages, files for store page. etc. This idea inspired from PHP native gettext() function. Read this article about gettext here.
- If you exposed translations to front-end frameworks like React or Vue it’s better to use caching when retrieving translations so that to improve performance.
What about translating content and Eloquent models? For this there is some Laravel packages that can do the hard work for you like spatie/laravel-translatable or astrotomic/laravel-translatable. I will write a post soon to learn about it.