إنشاء موقع متعدد اللغات بإطار العمل Laravel (محتوى متغير - dynamic content)

تم نشرها بتاريخ 2020-12-31

استعرضت في مقالة سابقة كيفية إنشاء مواقع ثابتة (Static Websites) متعددة اللغات، في هذه المقالة سأشرح عن إنشاء عدة ترجمات للمواقع ذات المحتوى المتغير (Dynamic Content) حيث يمكننا إدخال بيانات بترجمات مختلفة وعرضها حسب اللغة التي يختارها المستخدم. على سبيل المثال تطبيق لمطعم يمكننا من خلاله تصفح قائمة الوجبات بعدة لغات ليقوم كل مستخدم بعرض القائمة باللغة التي تناسبه. لنقم بتطوير نسخة بسيطة عن هذا التطبيق.

سنستخدم لبناء هذا التطبيق البسيط حزمة laravel-translatable تمكننا هذه الحزمة من تخزين ترجمات كل حقل في قاعدة البيانات، ثم تتولى هذه الحزمة مهمة عرض الترجمة الخاصة باللغة المختارة حاليا.


متطلبات هذه الحزمة هي:

  • إضافة Spatie\Translatable\HasTranslations-trait إلى Model الذي به حقول تحتوي على ترجمات.
  • ضافة خاصية $translatable وهي مصفوفة بإسماء الحقول التي تحتوي على ترجمات.
  • أخيرا أن يكون نوع الحقل في قاعدة البيانات text أو json في حال كانت قاعدة البيانات تدعم الحقول من نوع json.


نبدأ أولاً بإنشاء Model خاص لكل وجبة بالقائمة وننشأ معه Migration و Factory

php artisan make:model MenuItem -mf


نقوم بتحديد حقول الجدول: 

  • حقل الاسم name الذي يمثل اسم الوجبة.
  • حقل السعر price الذي يمثل سعر الوجبة.
  • حقل ingredients الذي يمثل مكونات الوجبة.

 public function up()
   {
       Schema::create('menu_items', function (Blueprint $table) {
           $table->id();
           $table->json('name');
           $table->float('price');
           $table->json('ingredientes');
           $table->timestamps();
       });
   }



نقوم بتكوين قاعدة بيانات وربطها بالتطبيق عن طريق تحديد المتغيرات داخل ملف .env ثم نقوم بترحيل الجداول:

php artisan migrate

الخطوة التالية هي تثبيت الحزمة:

composer require spatie/laravel-translatable

بتطبيق هذه المتطلبات سيكون محتوى MenuItem Model كالتالي


namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;

class MenuItem extends Model
{
   use HasFactory;
   use HasTranslations;

   public $translatable = ['name', 'ingredientes'];

   protected $guarded = ['id'];
}




يحتوى التطبيق على صفحتين، صفحة لعرض قائمة الاكل وصفحة لإنشاء وجبة جديدة وستكون كالتالي:

  • صفحة عرض الوجبات (قائمة الوجبات):


  • صفحة إنشاء وجبة جديدة، بإدخال الاسم والمكونات باللغتين العربية واﻹنجليزية:

قمت بإتباع خطوات المقالة السابقة لترجمة محتوى الثابت للواجهات لنتمكن من عرض الواجهتين باللغتين العربية والانجليزية، أما المحتوى المتغير والذي هو اسماء الوجبات ومكوناتها فالحزمة تتولى عرض الترجمة المناسبة حسب locale الحالي للتطبيق لأننا مثلما فعلنا في المقالة السابقة في SetLocale Middleware نقوم بتغيير لغة التطبيق حسب ما يختاره المستخدم من قائمة التطبيق العلوية.

ملاحظة بعض الأجزاء المضافة في الاكواد التالية هي تطبيقاً للمقالة السابقة فقم بإطلاع على عليها  لتتمكن من تغيير لغة التطبيق وترجمة المحتوى الثابت للواجهات تم يمكنك اﻹستمرار بهذه المقالة.


إنشاء صفحة إضافة وجبة جديدة

 نضيف routes  خاصات بها:

routes/web.php


Route::get('/', function () {
   return redirect(app()->getLocale());
});

Route::group([
   'prefix' => '{locale}',
   'where' => ['locale' => 'ar|en'],
   'middleware' => 'setlocale',
], function () {

   Route::get('/', function () {
       return view('welcome');
   })->name('welcome');




   Route::get('menu-items/create', [MenuItemController::class, 'create'])->name('menu-items.create');
   Route::post('menu-items', [MenuItemController::class, 'store'])->name('menu-items.store');
});


نضيف MenuItemController كالتالي:


namespace App\Http\Controllers;

use App\Models\MenuItem;

class MenuItemController extends Controller
{
   public function create()
   {
       return view('menu-items.create');
   }

