Skip to content
Muhammet Şafak
tr
Web Development 4 min read

Multilingual (i18n) Applications in Laravel

Practical notes on adding multi-language support to Laravel: organizing language files, separating text from code, and managing locale switching.


Adding multi-language support to a project after the fact is far more painful than designing for it from the start. I learned this the hard way on a previous project: Turkish strings were hardcoded directly inside Blade templates, and when we needed to add a second language, we had to scan through every single view file. After that experience, I now add i18n (internationalization) support at the beginning of every new project.

How the language system works in Laravel

Laravel stores language files in the resources/lang directory. A separate subdirectory is created for each language — resources/lang/tr, resources/lang/en, and so on. Inside each directory you’ll find PHP files that each return a key/value array:

resources/lang/
├── tr/
│   ├── auth.php
│   ├── validation.php
│   └── messages.php
└── en/
    ├── auth.php
    ├── validation.php
    └── messages.php

A messages.php file might look like this:

<?php

return [
    'welcome'    => 'Hoş geldiniz',
    'logout'     => 'Çıkış Yap',
    'save'       => 'Kaydet',
    'not_found'  => ':name adında bir kayıt bulunamadı.',
];

And its English counterpart at resources/lang/en/messages.php:

<?php

return [
    'welcome'    => 'Welcome',
    'logout'     => 'Log Out',
    'save'       => 'Save',
    'not_found'  => 'No record found with the name :name.',
];

Using translations in Blade templates

In view files, you use either the __() helper function or the @lang directive:

<h1>{{ __('messages.welcome') }}</h1>
<button>{{ __('messages.save') }}</button>

Or, when passing parameters:

<p>{{ __('messages.not_found', ['name' => $username]) }}</p>

Laravel automatically replaces placeholders like :name with the provided values.

Setting the application locale

The active locale is set via App::setLocale(). Doing this inside a middleware is a clean approach:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\App;

class SetLocale
{
    public function handle($request, Closure $next)
    {
        $locale = $request->user()
            ? $request->user()->locale
            : session('locale', config('app.locale'));

        App::setLocale($locale);

        return $next($request);
    }
}

Once this middleware is registered in app/Http/Kernel.php, the locale is automatically set on every request.

Switching languages

A simple route is all you need to let users change their language preference:

<?php

Route::get('/language/{locale}', function (string $locale) {
    if (!in_array($locale, ['tr', 'en'])) {
        abort(404);
    }

    session(['locale' => $locale]);

    return redirect()->back();
})->name('language.switch');

In the view:

<a href="{{ route('language.switch', 'tr') }}">Türkçe</a>
<a href="{{ route('language.switch', 'en') }}">English</a>

For authenticated users, persisting the preference in the users table is a more durable solution; session storage alone is sufficient only for guest users.

There is a subtle gotcha here: if a user closes the browser tab and comes back after the session has expired, the locale reverts to the default. For authenticated accounts, the preference must be stored in the database — sessions are only adequate for unauthenticated visitors.

JSON language files

Starting with Laravel 5.4, JSON-format language files are supported. This approach uses the actual sentence as the key, which some developers prefer for simpler content:

{
  "Welcome": "Hoş geldiniz",
  "Log Out": "Çıkış Yap"
}

Usage looks slightly different:

__('Welcome') // returns 'Hoş geldiniz' when the 'tr' locale is active

This works well for small projects. For larger codebases, key-based PHP files are more maintainable because the keys stay stable even as the translations themselves change.

Translating validation messages

Laravel’s built-in validation messages are also stored in language files. Fill out resources/lang/tr/validation.php with Turkish messages and form errors will appear in Turkish automatically. Instead of writing these files by hand, you can take advantage of community-maintained translation packages that are available as Composer dependencies.

One thing I pay attention to: for project-specific error messages, it is cleaner to use the messages() method on FormRequest classes rather than adding them to the global validation file. Once that general file grows too large, it becomes hard to tell which messages you defined yourself versus which came from the framework.

Date and number formatting

In a multilingual application, language files alone are not enough — dates and numbers need to be localized as well. The Carbon library ships with locale support:

<?php

Carbon::setLocale('tr');
echo $event->starts_at->diffForHumans(); // outputs a localized expression instead of "3 hours ago"

For number formatting, PHP’s NumberFormatter class or a simple helper function does the job.

Conclusion

When i18n support is added from the start, the overhead is minimal — it is just a matter of getting into the habit of writing strings as language file keys instead of literal text. Adding it retroactively, on the other hand, means scanning through all your view files. If there is any chance the project will need a second language down the road — and there usually is — building that habit early saves a lot of time later.

Tags: #Laravel
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind