Logo
Paragraph blog

How to localize a Laravel application

Paragraph 17 May 2022

Laravel is the most popular PHP framework and one of the most popular software frameworks in general. It gained a lot of popularity in recent years due to its comprehensive set of features, nice expressive API, and great community support.

In this post, we will show how easy it can be to localize a Laravel app. We will start with an English-only application and add Spanish as the second language. We will then install the Paragraph package so that we can manage the Spanish translations through a web interface, instead of editing them in the code.

Starting

A brand new Laravel installation only got one route and one template, it’s this welcome page that you can see by visiting the / (root) path:

So let’s translate it. In this post, we will translate the “Documentation” headline and “Laravel has wonderful…” block of text. Let’s see what do we have in the view file, in resources/views/welcome.blade.php:


<div class="p-6">
    <div class="flex items-center">
        <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
        <div class="ml-4 text-lg leading-7 font-semibold">
            <a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">
                Documentation
            </a>
        </div>
    </div>

    <div class="ml-12">
        <div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
            Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you
            are new to the framework or have previous experience with Laravel, we recommend reading all of
            the documentation from beginning to end.
        </div>
    </div>
</div>
            

The text is currently included in the HTML markup so there is no way to substitute it with another language, we have to do something. Laravel offers global helper functions __(), trans() and trans_choice() as well as Blade directives @lang and @choice.

It makes no difference whether we use global functions or directives — behind the scenes the same thing is going to happen. Let’s wrap texts in @lang because it looks a bit cleaner this way:


<div class="p-6">
    <div class="flex items-center">
        <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
        <div class="ml-4 text-lg leading-7 font-semibold">
            <a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">
                @lang('Documentation')
                <!-- This looks a bit cleaner than curly braces but it's up to you really -->
            </a>
        </div>
    </div>

    <div class="ml-12">
        <div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
            @lang('Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you
            are new to the framework or have previous experience with Laravel, we recommend reading all of
            the documentation from beginning to end.')
        </div>
    </div>
</div>
            

If you refresh the page now, you won’t see any difference because by default Laravel localization (translator) class just falls back to the original value. If there are no translations available, we just don’t substitute the text.

Two ways of storing texts

You have two ways of storing the original texts (in your base language) with Laravel — either you can just keep them in the Blade files, wrapped in special functions, or you could move them to language files, and then reference them by ids. For example, if your default locale is set to ‘en’ in config/app.php file, you could create ‘en’ folder in resources/lang:


resources/lang/en/welcome.php
            

And that file could include something like:


<?

return [
  'documentation-headline' => 'Documentation',
  'documentation-description' => 'Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you
are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.'
];
            

Then your view has to change to something like this:


<div class="p-6">
    <div class="flex items-center">
        <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
        <div class="ml-4 text-lg leading-7 font-semibold">
            <a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">
                @lang('welcome.documentaton-title')
            </a>
        </div>
    </div>

    <div class="ml-12">
        <div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
            @lang('welcome.documentaton-description')
        </div>
    </div>
</div>
            

It’s up to you which way you want to go — use the text ids or have the original language texts in the Blade templates but note that if you make a typo or a reference to a non-existing text, your users will see the “funny” looking string id instead of real text:

Switching the language

Now you have to decide how do you want to switch the language in your app. Some of the most common options include:

  • Have the language code in the URL so that users can access pages in different languages by changing the URL, good for SEO
  • Remember the user-chosen language in the session
  • Store user-chosen language in the database, in user settings (eg. on user model)

Either way, Laravel wants you to use the built-in language (locale) switching mechanism so you need to connect your logic to framework logic. One of the best ways to do this in Laravel is by adding a middleware that would dynamically set the language (locale) based on URL or session or anything else that you want really. Once Laravel locale is set, helper functions and Blade directives will start translating your texts correctly.

Language as a route parameter

First, let’s try to add language to the URL. This way is very good for SEO because search engines like Google will find all your language variants increasing your search positions across different countries and markets.

We could of course use a query string parameter instead, but in SEO world matching paths are usually preferred to query parameters.

So, we have to modify our routes/web.php file since the default route doesn’t have any parameters in it:


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

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

Now you can visit URLs like “/en” or “/es” but they will all display English texts since we are not setting the application locale anywhere. Let’s create a HTTP middleware in app\Http\Middleware folder that does the trick:


<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetAppLocale
{
    public function handle(Request $request, Closure $next, ...$guards)
    {
        if ($request->locale) {
            app()->setLocale($request->locale);
        }

        return $next($request);
    }
}
            

All it does is it checks if the current HTTP request has a “locale” parameter on it and if it does, then it will pass the value to Laravel. If there is no “locale” in the route, it’s just skipped so that nothing breaks. Let’s add this middleware to all Laravel routes by editing the App\Http\Kernel class:


/**
 * The application's route middleware groups.
 *
 * @var array<string, array<int, class-string|string>>
 */
protected $middlewareGroups = [
    'web' => [
        // all other default middleware
        SetAppLocale::class,
    ],
    ...
];
            

Nice! Now if you go to “/es” page the locale should be set to Spanish but you will still see the English texts because we haven’t provided translations to Laravel.

Now depending on whether you chose to use text ids or original English texts in the Blade files, you either need to create resources/lang/es.json file:


{
    "Documentation": "Documentación",
    "Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.": "Laravel tiene una documentación maravillosa y completa que cubre todos los aspectos del marco. Ya sea que usted es nuevo en el marco o tiene experiencia previa con Laravel, le recomendamos leer todo la documentación de principio a fin."
}
            

Or in resources/lang/es/welcome.php file:


<?php

return [
    'documentation-title' => 'Documentación',
    'documentation-description' => 'Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.": "Laravel tiene una documentación maravillosa y completa que cubre todos los aspectos del marco. Ya sea que usted es nuevo en el marco o tiene experiencia previa con Laravel, le recomendamos leer todo la documentación de principio a fin.'
];
            

If we refresh the /es page now, we will see this:

“Documentation” got translated to “Documentación” correctly but the text block below it didn’t. This is because we used the “es.json” file in this example and the whitespace in welcome.blade.php file (original English text) is different from what we have in “es.json”. If we go back to welcome.blade.php and make the English text a single line:


<div class="p-6">
    <div class="flex items-center">
        <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" class="w-8 h-8 text-gray-500"><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
        <div class="ml-4 text-lg leading-7 font-semibold">
            <a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">
                @lang('Documentation')
            </a>
        </div>
    </div>

    <div class="ml-12">
        <div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
            @lang('Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.')
        </div>
    </div>
</div>;
            

It will start working correctly:

Which is a bit annoying, to be honest. There is no way to consistently have the same whitespaces in Blade templates and in language files — basically one extra space will ruin the translation. And if we make all long texts single-lined — that’s going to look bad in our IDE, hard to edit, too.

Text ids vs texts in views

Issues with text ids:

  • When the application grows, it’s hard to keep track of ids. They need to be unique, but we don’t remember what already exists and what doesn’t. If something does, it’s unclear whether it’s ok to reuse it or not. Typically, after a few years, the application will have many duplicates and many unused text ids
  • When looking at the code, sometimes it’s not immediately clear what’s going to be said to the user

Issues with texts in the views:

  • The key problem is the system fragility — you wanted to modify the original English text a little bit and now all of your translations are broken because they rely on a 100% match

So neither solution is perfect and you have to decide for yourself what you can live with.

Storing language in session or user profile

It’s very easy to modify the middleware to take the language from the session or user profile instead:


<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetAppLocale
{
    public function handle(Request $request, Closure $next, ...$guards)
    {
        if ($request->user()) {
            app()->setLocale($request->user()->locale);
        }

        // or

        if ($request->session()->has('locale')) {
            app()->setLocale($request->session()->get('locale'));
        }

        return $next($request);
    }
}
            

To change the language in the session, you could introduce links in the UX interface that would set the new locale and redirect the user back, eg:


<?php

use Illuminate\Support\Facades\Route;


Route::get('/change-language/{locale}', function ($request) {
    session()->set('locale', $request->locale);

    return redirect()->back();
});
            

Installing Paragraph

Now, let’s say we don’t want to touch the code every time we need to change the copy. We don’t want to pollute the commit history, and we also want our non-dev colleagues to participate in the text management process. Let’s install the Paragraph app via Composer:


$ composer require paragraph/laravel
            

Create a new Laravel app in Paragraph dashboard, you will receive your project ID and API key, copy & paste them in your .env file, eg:


PARAGRAPH_PROJECT_ID=CPTlwY2K2of-DU3RE43F5
PARAGRAPH_API_KEY=9qIBoHumEuS4scIvcqP0H7Dz
            

Now let’s run a console command that will initialise your project — it will scan for all existing texts and push them to Paragraph:


$ php artisan paragraph:init
Where should we look for language files?
  [0] ./example-app/resources/lang
  [1] My template path is not in the list
 >

Discovered 6 language files with a total of 92 texts
Where should we look for Blade templates?
  [0] ./example-app/resources/views
  [1] My template path is not in the list
 >

Discovered 1 view templates
Found 0 texts in the view templates
Sending to Paragraph
Done!

Let's try to render one page, what URL should we try?
 > /es
            

By following the prompts, we successfully parsed all language files and all Blade template files. We can see all our texts in the Paragraph dashboard now:

What’s even cooler, we can see the rendered page as well:

From now on, we and our colleagues can edit the texts right here, as if it was a CMS or WordPress, and not a Laravel app:

Any typo will be automatically detected and you can use machine translations (eg. Bing Translate, Amazon Translate, Google Translate) as a starting point for your texts or even outsource to human translators.

Tags

#laravel

Similar posts

About this blog

Paragraph is a SaaS app that allows to manage your product’s texts and communications as if it was a CMS — using a nice web interface. No more editing in code!

This blog will discuss emerging trends and technologies related to development and product localization.

Paragraph