How to Create a Dynamic Multi-Language Website in Laravel

Multi-language websites are trendy as they present the web page in different languages. Users can choose the language and the website can display information in the selected language. Laravel provides an easy way to implement multi-language for a website, it is called Laravel Localization.

In this topic, we will develop a Laravel 10 application that can work for multiple languages. Users can choose the language from a dropdown list and data will be displayed in the selected language. We will make it dynamic by storing language names and other data in a MySQL table in multiple languages.

Multi-language in Laravel Laravel localization

Download Source code from github.

How to setup Laravel Localization

Before developing this application, let us look at what Laravel 10 provides for multiple language implementations. Laravel 10 offers easy ways to retrieve strings for various languages using translation files for each language. It uses language files for each language under the 'lang' folder within the Laravel project. By default, Laravel 10 does not provide the 'lang' directory. We need to use the below artisan command to publish it.


php artisan lang:publish

Once this is done, we will have a 'lang\en' folder in the project root. Laravel provides language files for English(en) language by default. If we add another language, we should create a folder with the name as the language code under the 'lang' directory. For example, if we add French (fr), we should have a folder 'lang\fr' and all the translation files related to the French language should be placed under it.

The application's default language is set in config\app.php file. There you will see the Localization parameters as given below:


<?php
 'locale' => 'en',
 'fallback_locale' => 'en',

You can change the default language for an HTTP request using App::setLocale($locale); where $locale is the language code.

How to Create Laravel Translation File

A translation file is a .php file with key=>value pairs for translations. For example, for French, we can have a messages.php file under the 'lang\fr' directory. The contents of the file can be as given below:

For English (lang\en\messages.php)


<?php
return [
'title' => 'Use Multiple Languages in Laravel',
'submit' => 'Submit',
'cancel' => 'Cancel',
'edit' => 'Edit',
];

For French (lang\fr\messages.php)


<?php
return [
'title' => 'Utiliser plusieurs langues dans Laravel',
'submit' => 'Soumettre',
'cancel' => 'Annuler',
'edit' => 'Modifier',
];

Only the right side (values) will change for different languages, left side (keys) will remain the same.

There is another way to create the translation files. these are JSON files. In that case, each language will have a JSON file under the 'lang' directory, like en.json, fr.json, etc. In this topic, we will use only the first approach which uses the php files for translations.

How to Retrieve Translations

To use the translations on a web page, we will use the keys from the translation files; the values will be displayed by Laravel automatically. For example, if we want to display the title of a page, we will use the below line for the web page title in html.


{{__('messages.title')}}

We can also use trans() function to get the translated values.


{{trans('messages.title')}}

In case, a translation key does not exist, it will return the given key. I hope, by now you have an idea of how to set up Laravel Localization for different languages. Now, we will develop a multi-language website using Laravel 10 with MySQL database.

Develop a Multi-Language Website in Laravel 10

We will develop a website using Laravel Localization. We will store the languages in a MySQL table using a form and display the content in a language selected by the user. This will be dynamic because, for a new language, we do not need to change our code; only the language translation files are to be created for the newly added language.

What are we going to do?

  1. Create Laravel Project and Migration: We will create a Laravel 10 project. We will create the migrations for three custom tables, these are 'languages', 'categories' and 'category_translations'. The 'languages' table will store different languages and 'categories' and 'category_translations' will store Product Categories with translated data for multiple languages. We will add/update languages and Categories using the forms. The 'category_translations' table is to store category names and descriptions for each category and each language.
  2. Setup Laravel Localization: We will publish Laravel language files. We will set up Laravel Localization so that our website can work for multiple languages.
  3. Create a Top Menu and a form to add/update Languages: The Top Menu will have menu items for maintaining languages and Categories. Also, it will display a list of active languages in a dropdown. We will add English(en), French(fr) and Hindi(hi).
  4. Create a form to add/update product categories in multiple languages: This form will have multiple tabs for different languages so that we can enter category names and descriptions in multiple languages and submit the form.
  5. Add more Languages without changing the code: Finally, we will add more languages and create translate files. We do not have to change our code for a new language, we only have to create the translation files for that language.

Let's start working on the project.

Create Laravel Project and Migration

Let us create a project in Laravel 10. Make sure you have Composer and PHP installed in your system. We will use the below command to create the project, our project name is lara_lang.


composer create-project --prefer-dist laravel/laravel lara_lang

Create a database named 'lara_lang' in MySQL and update .env file for database details. Update the file as below:


DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lara_lang
DB_USERNAME=root
DB_PASSWORD=

We will create three custom tables

  1. Table: LANGUAGES

    This table will store all languages. Along with the name and code, it will have a status column to make a language Active/Inactive. Using a form, we will add and update languages.

  2. Table: CATEGORIES

    This table will store all product categories. All non-translatable fields can be stored in this table. In our case, we will have only the ID column in this table. Translatable fields such as category name and description will be stored in a translate table as described next.

  3. Table: CATEGORY_TRANSLATIONS

    This table is the translation table for all categories. It will store category names and descriptions in different languages. It will have Category ID and Language Code as the foreign keys.

We will create the migrations for these tables. Go to your project folder and run the below command to create migration using the VS code terminal.

Create Language Model with migration


php artisan make:model Language -m

Create Category Model with migration


php artisan make:model Category -m

Create CategoryTranslation Model with migration


php artisan make:model CategoryTranslation -m

The above three commands will create the migration files as given below:

<yyyy_mm_dd_xxxxxx>_create_languages_table.php
<yyyy_mm_dd_xxxxxx>_create_categories_table.php
<yyyy_mm_dd_xxxxxx>_create_category_translations_table.php
They will be created under the database/migrations folder. The three models, named "Language", "Category" and CategoryTranslation will be created under the app/Models folder. Here yyyy_mm_dd_xxxxxx is the datetime when the migration files were created.

Open each migration file and Update the up() methods as below:

Migration scripts for 'languages' table


public function up(): void
    {
        Schema::create('languages', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('code')->unique();
            $table->tinyInteger('status')->comment('0-not active, 1-active');
            $table->timestamps();
        });
    }

Language code is unique, example, 'en' (English), 'fr'(French), 'es'(Spanish), 'ar'(Arabic), 'hi'(Hindi).

Migration scripts for categories table


public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

We are not adding any columns here, we are keeping only default columns. You can add only non-translatable columns in this table. Translatable columns should be placed in the translation table which is given next.

Migration scripts for category_translations table


public function up(): void
    {
        Schema::create('category_translations', function (Blueprint $table) {
            $table->id();
            $table->foreignID('category_id')->index();
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
            $table->string('language_code');
            $table->foreign('language_code')->references('code')->on('languages')->onDelete('cascade')->onUpdate('cascade');
            $table->string('name');
            $table->string('descr');
            $table->timestamps();
        });
    }

This translation table will store category names and descriptions in various languages. category_id and language_code are the foreign keys.

Let us now run the Laravel migration to create the custom and the Laravel default tables. Run the below command from the project folder to create the tables:


php artisan migrate

After running the migration, the below tables are created, and our custom tables are marked.

laravel migration for multi-languages

Update the Models

Let us update the models to add $fillable columns and the relationship functions wherever applicable.

Model Language

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Language extends Model
{
    use HasFactory;
    protected $fillable = ['name','code','status'];
}
Model Category

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;
   
    public function cat_translation(){
        return $this->hasMany(CategoryTranslation::class);
    }
}

There is a hasMany relationship between Category and CategoryTranslation, we will use this function whenever we want to retrieve category details.

Model CategoryTranslation

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class CategoryTranslation extends Model
{
    use HasFactory;
    protected $fillable = ['category_id','language_code','name', 'descr'];
}

Setup Laravel Localization

We need to publish Laravel's language files by using the artisan command. Run the below command from the terminal to create the 'lang' directory and the default language files.


php artisan lang:publish

This will create the 'lang\en' directory with the Laravel supplied files for English as below :

Multi-language website in Laravel

There are two localization parameters defined in config/app.php file. These are 'locale' and 'fallback_locale' and they are set as 'en' (English).


<?php
 'locale' => 'en',

 'fallback_locale' => 'en',

Now, we will create our translation file for the website under the 'lang/en' directory, let us name the file site.php. As of now, we will create for English only, later when we add more languages, we will add site.php for each language.

Let us write the title of the website in this file, for the time being:

lang/en/site.php


<?php
return [
'title' => 'Use Multiple Languages in Laravel',
];

Create a Top Menu and a form to add/update the languages

We will create a Top Menu with a dropdown to show the list of languages. Also, we will develop a form to add/update the languages.

To set the current language, we will use middleware. All the routes will go through this middleware. Our menu will look like below:

Multi-language website in Laravel

Create a middleware to set the language.

Create the middleware named LanguageMiddleware using the below command:


php artisan make:middleware LanguageMiddleware

This middleware will set the current language from the session to App locale. It will also share the current language and list of all other active languages using view share so we can use them in the menu. Let's see the code for the middleware:

app/Http/Middleware/LanguageMiddleware.php


<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

use Illuminate\Support\Facades\Session;
use App\Models\Language;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\App;

class LanguageMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param    \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        if (!Session::has('locale')){
            Session::put('locale',App::getLocale());
        }
        App::setLocale(session('locale'));

        $currLang = Language::where('code',session('locale'))->where('status', 1)->first();

        // Languages other than current selcetd language
        $languagesNotCurrent = Language::where('code','!=',session('locale'))
                                ->where('status', 1)->get();
       
        View::share('clang', $currLang);
        View::share('languages', $languagesNotCurrent);

        return $next($request);
    }
}

We are setting the session locale from the App locale if the session locale is not set. Otherwise, we are setting the App locale from the session. Then we get the current language details from the Language model using the Session locale. Also, we are getting a list of all languages other than the current language to use in the dropdown. These are all required in our view, and that is why, we are using view::share here.

Update Kernel.php to add the Middleware alias.


<?php
protected $middlewareAliases = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
        'signed' => \App\Http\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'lang' => \App\Http\Middleware\LanguageMiddleware::class,
    ];

Create Controllers

We will create two controllers - one for the Home page and the other, a resource controller, to add/update the languages.


php artisan make:controller HomeController

php artisan make:controller LanguageController

HomeController Methods

The method setLanguage() will set the selected language in the session and App locale. This function is called when a user selects a language from the dropdown.

app/Http/Controllers/HomeController.php


class HomeController extends Controller
{
    public function setLanguage($lang)
    {
        Session::put('locale',$lang);
        App::setLocale($lang);
        return redirect()->back(); 
    }
  public function index(){
    return view('index');
  }
}

LanguageController Methods

The LanguageController is used to add and update languages using the Language model. There will be standard methods that will be used here - create(), store(), edit() and update(). I am not going to explain in detail about these methods. The only change we have to make here is whenever we want to show any message, we have to use translate files. Our language file is lang/en/site.php, so, we will update this as per the requirements. See the below methods:

app/Http/Controllers/LanguageController.php


<?php
public function index()
    {
        $lang = Language::orderBy('id', 'asc')->get();
        return view('language.index', compact('lang'));
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('language.add_language');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'code' => 'required|unique:languages',
            'status' => 'required',
            ],
            [
              'name.required' => trans('site.lang.name_required'),
              'code.required' => trans('site.lang.code_required'),
              'status.required' => trans('site.lang.status_required'),
              'code.unique' => trans('site.lang.code_unique'),
          ],
        );

      try {
        Language::create($request->only('name', 'code', 'status'));
        return redirect()->route('languages.index')->withSuccess(trans('site.lang.add_success'));
      } catch (\Throwable $th) {
        // return back()->withError($th->getMessage())->withInput();
        return back()->withError(trans('site.lang.add_error'))->withInput();
      }

   }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id)
    {
        $edit = Language::findOrFail($id);
        return view('language.add_language', compact('edit'));
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id)
    {
        $request->validate([
          'name' => 'required',
          'code' => 'required|unique:languages,code,'.$id,
          'status' => 'required',
          ],
          [
            'name.required' => trans('site.lang.name_required'),
            'code.required' => trans('site.lang.code_required'),
            'status.required' => trans('site.lang.status_required'),
            'code.unique' => trans('site.lang.code_unique'),
        ],);

      try {
        Language::where('id', $id)->update($request->only('name', 'code', 'status'));
        return redirect()->route('languages.index')->withSuccess(trans('site.lang.update_success'));
      } catch (\Throwable $th) {
        return back()->withError($th->getMessage())->withInput();
        // return back()->withError(trans('site.lang.update_error'))->withInput();
      }
    }

You can see we are not writing the actual messages, but we are using keys defined in the translate file site.php which is given below:

lang/en/site.php


<?php
return [
'title' => 'Use Multiple Languages in Laravel',
'home_text' => 'This Tutorial is about implementing Multiple Languages in Laravel 10. You can maintain languages in a database table and also store and view data in different languages.',
'home' => 'Home',
'maintain_lang' => 'Maintain Languages',
'maintain_cat' => 'Maintain Categories',
'submit' => 'Submit',
'cancel' => 'Cancel',
'edit' => 'Edit',
'active' => 'Active',
'inactive' => 'Inactive',
'not_found' => 'No data Found',
'name' => 'Name',
'code' => 'Code',
'status' => 'Status',
'action' => 'Action',
'descr' => 'Description',

'lang' =>[
    'name' => 'Language Name',
    'code' => 'Language Code',
    'add' => 'Add Language',
    'update' => 'Update Language',
    'list' => 'List of Languages',
    'placeholder_name' => 'Enter Language Name',
    'placeholder_code' => 'Enter Language Unique Code',
    'name_required' => 'Language Name is required',
    'code_required' => 'Language Code is required',
    'status_required' => 'Status is required',
    'code_unique' => 'Language Code already exists',
    'update_success' => 'Language Updated Successfully',
    'update_error' => 'Error: could not update language, please contact Admin',
    'add_success' => 'Language Added Successfully',
    'add_error' => 'Error: could not Add language, please contact Admin',
    ],
];

These are all key=>value pairs. For another language you have to translate the values (right part) in that language, keys (left part) will remain the same.

Update the Routes

We will create one route for the home page, one for setting the locale when the user selects a language from the dropdown and a resource route to add/update Languages. All these routes should go through the LanguageMiddleware.

routes/web.php


<?php
Route::middleware(['lang'])->group(function() {
            Route::get('/', [HomeController::class, 'index']);
            Route::get('lang/{locale}', [HomeController::class, 'setLanguage'])->name('setlocal');

            Route::resource('languages', LanguageController::class);
  
});

Views

Below are the views for the menu and add/edit forms including the layouts. We have created two directories - 'layouts' and 'language' under 'views'. In all the views we will use translated values.

resources/views/layouts/header.blade.php


<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="{{asset('css/style.css')}}">
    <title>{{__('site.title')}}</title> 
</head>

See in line 2, we are using app()->getLocale() for the language. '_' is normally used for regions. For example, if en_GB or en_US is defined in the App locale, we need to change '_' (underscore) to '-' (hyphen) here using str_replace().

resources/views/layouts/footer.blade.php


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>

resources/views/layouts/top_menu.blade.php


<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="{{url('/')}}">Logo</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="{{url('/')}}">{{__('site.home')}}</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="{{route('languages.index')}}">{{__('site.maintain_lang')}}</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="{{route('categories.index')}}">{{__('site.maintain_cat')}}</a>
          </li>
        </ul>
        <div class="lang pull-right">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
                {{ $clang->name ?? '' }}
            </a>
            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                @foreach ($languages as $lang)
                <li> <a class="dropdown-item" href="{{ route('setlocal',['locale'=>$lang->code]) }}">{{ $lang->name }}</a></li>
                @endforeach
            </ul>
          </ul>
        </div>
      </div>
    </div>
  </nav>

Check the menu items, these are home, languages, and categories. At the end, the current selected language is displayed with a dropdown for all other active languages. Note that the current language ($clang in line 23) and the $languages used in the for-loop are shared by the middleware using view share as shown earlier.

resources/views/layouts/master.blade.php


@include('layouts.header')

    @include('layouts.top_menu')
    
@yield('main-section')
@include('layouts.footer') </body> </html>

View to display list of Languages from the database

Laravel 10 dynamic multi-language website

The index view to show the above list of Languages is given below:

resources/views/language/index.blade.php


@extends('layouts.master')
@section('main-section')

<div class="table-responsive">
    <h1>{{__('site.maintain_lang')}}</h1>
    <div class="mb-3 mt-3 text-end"><a class="btn btn-primary" href="{{route('languages.create')}}">{{__('site.lang.add')}}</a></div>
    @if(session('success'))
    <div class="alert alert-success">{{session('success')}}</div>
    @endif
    <h2>{{__('site.lang.list')}}</h2>
    <table class="table table-bordered table-striped table-hover">
        <thead>
            <tr>
                <th>{{__('site.name')}}</th>
                <th>{{__('site.code')}}</th>
                <th>{{__('site.status')}}</th>
                <th>{{__('site.action')}}</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($lang as $row)
                
            <tr class="">
                <td>{{$row->name}}</td>
                <td>{{$row->code}}</td>
                <td>{{$row->status ? trans('site.active') : trans('site.inactive')}}</td>
                <td>
                    <a href="{{route('languages.edit', $row->id)}}" class="btn btn-primary {{session('locale') == $row->code ? 'disabled' : ''}}" >{{__('site.edit')}}</a></td>
            </tr>
            @empty
                <tr>
                    <td colspan=4>{{__('site.not_found')}}</td>
                </tr>
            @endforelse
           
        </tbody>
    </table>
</div>
@endsection

Note that h1, h2 and table header come from the translate files. Also, note that the Edit button is disabled for the current language. This is done to make sure that the current language is not updated as inactive.

View to add/update Language

We will be using the same view to add and update the languages. You can read the topic How to Use the Same Form for Create and Update in Laravel for more detail.

add languages in database for Multi-language in Laravel 10

resources/views/language/add_language.blade.php


<?php
@extends('layouts.master')
@section('main-section')
@if (isset($edit->id))
  <h1>{{__('site.lang.update')}}</h1>
@else
<h1>{{__('site.lang.add')}}</h1>
@endif
@if (isset($edit->id))
    <form class="form1" action="{{route('languages.update', $edit->id)}}" method="post">
        @method('PUT')
@else
    <form class="form1" action="{{route('languages.store')}}" method="post">
@endif
    @if(session('error'))
    <div class="alert alert-danger">{{session('error')}}</div>
    @endif
    @csrf
    <div class="mb-3">
      <label for="" class="form-label">{{__('site.lang.name')}}</label>
      <input type="text"
        class="form-control" name="name" id="name" placeholder="{{__('site.lang.placeholder_name')}}" value="{{old('name', isset($edit->id) ? $edit->name : '')}}">
        <div class="text-danger">@error('name') {{$message}}@enderror</div>
    </div>

    <div class="mb-3">
        <label for="" class="form-label">{{__('site.lang.code')}}</label>
        <input type="text"
          class="form-control" name="code" id="code" placeholder="{{__('site.lang.placeholder_code')}}" value="{{old('code', isset($edit->id) ? $edit->code : '')}}">
          <div class="text-danger">@error('code') {{$message}}@enderror</div>
      </div>
      <div class="mb-3">
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="status" id="inlineRadio1" value="1" {{isset($edit->id) ? ($edit->status ? 'checked' : '') : ''}}>
            <label class="form-check-label" for="inlineRadio1">{{__('site.active')}}</label>
          </div>
          <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" name="status" id="inlineRadio2" value="0" {{isset($edit->id) ? ($edit->status ? '' : 'checked') : ''}}>
            <label class="form-check-label" for="inlineRadio2">{{__('site.inactive')}}</label>
          </div>
          <div class="text-danger">@error('status') {{$message}}@enderror</div>
    </div>
      <input type="submit" class="btn btn-primary" value="{{__('site.submit')}}">
      <a href="{{route('languages.index')}}" class="btn btn-danger">{{__('site.cancel')}}</a>
</form>
@endsection

Form fields, labels, names and placeholders are taken from the translate file. Since we are using the same form for Add and Update, separate routes are used for the form action.

Here is the stylesheet:

public/css/style.css


h1,h2,h3, p{
    text-align: center;
    margin:20px 0 20px 0;
}
.navbar-brand {
margin: 0 100px 0 50px;
}
.navbar-nav .nav-item{
margin-left: 50px;
}
.navbar-nav .nav-item .nav-link{
font-size: 16px;
font-weight: 500;
color:#fff;
}
.lang{
    margin-right: 100px;
}
.table-responsive{
    width: 60% !important;
    margin:auto;
    text-align: center;
}
tr th {
background-color: #07072b!important;
color: #fff!important;
}
.form1{
    margin:auto;
    width:60%;
}

Start the server and add languages using the Add form.


php artisan serve

Run localhost:8000 in the browser, you will see the below screen.

Build a dynamic Multi-language website in Laravel

We have not added any languages yet. The language dropdown will be empty and we do not see the current language. Now click on Maintain Languages and add language, English (en). See below:

Multi-language in Laravel add language form
Localization Implementation in Laravel with Language

After submitting the form, we will see the below screen. You can now see English is displayed, but the dropdown is empty. So, we will add another language, French.

Laravel 10 Tutorial - Localization

Add French, use Google Translate to translate from one language to another.

Laravel translation files

If you submit, you will see the dropdown showing the French language and it is also added to the list. We cannot use French now, since we have not added French to the Laravel 'lang' directory. So, create a directory 'fr' under 'lang' and copy site.php from 'en' directory and paste it into 'fr' directory. Now you have to get the translated values for French in the site.php. Using Google Translate, we can replace English texts with corresponding French-translated texts.

Once that is done, we will have the below site.php under 'lang/fr' directory.


<?php
return [
'title' => 'Utiliser plusieurs langues dans Laravel',
'home_text' => 'Ce didacticiel concerne l\'implémentation de plusieurs langues dans Laravel 10. Vous pouvez gérer les langues dans une table de base de données et également stocker et afficher des données dans différentes langues.',
'home' => 'Maison',
'maintain_lang' => 'Maintenir les langues',
'maintain_cat' => 'Maintenir les catégories',
'submit' => 'Soumettre',
'cancel' => 'Annuler',
'edit' => 'Modifier',
'active' => 'Active',
'inactive' => 'Inactive',
'not_found' => 'Aucune donnée disponible',
'name' => 'Nombre',
'code' => 'Código',
'status' => 'Statut',
'action' => 'Acción',

'lang' =>[
    'name' => 'Nom de la langue',
    'code' => 'Code de langue',
    'placeholder_name' => 'Entrez le nom de la langue',
    'placeholder_code' => 'Entrez le code unique de la langue',
    'name_required' => 'Le nom de la langue est requis',
    'code_required' => 'Le code de langue est requis',
    'status_required' => 'Le statut est requis',
    'code_unique' => 'Le code de langue existe déjà',
    'update_success' => 'Langue mise à jour avec succès',
    'update_error' => "Erreur : impossible de mettre à jour la langue, veuillez contacter l'administrateur",
    'add_success' => 'Langue ajoutée avec succès',
    'add_error' => 'Erreur : impossible d\'ajouter la langue, veuillez contacter l\'administrateur',
    'add' => 'Ajouter une langue',
    'list' => 'Liste des langues',
    ],
];

If we now refresh the page and select French, you will see the below screen:

laravel localization tutorial

You can see all the texts are translated into the French Language. If you select English, it will display all in English.

Now you can add more languages and create the translate files for each language in a separate directory under the 'lang' directory. You can update the languages and make it Inactive. Only active languages will be shown in the dropdown.

I have added Hindi and created the translate file in lang\hi\site.php. See below:

Change app default locale dynamically

Create a form to add/update product categories in multiple Languages

Now we will create a form to add data in multiple languages. So, if there are 3 active languages, data will be stored in 3 languages in the database. Our Category and CategoryTranslation models are created for that.

Maintaining languages and product categories are Admin jobs, but users can view the product categories on the website. So, the admin has to enter the data in all supported languages for the website.

Add Category form will have separate tabs for each language, it will be created dynamically and all translatable fields will be under each tab. See below:

Using a database for localization in Laravel

We will create a resource controller named CategoryController, add a resource route for the category and create an add/edit form. Add this route in web.php within the middleware.

Resource route for category


Route::resource('categories', CategoryController::class);

CategoryController

Method index()

Since we have a one-to-many relationship between Category and CategoryTranslation we will use the cat_translation() function defined in the Category Model. This will give all categories with names and descriptions for the current language.


public function index()
    {
        $cat = Category::with(['cat_translation' => function($query){
          $query->where('language_code', App::getLocale());
        }])->get();

        return view('category.index', compact('cat'));
    }

Add/Edit form for categories

Our add category form will look like below.

Laravel 10 How To Create Multi Language Website Tutorial

You can see that multiple tabs are used for languages to enter category names and descriptions. So, we need to send all active languages to the view. We will also assume that English is the default language, so, the active tab will be for English. In the controller, we will define a variable for the active language, see the code below:


<?php
class CategoryController extends Controller
{
  protected $active_lang = 'en';
    public function create()
        {
            $all_languages = Language::where('status', '1')->orderBy('id', 'asc')->get();
            $active_lang = $this->active_lang;
            return view('category.add_category', compact('all_languages', 'active_lang'));
        }
}

We are sending a list of all active languages and the currently selected language to the view add_category.

CategoryController store() method

Note that the category name and description are the arrays in the form and are created dynamically, so after the form is submitted a single row is to be inserted into the Category Model and multiple rows (one row for each language code) are to be inserted into the CategoryTranslation model.


<?php
   public function store(Request $request)
    {
        $request->validate([
            'name.*' => 'required',
            'descr.*' => 'required',
        ]);

        $name = $request->name;
        $descr = $request->descr;

      DB::beginTransaction();
      try {
        $create = Category::create();
        $last_id = $create->id;
        foreach($name as $key=>$value){
          CategoryTranslation::create([
            'category_id' => $last_id,
            'language_code' => $key,
            'name' => $name[$key],
            'descr' => $descr[$key],
          ]);
        }
        DB::commit();

        return redirect()->route('categories.index')->withSuccess(trans('site.cat.add_success'));
      } catch (\Throwable $th) {
        DB::rollBack();
        return back()->withError(trans('site.cat.add_error'))->withInput();
      }
   }

You can see a row is inserted into the Category model and using a loop, multiple rows are inserted into the CategoryTranslation model. Note that we are displaying success and error messages from the language files.

CategoryController edit() method

We have to get the details of the category along with the translations. Then we will use two arrays, one for the category name and the other for the description using the language code as the array keys.


<?php
public function edit(string $id)
    {
        $edit = Category::with('cat_translation')->find($id);
        $active_lang = $this->active_lang;
        $all_languages = Language::where('status', '1')->orderBy('id', 'asc')->get();
        foreach ( $edit->cat_translation as $row ) {
          $edit_name[ $row->language_code ] = $row->name;
          $edit_descr[ $row->language_code ] = $row->descr;
         
      }
        return view('category.add_category', compact('edit','edit_name','edit_descr', 'active_lang', 'all_languages'));
    }

CategoryController update() method

update() method will be similar to the store() method, except we will use updateOrCreate() instead of create() method on the CategoryTranslation model. Here, only the translate table will be updated.


<?php
public function update(Request $request, string $id)
    {
      $request->validate([
        'name.*' => 'required',
        'descr.*' => 'required',
    ]);

    $name = $request->name;
    $descr = $request->descr;

    DB::beginTransaction();
    try {
    
      foreach($name as $key=>$value){
        CategoryTranslation::updateOrCreate(
          [
          'category_id' => $id,
          'language_code' => $key,
          ],
          [
          'name' => $name[$key],
          'descr' => $descr[$key],
          ]
        );
      }
      DB::commit();

      return redirect()->route('categories.index')->withSuccess(trans('site.cat.update_success'));
    } catch (\Throwable $th) {
      DB::rollBack();
      return back()->withError(trans('site.cat.update_error'))->withInput();
    }
  }

View to display the list of categories

In this view, we will display all categories in an html table. We will display the names and descriptions with an Edit button for each row. Note that we have to use the language files for all the texts. Below is the blade file:

resources/views/category/index.blade.php


@extends('layouts.master')
@section('main-section')

<div class="table-responsive">
    <h1>{{__('site.maintain_cat')}}</h1>
    <div class="mb-3 mt-3 text-end"><a class="btn btn-primary" href="{{route('categories.create')}}">{{__('site.cat.add')}}</a></div>
    @if(session('success'))
    <div class="alert alert-success">{{session('success')}}</div>
 @endif
    <h2>{{__('site.cat.list')}}</h2>
    <table class="table table-bordered table-striped table-hover">
       
        <thead>
            <tr>
                <th>{{__('site.name')}}</th>
                <th>{{__('site.descr')}}</th>
                <th>{{__('site.action')}}</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($cat as $row)
            @if (count($row->cat_translation))
            <tr>
                <td>{{$row->cat_translation->value('name')}}</td>
                <td>{{$row->cat_translation->value('descr')}}</td>
                <td>
                    <a href="{{route('categories.edit', $row->id)}}" class="btn btn-primary" >{{__('site.edit')}}</a>
                </td>
            </tr>
            @endif
            @empty
                <tr>
                    <td colspan=3>{{__('site.not_found')}}</td>
                </tr>
            @endforelse
           
        </tbody>
    </table>
</div>
@endsection

View to add/update category

We will be using the same form to add/edit category.

resources/views/category/add_category.blade.php


<?php
@extends('layouts.master')
@section('main-section')
@if (isset($edit->id))
  <h1>{{__('site.cat.update')}}</h1>
@else
  <h1>{{__('site.cat.add')}}</h1>
@endif
@if(isset($edit->id))
<form class="form1" action="{{route('categories.update', $edit->id)}}" method="post">
  @method('PUT')
@else
    <form class="form1" action="{{route('categories.store')}}" method="post">
@endif
    @if(session('error'))
    <div class="alert alert-danger">{{session('error')}}</div>
    @endif
    @if ($errors->any())
    <div class="alert alert-danger">{{__('site.cat.error_msg')}}</div>
@endif
    @csrf
    <!-- Nav tabs -->
    <ul class="nav nav-tabs">
      @foreach ($all_languages as $row )
      <li class="nav-item">
        <a class="nav-link {{$row->code == $active_lang ? 'active' : ''}}" data-bs-toggle="tab" href="#tab_{{$row->code}}">{{$row->name}}</a>
      </li>
      @endforeach
     
    </ul>

<!-- Tab panes -->
<div class="tab-content">
  @foreach ($all_languages as $row )
    <div class="tab-pane container {{$row->code == $active_lang ? 'active show' : ''}}" id="tab_{{$row->code}}">
      <div class="mb-3">
        <label for="" class="form-label">{{__('site.cat.name')}}</label>
        <input type="text"
          class="form-control" name="name[{{$row->code}}]" id="name" placeholder="{{__('site.cat.placeholder_name')}}" value="{{old('name.'.$row->code, (isset($edit_name) && !empty($edit_name[$row->code])) ? $edit_name[$row->code] : '')}}">
          <div class="text-danger"> @error('name.'.$row->code){{__('site.cat.name_required')}}@enderror</div>
      </div>
  
      <div class="mb-3">
          <label for="" class="form-label">{{__('site.cat.descr')}}</label>
          <input type="text"
            class="form-control" name="descr[{{$row->code}}]" id="code" placeholder="{{__('site.cat.placeholder_descr')}}" value="{{old('descr.'.$row->code, (isset($edit_descr) && !empty($edit_descr[$row->code])) ? $edit_descr[$row->code] : '')}}">
            <div class="text-danger"> @error('descr.'.$row->code){{__('site.cat.descr_required')}}@enderror</div>
        </div>

    </div>
  @endforeach
</div>    
      <input type="submit" class="btn btn-primary" value="{{__('site.submit')}}">
      <a href="{{route('categories.index')}}" class="btn btn-danger">{{__('site.cancel')}}</a>
</form>
@endsection

We are dynamically creating the tabs for each language and displaying the values in each tab for each language code for editing. Note that the name and description are the arrays with language code as the keys.

Now you can add product categories in multiple languages and also update them. I have added a few categories which are displayed in different languages as given below:

set up multiple languages for your web app

In French

create a multilingual project in Laravel 10

In Hindi

Building a multilingual site with Laravel Localization

Add more Languages without changing any code

You can add more languages, like Arabic (ar), Spanish(es), Bengali (bn) and create site.php for these languages. The application will work without modifying any code.

I added three more languages and created the site.php for each language. Add category form now shows more tabs as given below:

Managing Multilingual Content in Laravel

Download Source code from github.

Conclusion

In this topic we have covered three areas of Laravel Localization:

  • We have discussed how to set up Laravel Localization for Language Translation.
  • Then we developed a form to add/update Languages in the database for a dynamic multi-language website.
  • Then we developed a form to add/update product categories in multiple languages in the database. Also, we have seen how to display the data in multiple languages.