   public function store()
   {
       request()->validate([
           'ar.name'         => 'required|string',
           'ar.ingredientes' => 'required|string',
           'en.name'         => 'required|string',
           'en.ingredientes' => 'required|string',
           'price'           => 'required|numeric',
       ]);

       $menuItem = MenuItem::create([
           'name' => [
               'ar' => request('ar')['name'],
               'en' => request('en')['name'],
           ],
           'ingredientes' => [
               'ar' => request('ar')['ingredientes'],
               'en' => request('en')['ingredientes'],
           ],
           'price' => request('price'),
       ]);

       return redirect(route('menu-items.index', request()->locale));
   }
}


بدالة create قمنا بإرجاع view الخاص بإنشاء وجبة جديدة الذي سننشأه تحت مجلد views/menu-items/create.blade.php، وبدالة store الخاصة بفورم اﻹضافة قمنا بالتحقق من صحة البيانات وتم حفظها.

لنتبع طريقة الحفظ المستخدمة بالحزمة للحقول القابلة الترجمة، حيث يتم تحديد مختصر اللغة (نفس مختصرات اللغات المستخدم في app locale) تم يتم حفظ ترجمة هذه اللغة داخل مصفوفة:


'name' => [
               'ar' => request('ar')['name'],
               'en' => request('en')['name'],
           ],
           'ingredientes' => [
               'ar' => request('ar')['ingredientes'],
               'en' => request('en')['ingredientes'],
           ],


ستحفظ كالتالي على شكل json (حقل المكونات):

{"ar": "فلفل، بصل، طماطم، ذرة، جبن", "en": "pepper, onions, tomatoes, cheese"}


views/menu-items/create.blade.php

<div class="max-w-4xl mx-auto">

   <form action="{{ route('menu-items.store', request()->locale) }}" method="post">

       @csrf

       <div class="grid gap-12 grid-cols-2 bg-white shadow p-8 mt-5 pt-10">

           <div>

               <h3 class="text-lg leading-6 font-medium text-gray-900 pb-2">

                   {{ __('add_new_menu_item') }} - {{ __('arabic') }}

               </h3>

               <div class="space-y-6">

                   <div class="flex flex-col border-t border-gray-200 pt-5">

                       <label for="name" class="block text-sm font-medium text-gray-700">

                           {{ __('item_name') }}

                       </label>

                       <div class="mt-2 w-full">

                           <input type="text" name="ar[name]" id="name"

                               class="w-full border shadow-sm rounded-md py-2">

                       </div>

                       @error('en.name')

                       <div class="text-sm text-red-500">{{ $message }}</div>

                       @enderror

                   </div>

                   <div class="flex flex-col border-t border-gray-200 pt-5">

                       <label for="ingredientes" class="block text-sm text-gray-700">

                           {{ __('ingredientes') }}

                       </label>

                       <div class="mt-2 w-full">

                           <textarea id="ingredientes" name="ar[ingredientes]" rows="3"

                               class="shadow-sm border w-full rounded-md"></textarea>

                           <p class="mt-2 text-sm text-gray-500">

                               {{ __('separate_ingredientes_by_comma') }}

                           </p>

                           @error('ar.ingredientes')

                           <div class="text-sm text-red-500">{{ $message }}</div>

                           @enderror
                       </div>

                   </div>
               </div>
           </div>
           <div>

               <h3 class="text-lg leading-6 font-medium text-gray-900 pb-2">

                   {{ __('add_new_menu_item') }} - {{ __('english') }}

               </h3>
               <div class="space-y-6">

                   <div class="flex flex-col border-t border-gray-200 pt-5">

                       <label for="name" class="block text-sm font-medium text-gray-700">

                           {{ __('item_name') }}

                       </label>

                       <div class="mt-2 w-full">

                           <input type="text" name="en[name]" id="name"

                               class="w-full border shadow-sm rounded-md py-2">

                       </div>

                       @error('en.name')

                       <div class="text-sm text-red-500">{{ $message }}</div>

                       @enderror

                   </div>

                   <div class="flex flex-col border-t border-gray-200 pt-5">

                       <label for="ingredientes" class="block text-sm text-gray-700">

                           {{ __('ingredientes') }}

                       </label>

                       <div class="mt-2 w-full">

                           <textarea id="ingredientes" name="en[ingredientes]" rows="3"

                               class="shadow-sm border w-full rounded-md"></textarea>

                           <p class="mt-2 text-sm text-gray-500">

                               {{ __('separate_ingredientes_by_comma') }}

                           </p>

                           @error('en.ingredientes')

                           <div class="text-sm text-red-500">{{ $message }}</div>

                           @enderror
                       </div>
                   </div>
               </div>
           </div>

           <div class="flex flex-col">

               <label for="price" class="block text-sm font-medium text-gray-700">
                   {{ __('item_price') }}
               </label>

               <div class="mt-2 w-full">

                   <input type="number" name="price" id="price" class="w-full border shadow-sm rounded-md py-2">

               </div>

               @error('price')

               <div class="text-sm text-red-500">{{ $message }}</div>
               @enderror
           </div>
           <div class="pt-5">

               <div class="flex justify-end">

                   <button type="submit"

                       class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                       {{ __('add') }}
                   </button>
               </div>
           </div>
       </div>
   </form>
</div>


resources/views/layouts/app.blade.php

<!DOCTYPE html>
<html lang="en" dir="{{ app()->getLocale() == 'ar'? 'rtl' : 'ltr' }}">

<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">
   <title>Menu</title>
   <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
</head>

<body class="bg-gray-50">
   <nav class="bg-white">
       <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
           <div class="flex justify-between h-16">
               <div class="flex">
                   <div class="hidden md:ml-6 md:flex md:items-center md:space-x-4">
                       <a href="{{ route('menu-items.index', request()->locale) }}"
                           class="text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
                           {{ __('list_menu_items') }}
                       </a>
                       <a href="{{ route(request()->route()->getName(), 'en') }}"
                           class="text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
                           {{ __('english') }}
                       </a>
                       <a href="{{ route(request()->route()->getName(), 'ar') }}"
                           class="text-gray-900 px-3 py-2 rounded-md text-sm font-medium">
                           {{ __('arabic') }}
                       </a>
                   </div>
               </div>
           </div>
       </div>
   </nav>
   @yield('content')
</body>
</html>


عرض قائمة الوجبات

نضيف أولا get route الخاص بها


  Route::get('menu-items', [MenuItemController::class, 'index'])->name('menu-items.index');


MenuItemController


public function index()
   {
       return view('menu-items.index', ['items' => MenuItem::latest()->get()]);
   }


views/menu-items/index.blade.php

@extends('layouts.app')

@section('content')
<div class="max-w-6xl mx-auto mt-8">
   <div class="flex justify-end">
       <a href="{{ route('menu-items.create', request()->locale) }}"
           class="ml-3 py-2 px-4 border shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600">{{ __('add_new_item') }}</a>
   </div>
   <div class="bg-white shadow overflow-hidden sm:rounded-md mt-5">
       <ul class="divide-y divide-gray-200">
           @foreach ($items as $item)
           <li>
               <a href="#" class="block hover:bg-gray-50">
                   <div class="px-4 py-4 sm:px-6">
                       <div class="flex items-center justify-between">
                           <p class="text-sm font-medium text-indigo-600 truncate">
                               {{ $item->name }}
                           </p>
                       </div>
                       <div class="mt-2 sm:flex sm:justify-between">
                           <div class="sm:flex">
                               {{ $item->ingredientes }}
                           </div>
                           <div class="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
                               {{ $item->price }} {{ __('lyd') }}
                           </div>
                       </div>
                   </div>
               </a>
           </li>
           @endforeach
       </ul>
   </div>
</div>
@endsection

ملفات الترجمة التي استخدمتها لترجمة المحتوى الثابت


resources/lang/ar.json



{
   "add_new_menu_item": "أضف وجبة جديدة للقائمة الاكل",
   "item_name": "اسم الوجبة",
   "ingredientes": "المكونات",
   "separate_ingredientes_by_comma": "قم بفصل اسماء المكونات بالفاصلة.",
   "add": "إضافة",
   "list_menu_items": "القائمة الاكل",
   "english": "الإنجليزية",
   "arabic": "العربية",
   "item_price": "سعر الوجبة",
   "add_new_item": "إضافة وجبة جديدة",
   "lyd": "د.ل"
}



resources/lang/en.json


{
   "add_new_menu_item": "Add New Menu Item",
   "item_name": "Dish Name",
   "ingredientes": "Ingredientes",
   "separate_ingredientes_by_comma": "Separate Ingredientes by comma",
   "add": "Add",
   "list_menu_items": "List Menu Items",
   "english": "English",
   "arabic": "Arabic",
   "item_price": "Item Price",
   "add_new_item": "Add New Item",
   "lyd": "LYD"
}


نلاحظ عند عرض قائمة الوجبات فإنه فقط بطباعة الحقل المطلوب مثل {{ $item->name }} تتولى الحزمة عرض الترجمة المناسبة لهذا الحقل حسب اللغة التطبيق المحددة حالياً، ففي كل مرة نقوم بتغيير اللغة باستخدام SetLocale middleware تتغير ترجمة قائمة الوجبات.


من خلال هاتين المقالتين قمنا بتطبيق كيفية إنشاء مواقع بمحتوى ثابث ومتغير، يممكنكم الاطلاع أيضا على مقالتي السابقة ترجمة واستخدام رسائل التحقق التي استعرضت فيها كيفية إنشاء ترجمات لرسائل التحقق validation messages، من خلال هذه السلسلة يمكنك اﻵن انشاء تطبيق كامل يدعم تعدد الللغات.

خولة الشح

تمت كتابتها بواسطة خولة الشح