Programming Intermediate 11 min

How to Add Multilingual Support to a Laravel App

Building a Laravel app that works in multiple languages is straightforward once you understand where each piece lives: URL prefixes handle routing, translation files handle UI strings, a middleware sets the active locale, and spatie/laravel-translatable handles multilingual database content.

This guide covers the full stack — from the URL structure down to translatable Eloquent columns — and includes RTL support for Arabic and other right-to-left languages.

The approach described here scales to any number of languages without changing your controllers or business logic.

Step-by-step

  1. 1

    Define supported locales in config

    Create or update config/app.php to list the locales you support. Adding a custom supported_locales key keeps your route and middleware logic DRY — they read from config instead of hardcoding language codes.

    php
    // config/app.php
    'locale' => 'en',
    'fallback_locale' => 'en',
    'supported_locales' => ['en', 'ar'],
  2. 2

    Wrap routes in a locale prefix

    In routes/web.php, wrap all your public routes in a route group that uses {locale} as a URL prefix. This gives you clean, SEO-friendly URLs like /en/about and /ar/about without any query-string hacks.

    php
    // routes/web.php
    Route::prefix('{locale}')
        ->middleware(['setLocale'])
        ->group(function () {
            Route::get('/', [HomeController::class, 'index'])->name('home');
            Route::get('/about', [AboutController::class, 'index'])->name('about');
            // ... all other public routes
        });
  3. 3

    Create the SetLocale middleware

    This middleware reads the {locale} URL segment, validates it against your supported locales, and calls App::setLocale(). It also stores the locale in the session so the language switcher can read the current selection.

    bash
    php artisan make:middleware SetLocale
  4. 4

    Write the middleware logic

    Open app/Http/Middleware/SetLocale.php and add the locale-setting logic. If the locale in the URL is not in your supported list, fall back to the default rather than throwing a 404.

    php
    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\App;
    
    class SetLocale
    {
        public function handle(Request $request, Closure $next)
        {
            $locale = $request->route('locale');
            $supported = config('app.supported_locales', ['en']);
    
            if (in_array($locale, $supported)) {
                App::setLocale($locale);
                session(['locale' => $locale]);
            } else {
                App::setLocale(config('app.locale'));
            }
    
            return $next($request);
        }
    }
  5. 5

    Create translation files

    Laravel loads translation strings from resources/lang/{locale}/. Create one PHP file per logical group — for example common.php for shared UI strings. Use the __('common.save') helper anywhere in your code or Blade templates.

    php
    // resources/lang/en/common.php
    return [
        'save'       => 'Save',
        'cancel'     => 'Cancel',
        'welcome'    => 'Welcome, :name',
        'page_title' => 'My App',
    ];
    
    // resources/lang/ar/common.php
    return [
        'save'       => 'حفظ',
        'cancel'     => 'إلغاء',
        'welcome'    => 'مرحبًا، :name',
        'page_title' => 'تطبيقي',
    ];
  6. 6

    Install spatie/laravel-translatable for database content

    Static UI strings live in translation files, but content stored in the database — blog titles, product names, page content — needs a different approach. spatie/laravel-translatable stores each translatable column as a JSON object keyed by locale, and gives you a clean API to get and set values by locale.

    bash
    composer require spatie/laravel-translatable
  7. 7

    Make an Eloquent model translatable

    Add the HasTranslations trait to any model with multilingual columns, and list those columns in the $translatable array. The migration stores these as json columns. When you call $post->title, Spatie returns the value for the currently active locale automatically.

    php
    <?php
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Model;
    use Spatie\Translatable\HasTranslations;
    
    class Post extends Model
    {
        use HasTranslations;
    
        public array $translatable = ['title', 'slug', 'content'];
    }
    
    // Migration
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->json('title');
        $table->json('slug');
        $table->json('content');
        $table->timestamps();
    });
    
    // Writing translations
    $post->setTranslation('title', 'en', 'Hello World');
    $post->setTranslation('title', 'ar', 'مرحبا بالعالم');
    $post->save();
    
    // Querying by a specific locale
    Post::where('slug->en', 'hello-world')->first();
  8. 8

    Add a language switcher component

    Build a Blade component that generates locale-switching links by swapping the current URL's locale segment. The route() helper and the current route name make this clean.

    html
    {{-- resources/views/components/language-switcher.blade.php --}}
    @foreach(config('app.supported_locales') as $lang)
        @php
            $params = array_merge(request()->route()->parameters(), ['locale' => $lang]);
            $url = route(request()->route()->getName(), $params);
        @endphp
        <a href="{{ $url }}" class="{{ App::getLocale() === $lang ? 'active' : '' }}">
            {{ strtoupper($lang) }}
        </a>
    @endforeach
  9. 9

    Handle RTL layout for Arabic

    Arabic and other RTL languages need the HTML dir attribute flipped to rtl. Set this in your main Blade layout based on the active locale. You can then use CSS attribute selectors like [dir="rtl"] to apply mirrored styles without duplicating your entire stylesheet.

    html
    {{-- resources/views/layouts/app.blade.php --}}
    @php
        $rtlLocales = ['ar'];
        $isRtl = in_array(App::getLocale(), $rtlLocales);
    @endphp
    
    <html lang="{{ App::getLocale() }}" dir="{{ $isRtl ? 'rtl' : 'ltr' }}">

Tips & gotchas

  • Always generate locale-aware URLs using the `route()` helper with the `locale` parameter — never hardcode `/en/` or `/ar/` in your Blade templates.
  • For the slug column specifically, query it with the JSON arrow syntax: `where("slug->{$locale}", $value)` — Spatie does not override `where()`, so you need the raw JSON path.
  • Use `App::getLocale()` in your controllers to conditionally load locale-specific data; never rely on `session('locale')` alone because the session may not be set on the first request.
  • Keep your translation files organised by feature, not by page — `auth.php`, `common.php`, `blog.php` — so they stay manageable as the app grows.

Wrapping up

Your Laravel app now supports multiple languages end-to-end: URL-based locale routing, UI strings via translation files, and database content via Spatie's translatable columns, with proper RTL handling for Arabic. From here, consider adding a locale redirect at / based on the user's Accept-Language header, or persisting the locale preference in the user's profile.

#Laravel #i18n #Spatie
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.