Let's Connect

A developer's screen showing source code for a multi-language Laravel application

Laravel localization breaks down the moment you treat it as an afterthought. I have shipped apps serving English and Bengali (bn) to users across two regions, and the failures are always the same: hardcoded strings in Blade, a locale that resets on every request, and Bengali text that renders as boxes because nobody picked a font that supports the script. The fix is a small set of disciplined choices: put every string in lang/ files, resolve the locale once in middleware from the user's preference or region, and configure Carbon and a fallback so nothing ever renders blank or wrong. Get those three right and adding a third or fourth language is an afternoon, not a rewrite.

Where do translation files live, and which format do I use?

In Laravel 11 and 12 the lang/ directory is not published by default. Run php artisan lang:publish to scaffold it. You then have two formats, and you should use both for different jobs. PHP array files under lang/{locale}/ are keyed by a short identifier and are best for structured strings you reference by name (validation, navigation, emails). JSON files at lang/{locale}.json are keyed by the full English source string and are best for one-off UI copy where inventing a key is more friction than value.

lang/en/messages.php
<?php

return [
    'welcome' => 'Welcome back, :name',
    'invoices' => [
        'title' => 'Your invoices',
        // trans_choice pluralization: {0} ... | [1,*] ...
        'count' => '{0} No invoices|{1} :count invoice|[2,*] :count invoices',
    ],
];
Resolving strings in PHP/Blade
// Keyed array file, dot notation, with replacement
echo __('messages.welcome', ['name' => $user->name]);

// JSON file, keyed by the English source string
echo __('Save changes');

// Pluralization — Laravel picks the segment by count
echo trans_choice('messages.invoices.count', $invoices->count(), [
    'count' => $invoices->count(),
]);

The hard rule: no literal user-facing text in Blade, ever. Not in a button label, not in a flash message, not in an email subject. The day you decide to add Bengali, every hardcoded string becomes a manual hunt through templates. Wrap them in __() from day one. The corresponding lang/bn/messages.php and lang/bn.json simply mirror the keys with translated values.

How do I resolve the locale per user and per region?

Laravel does not change the locale per request on its own. App::getLocale() returns whatever config('app.locale') says until you override it. You want a single middleware that decides the active locale in a clear priority order: an authenticated user's saved preference wins, then a route prefix or subdomain (useful when /bn or bn.example.com maps to a region), then the browser's Accept-Language header, and finally the configured default. Resolve it once, call App::setLocale(), and the rest of the request — translations, validation, Carbon — follows.

app/Http/Middleware/SetLocale.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;

class SetLocale
{
    private const SUPPORTED = ['en', 'bn'];

    public function handle(Request $request, Closure $next): Response
    {
        // getPreferredLanguage() always returns a value from the
        // supported list (the first entry as a fallback), so this
        // chain never resolves to null.
        $locale =
            $request->user()?->locale
            ?? $this->fromPrefix($request)
            ?? $request->getPreferredLanguage(self::SUPPORTED);

        if (! in_array($locale, self::SUPPORTED, true)) {
            $locale = config('app.fallback_locale');
        }

        App::setLocale($locale);

        return $next($request);
    }

    private function fromPrefix(Request $request): ?string
    {
        $segment = $request->segment(1);

        return in_array($segment, self::SUPPORTED, true) ? $segment : null;
    }
}

In Laravel 11 and 12 there is no app/Http/Kernel.php — you register middleware in bootstrap/app.php. Append it to the web group so it runs on every browser request after the session (and therefore the authenticated user) is available.

bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\SetLocale::class,
    ]);
})
Lines of structured code on a dark editor representing locale resolution logic in a middleware class
The locale decision lives in one place — middleware — so every translation, validation message, and date in the request agrees on the active language.

What does Bengali (bn) need that English does not?

Three things bite people specifically with Bengali. First, rendering: a default system font stack will not show Bengali conjuncts correctly, so ship a font that covers the script (Noto Sans Bengali is the safe default) and apply it for the bn locale. Second, dates and numbers: set Carbon's locale or your humanized dates stay entirely in English — but note Carbon translates the words, not the digits, so diffForHumans() under bn gives you '2 দিন আগে' with a Latin '2'. If you want native Bengali numerals (১,২,৩), reach for the Number helper, which localizes digits and currency for you. Third, the fallback. If a bn key is missing, you want it to quietly fall back to English rather than echo the raw translation key into the UI.

  • Set fallback_locale to 'en' in config/app.php so a missing bn string never renders as a bare key like messages.invoices.title.
  • Call Carbon::setLocale(app()->getLocale()) (or $date->locale('bn')) before formatting, otherwise diffForHumans() returns English text under a Bengali UI.
  • Carbon's bn locale translates words but leaves digits in Latin form; use the Number helper when you need native Bengali numerals.
  • Load a Bengali-capable webfont and scope it with :lang(bn) or an html[lang='bn'] selector so Latin-script pages are not penalized.
  • Set the <html lang> attribute from app()->getLocale() — screen readers and the browser font fallback both depend on it.
Locale-aware dates and numbers
use Carbon\Carbon;
use Illuminate\Support\Number;

Carbon::setLocale(app()->getLocale()); // 'bn'

// '2 দিন আগে' — words are Bengali, but the digit stays Latin
$invoice->created_at->diffForHumans();

// Native Bengali numerals + currency: '১,৫০০.০০৳'
Number::currency(1500, in: 'BDT', locale: app()->getLocale());
If a missing translation key ever reaches the screen, your localization is not done — it is just hiding the bug behind a fallback you forgot to configure.Md Raihan Hasan

How do I translate validation messages and database content?

Validation is the easy half. Laravel ships lang/en/validation.php, and Bengali messages drop into lang/bn/validation.php with the same keys; the framework picks them up automatically based on the active locale. Use the custom and attributes arrays to localize field names so an error reads naturally in Bengali rather than exposing your column names.

Database content is the hard half, because translatable model attributes do not belong in lang/ files. You have two sane options. A dedicated translations table keyed by (translatable_type, translatable_id, locale, key) gives you full control and works with any storage. Otherwise the spatie/laravel-translatable package stores each translatable column as a JSON map of locale to value and resolves it transparently against the active locale. Pick the package unless you have a reason to own the schema. Either way, cache the resolved strings — translation lookups on every page render add up, and I cover that in my Laravel performance optimization guide. Keep the resolution logic out of your controllers and models too; a thin translator service is far easier to test, which is the whole point of testable service classes in Laravel.

Localization is one of those features where the difference between a clean implementation and a painful one is decided in the first week, not the last. Resolve the locale once in middleware, never let a literal string into a Blade template, configure the fallback so missing keys degrade gracefully, and treat Bengali's font, digit, and Carbon requirements as first-class instead of patching them after a user reports boxes on their screen. Do that and the second language costs you translation effort — not an architecture rewrite.