Localization trong Laravel


Để đáp ứng nhu cầu sử dụng của nhiều loại khách hàng, đa phần các website đểu phải sử dụng đa ngôn ngữ. Và vấn đề này được Laravel hỗ trợ và giải quyết vô cùng đơn giản.

Trong bài viết này mình sẽ giới thiệu các bạn cách để làm chức năng đa ngôn ngữ một cách dễ dàng và tốn ít thời gian nhất.
Đồng thời sẽ có một phần hướng dẫn nâng cao cho bạn nào hứng thú. Chúng ta sẽ làm đa ngôn ngữ cho cả database.

Giới thiệu

Laravel’s Localization cung cấp những cách để nhận và hiển thị chuỗi dưới nhiều ngôn ngữ, giúp dễ dàng sử dụng tính năng đa ngôn ngữ trong ứng dụng. Các chuỗi đa ngôn ngữ có thể được lưu dưới dạng file .php hoặc file json bên trong thư mục resources/lang . Ngôn ngữ mặc định của ứng dụng được lưu trữ trong file config/app.php với giá trị mặc định là locale = "en". Chúng ta có thể thay đổi trực tiếp file config hoặc sử dụng hàm setLocale của facade App:

Route::get('welcome/{locale}', function ($locale) {
    App::setLocale($locale);
    //
});

Bạn nên cấu hình “fallback language”, để khi ngôn ngữ đang active không chứa chuỗi dịch thì có thể sử dụng fallback này. Nó giống như kiểu ngôn ngữ mặc định vậy, bạn cấu hình trong file config/app.php:

'fallback_locale' => 'en',

Khai báo chuỗi dịch

Sử dụng file php

Đầu tiên, ta cần thêm các subfolder vào bên trong thư mục resources/lang cho những ngôn ngữ mà ứng dụng hỗ trợ. Ví dụ dưới đây mình tạo 2 subfolder là envi để chứa file ngôn ngữ của tiếng Anh và tiếng Việt:

/resources
    /lang
        /en
            messages.php
        /vi
            messages.php

Trong các subfolder đặt tên sao cho phù hợp với mục đích của nó, nội dung các file như sau:
resources\lang\en\messages.php

<?php

return [
    'welcome' => 'Welcome To My Website!',
    'field' => [
        'name' => 'Name',
    ],
];

resources\lang\vi\messages.php

<?php

return [
    'welcome' => 'Chào mừng bạn đến với website!',
    'field' => [
        'name' => 'Tên',
    ],
];

Từ đây bạn có thể sử dụng các key value trong file ngôn ngữ bằng cách sử dụng hàm helper __() của Laravel theo cú pháp: __(<Tên file> . <Tên key>). Ở ví dụ trên sẽ là: __('messages.welcome') . Trên file blade ta dùng {{ __('messages.welcome') }}, và kết quả phụ thuộc vào ngôn ngữ hiện tại của website. Đi vào phần tử con của mảng nếu có: __('messages.field.name') .

Đôi khi ta muốn truyền tham số vào chuỗi ngôn ngữ bằng cách sử dụng : trước giá trị muốn truyền vào. Bạn có thể định nghĩa welcome với 1 placeholder name như sau: 'welcome' => 'Welcome, :name'. Để thay thế placeholder khi hiển thị chuỗi, truyền vào mảng giá trị cần thiết: __('messages.welcome', ['name' => 'Nguyễn Quang Đạt']).

Sử dụng file JSON

Cách sử dụng file php thường được áp dụng trong các ứng dụng nhỏ lẻ, ít sử dụng và hiển thị nhiều ngôn ngữ. Nhưng với những ứng dụng, website lớn hơn thì sao? Với ứng dụng lớn hơn thì việc sử dụng đến đa ngôn ngữ rất nhiều, bởi vậy việc tạo các file với keyword ngắn trở nên bất tiện và khó nhớ hơn. Bởi vậy Laravel đã cung cấp thêm cho chúng ta một cách để viết đa ngôn ngữ dễ dàng hơn, đó là sử dụng file json thay vì file php.

Điểm khác nhau đầu tiên giữa 2 loại là file php cần tạo thư mục có tên tương ứng với tên ngôn ngữ và lấy nó là tên locale, Ngược lại với cách sử dụng json file thì chỉ cần tạo file json tương ứng với ngôn ngữ mà ứng dụng của bạn hỗ trợ là được.

Một điểm lợi nữa và là điểm mấu chốt khiến mình nghĩ cách này tiện lợi hơn rất nhiều đó là file php yêu cầu keyword ngắn và viết liền ( điều này khiến mỗi lần muốn viết lại phải đau đầu nghĩ sao cho keyword phù hợp và tối ưu ) trong khi file json chỉ cần sử dụng toàn bộ chuỗi, hoặc thậm chí đoạn văn làm keyword. Điều này khiến lập trình viên cảm thấy dễ chịu hơn nhiều bởi khi nhìn trên file blade ta cũng có thể biết được đoạn chuỗi đó nói về cái gì.
Để sử dụng cách này, ta cần phải tạo các file ngôn ngữ với đuôi .json như sau:

/resources
    /lang
        en.json
        vi.json

File resources/lang/en.json

{
    "Welcome to my website!": "Welcome to my website!"
}

File resources/lang/vi.json

{
    "Welcome to my website!": "Chào mừng bạn đến với website!"
}

Việc hiển thị ra blade thì vẫn dùng hàm helper __() tương tự như trên nha mình không nói lại nữa.

Sử dụng trong website

Ở trên mình đã nói qua về localization trong Laravel, sau đây mình xin phép hướng dẫn cách để làm chứ năng đa ngôn ngữ cho website sử dụng Session và middeware.
Đầu tiên bạn cần tạo 1 route để xử lí thực thi khi người dùng chuyển ngôn ngữ:

Route::get('change-language/{language}', [HomeController::class, 'changeLanguage'])->name('change-language');

Trong hàm changeLanguage khai báo như sau:

$language = $request->language;

Session::put('language', $language);
return redirect()->back();

Tiếp theo bạn tạo 1 middleware tên là Localization thông qua câu lệnh: php artisan make:middleware Localization. Nhớ thêm vào Kelnel.php middleware mình vừa tạo:

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    ...
    'localization' => \App\Http\Middleware\Locale::class,
];

Trong file middleware Localization.php

class Locale
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $language = Session::get('language', config('app.locale'));

        App::setLocale($language);
        
        return $next($request);
    }
}

Rồi đặt tất cả các route cần sử dụng đa ngôn ngữ vào bên trong middeware này:

Route::middleware('localization')->group(function () {
    Route::get('/', function () {
        return view('welcome');
    });
    Route::get('change-language/{language}', [HomeController::class, 'changeLanguage'])->name('change-language');
});

Vậy là xong . Việc cuối cùng bạn cần làm là thêm đường link dẫn tới trang thay đổi ngôn ngữ hoặc selection dẫn tới trang mong muốn là được:

English
Việt Nam

Đa ngôn ngữ Siêu cấp Cực mạnh

Đoạn phía trên là phần căn bản để xây dựng website đa ngôn ngữ cho các label sẵn trên website của chúng ta. Vậy còn phần dữ liệu thì sao? Không lẽ giờ đa ngôn ngữ rồi nó thành: Title: Đa ngôn ngữ trong Laravel. Bây giờ mình sẽ hướng dẫn các bạn đa ngôn ngữ trong database.
Ở đây có nhiều cách để làm đa ngôn ngữ đơn giản hơn như dùng thư viện translate tự động bla bla… nhưng nó không theo ý mình muốn dược. Và ở đây ta hoàn toàn chủ động được nhang. Bắt tay ngay nào.

Yêu cầu

  • Mysql 5.7.8 trở lên (miễn sao hỗ trợ kiểu dữ liệu dạng json)

Chúng ta cài đặt package qua composer:

composer require spatie/laravel-translatable mcamara/laravel-localization

Và ide-helper hay debugbar nếu muốn nhá:

composer require --dev barryvdh/laravel-ide-helper barryvdh/laravel-debugbar

Tạo bảng đa ngôn ngữ

Sử dụng command:

php artisan make:migration create_multilang_table --create=multilang
class CreateMultilangTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('multilang', function (Blueprint $table) {
            $table->id();
            $table->json('name');
            $table->json('description');
            $table->json('content');
            $table->string('type');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('multilang');
    }
}

Ở đây mình sẽ ứng dụng đa ngôn ngữ cho 3 field name, description, content và file type không đa ngôn ngữ để chúng ta thấy sự khác biệt.

Tạo model Multilang:

php artisan make:model Multilang
<?php

namespace App\Models;

use App\Traits\TranslatableTrait;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Multilang extends Model
{
    use HasFactory;
    use TranslatableTrait;

    protected $table = 'multilang';

    protected $fillable = [
        'name',
        'description',
        'content',
        'type'
    ];

    public $translatable = [
        'name',
        'description',
        'content',
    ];
}

Những field nào sử dụng đa ngôn ngữ thì khai báo trong $translatable.
use TranslatableTrait; ở đâu ra? Bây giờ mình sẽ tạo file TranslatableTrait.php:

Tạo Trait để sử dụng ở nhiều model

Trong folder app tạo folder Traits, sau đó tạo file TranslatableTrait.php trong folder này. Sau đó copy code này vào:

<?php

namespace App\Traits;

use Spatie\Translatable\HasTranslations as BaseHasTranslations;

trait TranslatableTrait
{
    use BaseHasTranslations;

    /**
     * Encode the given value as JSON.
     *
     * @param  mixed  $value
     * @return string
     */
    protected function asJson($value)
    {
        return json_encode($value, JSON_UNESCAPED_UNICODE);
    }

    protected function getLocale(): string
    {
        return request('edit_locale') ?: config('app.locale');
    }

    public function getTranslatableAttributes(): array
    {
        return is_array($this->translatable)
            ? $this->translatable
            : [];
    }

    public function setAttribute($key, $value)
    {
        // Pass arrays and untranslatable attributes to the parent method.
        if ((!$this->isTranslatableAttribute($key) || is_array($value)) ) {
            return parent::setAttribute($key, $value);
        }

        // If the attribute is translatable and not already translated, set a
        // translation for the current app locale.
        return $this->setTranslation($key, $this->getLocale(), $value);
    }
}

Cuối cùng tronng bước setup, chúng ta publish config laravellocalization, ta chạy command:

php artisan vendor:publish

Sau đó nhập vị trí của Mcamara\LaravelLocalization\LaravelLocalizationServiceProvide vào và enter.

Sau đó sẽ có file config/laravellocalization.php được publish và mở command những ngôn ngữ mà bạn muốn sử dụng trong array supportedLocales . Ở đây mình chỉ sử dụng tiếng Anh và tiếng Việt
Cuối file có option hideDefaultLocaleInURL mặc định là false. Chuyển thành true nếu bạn muốn ẩn ngôn ngữ mặc định của ứng dụng trên URL.
Ví dụ như ứng dụng của ta set locale = "vi" thì đường dẫn sẽ có dạng:

// English
domain/en/xyz.html 

// Việt Nam
domain/xyz.html

Khai báo Route và Controller

Mở file routes/web.php và thêm một số route để demo:

use App\Http\Controllers\MultilangController;

Route::prefix('admin')->group(function() {
    Route::get('multi/create', [MultilangController::class, 'formMulti']);

    Route::post('store-multi', [MultilangController::class, 'storeMulti'])
        ->name('admin.multi.store');

    Route::get('multi/{id}', [MultilangController::class, 'editMulti']);

    Route::put('multi/{id}', [MultilangController::class, 'updateMulti'])
        ->name('admin.multi.update');
});
// Không cần use LaravelLocalization
Route::prefix(LaravelLocalization::setLocale())->group(function() {
    Route::get('multi/{id}', [MultilangController::class, 'detail']);
});

Tạo file MultilangController.php bằng command:

php artisan make:controller MultilangController

Mở và khai báo các function cho MultilangController.

<?php

namespace App\Http\Controllers;

use App\Models\Multilang;
use Illuminate\Http\Request;

class MultilangController extends Controller
{
    public function formMulti()
    {
        return view('multiform');
    }

    public function storeMulti(Request $request)
    {
        Multilang::create($request->all());
        return redirect()->back();
    }

    public function detail($id)
    {
        $item = Multilang::find($id);
        return view('multidetail', compact('item'));
    }

    public function editMulti($id)
    {
        $item = Multilang::find($id);
        return view('editmultidetail', compact('item'));
    }

    public function updateMulti(Request $request, $id)
    {
        $item = Multilang::find($id);
        $item->update($request->all());

        return redirect()->back();
    }
}

Tạo các View

Tạo 3 file blade trong folder resources/views. Ở đây mình chỉ demo nên mình dùng thuần html. Nên nó sẽ xấu nhưng đẹp xấu không phải là cái mình cần ở bài này, nên các bạn thông cảm:

File resources/views/multiform.blade.php

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Create multi lang</title>
</head>
<body>
Lang:
    @foreach(LaravelLocalization::getLocalesOrder() as $localeCode => $properties)
        @if(get_current_edit_locale() == $localeCode)
            <strong>
                {{ $properties['native'] }}
            </strong>
            ( Editting )
        @endif
    @endforeach
<br> <br>
Other: <br>
    @foreach(LaravelLocalization::getLocalesOrder() as $localeCode => $properties)
        @if(get_current_edit_locale() != $localeCode)
            <a href="{{ Request::fullUrlWithQuery(['edit_locale' => $localeCode]) }}">
                {{ $properties['native'] }} add
            </a> <br>
        @endif
    @endforeach

<br>
<form action="{{ route('admin.multi.store') }}" method="post">
    @csrf
    <input type="hidden" name="save_locale" value="{{ request('edit_locale') }}">

    <input type="text" name="name" placeholder="Name"><br>
    <input type="text" name="description" placeholder="Description"><br>
    <input type="text" name="content" placeholder="Content"><br>
    <input type="text" name="type" placeholder="Type"><br>
    <button type="submit">Create</button>
</form>
</body>
</html>

File resources/views/multidetail.blade.php

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Multi detail</title>
</head>
<body>

    @foreach(LaravelLocalization::getLocalesOrder() as $localeCode => $properties)
        <li>
            <a href="{{ LaravelLocalization::localizeURL(Request::path(), $localeCode) }}">
                {{ $properties['native'] }}
            </a>
        </li>
    @endforeach
<br><br>
    {{ $item->name }} <br>
    {{ $item->description }} <br>
    {{ $item->content }} <br>
    {{ $item->type }} <br>
</body>
</html>

File resources/views/editmultidetail.blade.php

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Update multi lang</title>
</head>
<body>
<strong>"{{ get_current_edit_locale_name() }}"</strong> <br>
Lang:
@foreach(LaravelLocalization::getLocalesOrder() as $localeCode => $properties)
    @if(get_current_edit_locale() == $localeCode)
        <strong>
            {{ $properties['native'] }}
        </strong>
        ( Editting )
    @endif
@endforeach
<br> <br>
Other: <br>
@foreach(LaravelLocalization::getLocalesOrder() as $localeCode => $properties)
    @if(get_current_edit_locale() != $localeCode)
        <a href="{{ Request::fullUrlWithQuery(['edit_locale' => $localeCode]) }}">
            {{ $properties['native'] }} edit
        </a> <br>
    @endif
@endforeach

<br>
<form action="{{ route('admin.multi.update', $item->id) }}" method="post">
    @csrf
    @method('put')
    <input type="hidden" name="save_locale" value="{{ request('edit_locale') }}">

    <input type="text" name="name" value="{{ $item->name }}" placeholder="Name"><br>
    <input type="text" name="description" value="{{ $item->description }}" placeholder="Description"><br>
    <input type="text" name="content" value="{{ $item->content }}" placeholder="Content"><br>
    <input type="text" name="type" value="{{ $item->type }}" placeholder="Type"><br>
    <button type="submit">Update</button>
</form>
</body>
</html>

Tạo file helper.php

Trong mí file blade mình sử dụng một số hàm helper. Thì mình khai báo helper thoi làm gì căng :))
Tạo file helper.php trong folder app sau đó thêm code vào:

<?php

use Illuminate\Support\Facades\App;
use Illuminate\Support\Str;

if (!function_exists('get_current_edit_locale')) {
    /**
     * Get current edit locale
     *
     * @return string
     */
    function get_current_edit_locale()
    {
        return request('edit_locale') ?: App::getLocale();
    }
}

if (!function_exists('get_current_edit_locale_name')) {
    /**
     * Get current edit locale name
     *
     * @return string
     */
    function get_current_edit_locale_name()
    {
        $locale = get_current_edit_locale();

        $supportedLocales = config('laravellocalization.supportedLocales') ?: [];

        if (isset($supportedLocales[$locale])) {
            return $supportedLocales[$locale]['native'].' ('.$supportedLocales[$locale]['name'].')';
        }

        return Str::upper($locale);
    }
}

Tới đây helper vẫn chưa hoạt động đâu các bạn. Mình phải khai báo nó vào providers nữa. Mở file app/Providers/AppServiceProvider.php và nạp nó vào:

public function register()
{
    //
    require_once __DIR__.'/../helpers/helpers.php';
}

Vẫn chưa xong đâu các bạn, còn xíu nữa thoi.

Khai báo Middleware

Tạo một middleware để dùng cho việc update dữ liệu đa ngôn ngữ các bạn ạ:

php artisan make:middleware SaveLocaleMiddleware

Sau đó mở file app/Http/Middleware/SaveLocaleMiddleware.php và thêm đoạn code vào:

public function handle($request, Closure $next)
{
    if ($request->method() != 'GET' && ($locale = $request->input('save_locale'))) {
        \App::setLocale($locale);
    }

    return $next($request);
}

Lưu ý $request->input('save_locale') chính là field truyền trên form phía trên. Dựa vào đó sẽ cập nhật dữ liệu chính xác theo ngôn ngữ mà phía admin lựa chọn.

Sau đó push vào group middleware web trong provider. Mở file app/Providers/AppServiceProvider.php và thêm code vào:

public function boot()
{
    $this->bootMailConfig();
    $this->registerMiddleware();
}

protected function registerMiddleware()
{
    /** @var Router $router */
    $router = $this->app['router'];
    $router->pushMiddlewareToGroup('web', SaveLocaleMiddleware::class);

//        $router->aliasMiddleware('localize', \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class);
//        $router->aliasMiddleware('localizationRedirect', \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class);
//        $router->aliasMiddleware('localeSessionRedirect', \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class);
//        $router->aliasMiddleware('localeCookieRedirect', \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class);
//        $router->aliasMiddleware('localeViewPath', \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class);
}

Một số middleware mà thư viện cung cấp sẵn cho chúng ta nếu cần.

Tới đây là xong
Chạy php artisan serve và trải nghiệm thành quả nào. Sau khi tạo data, thử kiểm tra database coi có ra như sau không:

{"en": "Name", "vi": "ten"}

// 
{"vi": "ten"}

//
{"en": "Name"}

Nếu như trên nghĩa là bạn đã setup thành công rồi đó.

Kết luận

Một chiếc blog mình nghĩ là khá chất lượng. Bạn có thể tìm hiểu thêm những cách khác hoặc muốn hiểu rõ hơn thì thử tìm thêm doc về package này.

Nếu gặp lỗi gì trong quá trình cài đặt hoặc kiến thức mình cung cấp không chính xác thì có thể liên hệ trực tiếp với minh qua các kênh thông tin phía cuối trang.

Nguồn tham khảo:

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC