Content Index
- 1. What is Localization in Laravel and Why is it Important
- 2. Initial Localization Configuration in Laravel
- 3. Creating Language Files (PHP and JSON) and Text Strings for Translation
- 4. Displaying Translations in Views and Controllers
- 5. How to Change the Language Dynamically in Your App
- 6. Advanced Localization and Best Practices
- Middleware to Verify the es/en language prefix in Laravel
- Translations in Laravel Inertia
- The Manual Solution
- The Elegant Solution: Bilateral Integration with a Package
- Backend Installation (PHP)
- Language Files (Lang) Configuration
- Logic in Routes and the Controller
- The Localization Controller
- Consuming Translations in the Vue Component
- Demonstration and Conclusion
- Select Language in Laravel Livewire
- The component and the handling of language
- Interface transition and reload
- Middleware to maintain the language
- Language files
- 7. Conclusion: Experience and Practical Recommendations
- ❓Frequently Asked Questions
With the native features offered by Laravel for handling localization, that is, the place where the application is being consumed, and with this, being able to offer another service, such as the automatic translation of texts based on localization or user selection. In this section, we will see how to implement both topics.
We already know how to handle Custom Exceptions in Laravel.
Localization in Laravel is one of those features that, once you master them, allow you to take your applications to another level. In my experience, it not only improves usability but also opens the door to new markets and users. Throughout this guide, I will tell you how to configure, create, and manage translations in Laravel, relying on real examples and best practices.
1. What is Localization in Laravel and Why is it Important
Localization (L10n) is the process of adapting your application to different languages, currencies, or regional formats. Laravel integrates it natively, facilitating both text translation and automatic user language detection.
In one of my first multi-language projects, I discovered that minimal configuration was enough to offer the complete interface in English and Spanish. This capability is especially useful if your application has international users or if you plan to expand outside your initial market.
Key difference:
- Internationalization (i18n): preparing the app to handle multiple languages.
- Localization (L10n): adapting specific texts and configurations to each language.
2. Initial Localization Configuration in Laravel
When creating a new Laravel project, you will notice that the /lang folder does not exist. In my first attempts, I thought it was an error, but Laravel actually expects you to generate it yourself with a simple command:
$ php artisan lang:publishThis command creates the /lang folder and publishes the base language files that Laravel uses internally.
The lang:publish command will create the lang directory in your application and publish the default set of language files used by Laravel:
Inside config/app.php you can define the default language by modifying the line:
'locale' => 'es',If you work with multiple languages, it is also advisable to adjust the fallback language:
'fallback_locale' => 'en',In this way, if a translation is missing in Spanish, Laravel will display the text in English.
3. Creating Language Files (PHP and JSON) and Text Strings for Translation
Laravel provides two ways to manage translation strings, which are used to display the translated texts of our application in different languages; in both cases, we must create a folder to store them:
/lang
Where we create our translation files, whether PHP:
/lang /en messages.php /es messages.php
Or in JSONs:
/lang en.json es.json
We will use the format of the files in PHP; you can create as many as you want and modularize the messages according to your preferences.
Finally, it is up to us to define the translated strings and customize them by changing the predefined text by the application and creating our own; using the key/value pair, the key and the translation are indicated; for example:
lang/en/messages.php
'Welcome to our application!', ];lang/es/messages.php
'Bienvenido a nuestra aplicación!', ];4. Displaying Translations in Views and Controllers
Once the strings are created, you can display them with the __() function or with the @lang directive in Blade.
echo __('messages.welcome')As you can see, we must place the file name and the key as the argument for the __() function, which returns the translated text; we can use this scheme in both the controller and similar places, as well as in the view.
Common errors:
- Using an incorrect key (Laravel will return the literal key).
- Not publishing the /lang folder.
- Forgetting to clear the cache with php artisan config:clear after changing language files.
5. How to Change the Language Dynamically in Your App
To change the language during navigation, you can do it manually or through a middleware.
Generate localization middleware
$ php artisan make:middleware LocalizationAnd in the handle method:
public function handle($request, Closure $next)
{
$locale = $request->get('lang', config('app.locale'));
app()->setLocale($locale);
return $next($request);
}Thus, if the user visits /home?lang=es, the application will automatically switch to Spanish.
In one of my projects, I added a language selector in the site's header and saved the preference in the session to maintain the choice on each visit:
session(['locale' => $locale]);
app()->setLocale(session('locale', 'es'));6. Advanced Localization and Best Practices
Once you master the basic configuration, you can advance to more complex scenarios:
- Translation of dynamic content (from the database): using packages like Spatie Translatable.
- Translated routes: with Laravel Localization.
- Multi-language SEO: implementing tags.
Additionally, Laravel allows pluralization and variable substitution in strings:
'notifications' => '{0} You have no notifications|{1} You have one notification|[2,*] You have :count notifications',With this structure, the framework automatically chooses the correct translation based on the number.
Middleware to Verify the es/en language prefix in Laravel
The next development that we are going to carry out is to use a middleware to detect the language configured by the user and establish it using the translation text strings that we defined before (we previously saw how to use Queues and Jobs), in addition, we also define the acronym or language label in the URL to return the translation accordingly. ; for it:
$ php artisan make:middleware LanguagePrefixMiddlewareWhich will have the following content:
app\Http\Middleware\LanguagePrefixMiddleware.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class LanguagePrefixMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$language = $request->segment(1);
if(!in_array($language,['es','en'])){
return redirect('/es/blog');
}
app()->setLocale($language);
return $next($request);
}
}With the above code, it is only redirected when language tags are not available in the URL, then the corresponding language is set.
And we use the middleware in the routes:
Route::get('/{lang}/mi-ruta', 'MiControlador@miMetodo');For example, in our application:
function routeBlog() {
Route::get('', [BlogController::class, 'index'])->name('blog.index');
Route::get('detail/{id}', [BlogController::class, 'show'])->name('blog.show');
}
Route::group(['prefix' => '{locale}/blog','middleware' => LanguagePrefixMiddleware::class], function () {
routeBlog();
});
Route::group(['prefix' => 'blog','middleware' => LanguagePrefixMiddleware::class], function () {
function () {
routeBlog();
});As you can see, we created a routeBlog() function to group the routes to which we want to verify the use of the language, in this example, those of the blog, so that it can be consumed through locale:
- es/blog/*
- en/blog/*
And without the locale, and in this case, it is redirected to the Spanish language according to the redirection defined in the middleware:
- blog/*
Translations in Laravel Inertia
We are going to learn how you can create a multi-language application just like we would in a traditional Laravel development, but with the particularity that here we are using Inertia.js with Vue. It is worth noting that this same implementation will work exactly the same way if you use React or Svelte in your architecture.
In base Laravel, it is as simple as doing something like:
__('messages.welcome')Or
trans('messages.welcome')Let's remember that in Laravel's native schema we create our translation files and call the texts using the helper functions __() or trans(). The problem is that we are no longer working with Blade files, but with .vue, .jsx, or .svelte files. By changing environments, we lose direct access to those server functions.
The Manual Solution
Previously, the way to approach this was by creating a custom Middleware.
You had to read the translation files on the backend and pass them into Inertia::share() to inject that entire text load globally into the client.
While this approach works (it is very similar to what we did in previous classes with Flash messages), the reality is that it is quite cumbersome and heavy. Manually passing massive collections of translations back and forth can unnecessarily overload the page size and clutter the business logic.
The Elegant Solution: Bilateral Integration with a Package
To save us that manual work in the shadows, we are going to use a specialized package that perfectly connects the world of Laravel with the frontend ecosystem (Vue/React/Svelte) in a completely transparent way.
To implement this solution, we need to cover two installations (one for the server and one for the client):
Backend Installation (PHP)
We run the Composer command to require the package in our Laravel core:
$ composer require erag/laravel-lang-sync-inertiaThen, we generate the language folder:
$ php artisan lang:publishWe publish the configuration file:
$ php artisan erag:install-langAnd we install the Node package, which is the one we integrate from the Vue files:
$ npm install @erag/lang-sync-inertiaLanguage Files (Lang) Configuration
The first step is to have our translation files ready on the backend. This is done the traditional Laravel way: we create the lang/ folder in the root (or inside resources/lang/ depending on your version) and generate the subfolders for each language, such as es/ or en/, with their respective array return files.
Since we already covered this procedure in detail in the basic Laravel course, I will take it a bit for granted, but remember that it is the framework standard for indexing text strings.
lang\es\messages.php
<?php
return [
'title' => 'Demo de Localización',
'welcome' => '¡Bienvenido a nuestra aplicación!',
'description' => 'Esta es una demostración de localización con Laravel Inertia.',
'greeting' => '¡Hola, :name!',
'select_language' => 'Seleccionar Idioma',
'current_language' => 'Idioma Actual',
'switch_to' => 'Cambiar a',
'content' => [
'intro' => 'Bienvenido a la página de demostración de localización.',
'features' => 'Características',
'feature_1' => 'Fácil gestión de traducciones',
'feature_2' => 'Sincronización automática con el frontend',
'feature_3' => 'Soporte para múltiples idiomas',
'footer' => '¡Gracias por visitarnos!',
],
'buttons' => [
'submit' => 'Enviar',
'cancel' => 'Cancelar',
'save' => 'Guardar',
'back' => 'Volver',
],
];lang\en\messages.php
<?php
return [
'title' => 'Localization Demo',
'welcome' => 'Welcome to our application!',
'description' => 'This is a demonstration of Laravel Inertia localization.',
'greeting' => 'Hello, :name!',
'select_language' => 'Select Language',
'current_language' => 'Current Language',
'switch_to' => 'Switch to',
'content' => [
'intro' => 'Welcome to the localization demo page.',
'features' => 'Features',
'feature_1' => 'Easy translation management',
'feature_2' => 'Automatic sync with frontend',
'feature_3' => 'Support for multiple languages',
'footer' => 'Thank you for visiting!',
],
'buttons' => [
'submit' => 'Submit',
'cancel' => 'Cancel',
'save' => 'Save',
'back' => 'Go Back',
],
];Logic in Routes and the Controller
To control the dynamic language switching, I prepared a couple of routes in our web.php file: one in charge of rendering the home view (index) and another designed specifically to process the action of changing the locale.
The Localization Controller
Let's go to the controller to see how we manage the client's request:
app\Http\Controllers\LocalizationController.php
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class LocalizationController extends Controller
{
public function index()
{
$locale = session('locale', 'en');
app()->setLocale($locale);
syncLangFiles('messages');
return Inertia::render('localization/Index');
}
public function changeLanguage(string $locale)
{
$availableLocales = ['en', 'es'];
if (! in_array($locale, $availableLocales)) {
$locale = 'en';
}
session(['locale' => $locale]);
app()->setLocale($locale);
return to_route('localization.index');
}
}
routes\web.php
// LOCALIZATION
Route::prefix('localization')->group(function () {
Route::get('/', [LocalizationController::class, 'index'])->name('localization.index');
Route::get('/lang/{locale}', [LocalizationController::class, 'changeLanguage']);
});Consuming Translations in the Vue Component
Once the package is configured and injected into our Vue application instance, using it in components is extremely clean. We have both classic syntaxes available, so you can choose and use the one you like best in your templates:
resources\js\pages\localization\Index.vue
<script setup>
import { ref } from 'vue';
import { router } from '@inertiajs/vue3';
import { vueLang } from '@erag/lang-sync-inertia';
const { trans, __ } = vueLang();
const currentLocale = ref('en');
const availableLocales = [
{ code: 'en', name: 'English', flag: '' },
{ code: 'es', name: 'Español', flag: '' },
];
function changeLocale(locale) {
currentLocale.value = locale;
router.visit(`/localization/lang/${locale}`, {
preserveState: true,
});
}
</script>
<template>
<div class="min-h-screen p-8">
<div class="mx-auto max-w-4xl">
<div class="mb-8">
<h1 class="mb-2 text-3xl font-bold text-gray-800">
{{ __('messages.title') }}
</h1>
<p class="text-gray-600">
{{ __('messages.description') }}
</p>
</div>
<div class="mb-8 rounded-lg p-6 shadow">
<h2 class="mb-4 text-lg font-semibold">
{{ __('messages.current_language') }}
</h2>
<div class="flex gap-2">
<button
v-for="locale in availableLocales"
:key="locale.code"
@click="changeLocale(locale.code)"
class="rounded px-4 py-2 transition-colors"
:class="
currentLocale === locale.code
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
"
>
{{ locale.flag }} {{ locale.name }}
</button>
</div>
</div>
<div class="rounded-lg bg-white p-6 shadow">
<h2 class="mb-4 text-xl font-bold text-gray-800">
{{ trans('messages.welcome') }}
</h2>
<p class="mb-4 text-gray-600">
{{ __('messages.content.intro') }}
</p>
<div class="mb-6">
<h3 class="mb-3 text-lg font-semibold text-gray-700">
{{ __('messages.content.features') }}
</h3>
<ul class="space-y-2">
<li class="flex items-center gap-2">
<span class="text-green-500">✓</span>
{{ __('messages.content.feature_1') }}
</li>
<li class="flex items-center gap-2">
<span class="text-green-500">✓</span>
{{ __('messages.content.feature_2') }}
</li>
<li class="flex items-center gap-2">
<span class="text-green-500">✓</span>
{{ __('messages.content.feature_3') }}
</li>
</ul>
</div>
<div class="mb-6">
<h3 class="mb-3 text-lg font-semibold text-gray-700">
{{ trans('messages.greeting', { name: 'Developer' }) }}
</h3>
</div>
<div class="flex gap-4">
<button
class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
{{ __('messages.buttons.submit') }}
</button>
<button
class="rounded bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300"
>
{{ __('messages.buttons.cancel') }}
</button>
<button
class="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600"
>
{{ __('messages.buttons.save') }}
</button>
</div>
<div class="mt-8 border-t pt-4">
<p class="text-gray-500">
{{ __('messages.content.footer') }}
</p>
</div>
</div>
<div class="mt-8 rounded-lg bg-blue-50 p-6">
<h3 class="mb-2 font-semibold text-blue-800">How it works</h3>
<p class="text-sm text-blue-700">
This page uses the <code>__()</code> and
<code>trans()</code> functions from
<code>@erag/lang-sync-inertia</code> to display translations
synced from Laravel's language files.
</p>
<ul class="mt-2 text-sm text-blue-700">
<li>
<code>__('messages.title')</code> - Simple translation
</li>
<li>
<code
>trans('messages.greeting', {'{'} name: 'Developer'
{'}'})</code
>
- Translation with placeholders
</li>
</ul>
</div>
</div>
</div>
</template>The most important aspect of the previous implementation is the use of syncLangFiles:
syncLangFiles('messages');You can synchronize multiple:
syncLangFiles(['messages', 'auth']);If you need to generate the JSON for the translations:
$ php artisan erag:generate-langDemonstration and Conclusion
If we test the behavior in the browser, you will see that when you click the "English" button, the text instantly changes to "Welcome to our application". Thanks to storing the state in the Laravel session, if the user navigates through the site or reloads the page, the preference is perfectly maintained. If they switch back to "Español", the state updates and is preserved in the same way.
Keep in mind that, for the purpose of this demonstration, I limited the translations solely to the scope of this test page. If you are developing a real project, you should apply these same translation functions in your global components, such as the Sidebar, the Navbar, and the footer.
In short: working with languages in SPA architectures (with .vue or .jsx files) used to be a headache that required bloating the Inertia::share() method. Thanks to this two-pronged solution (a package for Composer and another for Node), we can keep using exactly the same native and clean Laravel syntax directly on the frontend, allowing us to scale the application without adding unnecessary noise to the code.
Select Language in Laravel Livewire
I'll show you how you can manage the language in Laravel. Here you can see that the interface now changes to Spanish, and then to English.
It's very simple: for this, we're going to use Livewire. However, if for some masochistic reason you don't want to use Livewire, you probably have the knowledge to make the changes yourself.
The important thing is to know the functions we need to call, beyond how we implement them; that is, to understand which methods to use for that purpose.
The component and the handling of language
This is the component. Here you can see that, once we receive the language (which comes from this list we have here), in this case I'm using Laravel + Livewire, every time we change the language using wire:model, we call the render() method defined:
<?php
namespace App\Livewire\User;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Illuminate\Support\Facades\App;
#[Layout('layouts.store')]
class UserProfile extends Component
{
public $language;
public function render()
{
if (!isset($this->language)) {
// al cargar el componente, este codigo se ejecuta al language NO estar seleccionado
// por el usuario
$this->language = session('locale') ?? App::getLocale();
} else {
// se ejecuta desde la vista asociada por el .live
session(['locale' => $this->language]);
App::setLocale($this->language);
}
return view('livewire.user.user-profile');
}
}There are many ways to do it, but this was the implementation I came up with. The important thing is that you get the user's language across. I'm showing you how I did it.
So, here's what we do:
- If it's not defined, I initialize the default language based on the language selected in the system (either in the session, which is where I save it, or using one of the methods to obtain the language at the application level).
- When we change the language using wire:model, we go here and set the language in the session, since I save it there.
- And also, very importantly, we must set the language at the framework level, because otherwise, we're not doing anything.
You can also save user preferences here if you want them to be persistent (for example, save them to a database).
In another video, I told you how I recommended saving user preferences without having to create 20 columns for 20 different preferences. What I was doing was saving a small JSON object.
I mentioned this before, and that's exactly what we're doing here.
Interface transition and reload
In the case of Livewire, to reload the entire interface, I also added a small AlpineJS that waits a few seconds for the request (wire:model) and then reloads the entire page.
This is so we can see the change applied to the entire interface, and that's why it's fully reflected:
<flux:select :label="__('Language')" id="language" wire:model.live="language" x-data @change="setTimeout(function(){window.location.reload()}, 100)" class="mt-1 block w-full rounded border-gray-300">
<option value="es">Español</option>
<option value="en">English</option>
</flux:select>Middleware to maintain the language
It's important that changes persist across all requests. If, for example, we return to "Blog" or navigate to another part of the application, the language would revert to the initial language.
To avoid this, we must implement middleware that is responsible for setting the language saved in the session for each request.
Because, again, this isn't persistent: if we reload the page without middleware, it's lost.
This is a pain. To solve it, we implemented the following middleware. As you can see:
$ php artisan make:middleware SetLocaleThen, within that middleware, we set the language we have in the session, if and only if it exists.
You can also pass the default language as a second parameter (if you don't want to use the conditional language), but in this case I defined it like this:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
class SetLocale
{
public function handle($request, Closure $next)
{
if (session('locale')) {
App::setLocale(session('locale'));
}
return $next($request);
}
}Where do we call this middleware? You can configure it directly in the routes if you prefer:
Route::middleware([SetLocale::class])But I mean putting it inside the web route group.
bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web([
// Tu middleware personalizado
\App\Http\Middleware\SetLocale::class,
]);
})Although a cleaner way is to use withMiddleware(), which is now available in Laravel 11.
With this method, you can define it directly in web-type routes, and this way, each request automatically executes this middleware.
Remember that, by definition, middleware executes before each HTTP request from the browser or other application.
Language files
Create your language files in English and Spanish, in this case for English and Spanish.
Define the key-value pairs as an object:
resources/lang/es.json
{
"Light": "Claro",
"Dark": "Oscuro",
***
} resources/lang/en.json
{
"Light": "Light",
"Dark": "Dark",
***
} Then, from the views, you simply reference the text using __(), which is the name of the method that takes the field name.
For example:
{{ __('Light') }}So, in this simple way, we have configured the language in Laravel. It's just two key steps.
7. Conclusion: Experience and Practical Recommendations
Localization is a tool that marks the difference between a functional app and an app prepared to scale globally.
In my experience, the most important thing is to maintain a clear structure in the language files, use consistent key names, and document new translations within the team.
If you work in a team, an additional tip is to use a tool like Phrase or Linguise to synchronize translations and avoid inconsistencies.
And remember: although Laravel does much of the work for you, the quality of localization depends on how organized you keep your texts and your workflow.
❓Frequently Asked Questions
- What is the difference between JSON and PHP translations?
- PHP files allow grouping texts by module, while JSON saves the strings directly. PHP is more scalable.
- How do I automatically detect the language?
- You can use Request::getPreferredLanguage() or create a middleware that reads the language from the browser or the URL.
- Can I translate dynamic content?
- Yes, with packages like Spatie Translatable you can save versions in multiple languages in the database.
- How do I change the language without reloading the page?
- You can use AJAX or Livewire to update the content based on the user's language selection.
The next step is to learn the Authorization system in Laravel with Gates and Policies.