التعامل مع API في Laravel – الجزء السابع: دعم أكثر من لغة في Laravel API

معظم التطبيقات الحديثة لا تعمل بلغة واحدة فقط، وخصوصًا تطبيقات الهواتف والمواقع التي تستهدف مستخدمين من عدة دول أو مناطق.

قد يختار المستخدم مثلًا اللغة العربية من داخل تطبيق الهاتف، وعندها يجب أن يقوم Backend بإرجاع أسماء المنتجات والتصنيفات والرسائل باللغة العربية.

أما إذا قام المستخدم بتغيير لغة التطبيق إلى الإنجليزية، فيجب أن يعيد API البيانات باللغة الإنجليزية.

في هذا الجزء من سلسلة Laravel API لن نتحدث بالتفصيل عن إنشاء ملفات الترجمة في Laravel، بل سنركز على نقطة محددة:

كيف نحدد لغة Request القادم من Frontend أو تطبيق الهاتف، وكيف نجعل Laravel API يتعامل مع هذه اللغة؟

سنقوم بذلك باستخدام Middleware يقرأ اللغة من HTTP Request ثم يقوم بتعيين Locale المناسب قبل وصول الطلب إلى Controller.

جدول المحتويات

  1. المشكلة التي نريد حلها
  2. ما هو Locale في Laravel؟
  3. كيف يرسل Client اللغة؟
  4. استخدام Accept-Language
  5. استخدام lang كـQuery Parameter
  6. إنشاء Language Middleware
  7. تحديد اللغات المسموحة
  8. تعيين لغة التطبيق
  9. تسجيل Middleware
  10. تطبيق Middleware على API Routes
  11. تطبيق Middleware على API بالكامل
  12. الحصول على اللغة الحالية
  13. إرجاع البيانات حسب اللغة من قاعدة البيانات
  14. استخدام اللغة داخل API Resource
  15. استخدام Laravel Translation Files
  16. ترجمة Validation Messages
  17. التعامل مع لغة غير مدعومة
  18. لغة المستخدم المسجل
  19. Fallback Locale
  20. اختبار اللغات باستخدام Postman
  21. استخدامه من تطبيق الهاتف
  22. أفضل الممارسات
  23. مثال متكامل
  24. ملخص الدرس
  25. الخلاصة

المشكلة التي نريد حلها

لنفترض أن لدينا تطبيق هاتف يدعم لغتين:

Arabic
English

ولدينا Endpoint:

GET /api/v1/brands

إذا اختار المستخدم اللغة العربية، نريد Response مثل:

{
    "id": 1,
    "name": "أبل"
}

أما إذا اختار الإنجليزية:

{
    "id": 1,
    "name": "Apple"
}

نريد أن يعرف Laravel لغة المستخدم قبل تنفيذ Controller.

لهذا سنستخدم Middleware.

ما هو Locale في Laravel؟

Laravel يحتفظ بلغة حالية للتطبيق تسمى:

Locale

يمكن تغييرها أثناء تنفيذ Request باستخدام:

App::setLocale('ar');

أو:

app()->setLocale('ar');

ويمكن معرفة اللغة الحالية باستخدام:

App::currentLocale();

أو:

app()->getLocale();

إذا قمنا بتعيين:

ar

فستتعامل عمليات الترجمة التي تتم خلال هذا Request مع اللغة العربية.

كيف يرسل Client اللغة إلى Laravel API؟

يجب أن يرسل Client معلومة تخبر Backend باللغة التي يريدها.

يوجد أكثر من أسلوب.

مثل:

Accept-Language: ar

أو:

?lang=ar

يفضل في REST API استخدام Header عندما تكون اللغة جزءًا من تفضيلات Request وليست Resource مستقلة.

استخدام Accept-Language

HTTP يحتوي أصلًا على Header مخصص لتحديد اللغات التي يفضلها Client:

Accept-Language

يمكن أن يرسل تطبيق الهاتف:

Accept-Language: ar

أو:

Accept-Language: en

وهذا يجعل URL نفسه ثابتًا:

GET /api/v1/brands

بدون إضافة اللغة إلى Query String في كل Request.

استخدام lang كـQuery Parameter

إذا كان التطبيق الحالي يعتمد على:

?lang=ar

يمكن الاستمرار بدعمه.

مثل:

GET /api/v1/brands?lang=ar

أو:

GET /api/v1/brands?lang=en

لكن يمكن تصميم Middleware بحيث يعطي الأولوية إلى:

Accept-Language

ثم يستخدم:

lang

كخيار بديل.

إنشاء Middleware خاص باللغة

سننشئ Middleware باسم:

SetLocale

باستخدام:

php artisan make:middleware SetLocale

سيتم إنشاء الملف داخل:

app/Http/Middleware/SetLocale.php

وسيكون هيكله قريبًا من:

<?php

namespace App\Http\Middleware;

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

class SetLocale
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {
        return $next($request);
    }
}

تحديد اللغات المسموحة

لا يجب أن نقبل أي قيمة يرسلها Client ثم نستخدمها مباشرة كلغة.

من الأفضل تعريف قائمة باللغات المدعومة.

مثل:

$supportedLocales = [
    'ar',
    'en',
];

إذا أرسل Client:

fr

ولم تكن الفرنسية مدعومة، فلا نقوم بتعيينها تلقائيًا.

هذه النقطة تصبح أكثر أهمية إذا كنا سنستخدم Locale لاختيار Column من قاعدة البيانات.

كتابة منطق Language Middleware

يمكن كتابة Middleware بالشكل التالي:

<?php

namespace App\Http\Middleware;

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

class SetLocale
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {

        $supportedLocales = [
            'ar',
            'en',
        ];

        $defaultLocale = 'ar';

        $locale =
            $request->header('Accept-Language')
            ?? $request->query('lang')
            ?? $defaultLocale;


        $locale = strtolower(
            substr($locale, 0, 2)
        );


        if (! in_array(
            $locale,
            $supportedLocales,
            true
        )) {
            $locale = $defaultLocale;
        }


        App::setLocale($locale);


        return $next($request);
    }
}

بهذا الشكل يقوم Middleware بالبحث أولًا عن:

Accept-Language

ثم عن:

lang

وإذا لم يجد أيًا منهما يستخدم:

ar

كلغة افتراضية لهذا المثال.

تسجيل Middleware في Laravel الحديث

في Laravel الحديث يتم تعريف Middleware aliases داخل:

bootstrap/app.php

نضيف:

use App\Http\Middleware\SetLocale;
use Illuminate\Foundation\Configuration\Middleware;

ثم داخل:

->withMiddleware()

نكتب:

->withMiddleware(
    function (Middleware $middleware): void {

        $middleware->alias([
            'locale' => SetLocale::class,
        ]);

    }
)

أصبح لدينا Middleware Alias باسم:

locale

تطبيق Language Middleware على Routes

يمكن تطبيق Middleware على مجموعة Routes:

Route::middleware('locale')
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });

وبذلك يتم تحديد لغة التطبيق قبل تنفيذ أي Route داخل المجموعة.

تطبيق Middleware على جميع API Routes

إذا كان كل API في التطبيق يدعم أكثر من لغة، فقد يكون من الأفضل إضافة Middleware إلى مجموعة:

api

مباشرة.

داخل:

bootstrap/app.php

يمكن استخدام:

->withMiddleware(
    function (Middleware $middleware): void {

        $middleware->api(
            prepend: [
                SetLocale::class,
            ]
        );

    }
)

وبهذه الطريقة يعمل Language Middleware تلقائيًا على API Routes.

إذا كنت تحتاجه على Routes محددة فقط، فاستخدام Alias مثل:

locale

يكون أكثر وضوحًا.

الحصول على اللغة الحالية

بعد مرور Request عبر Middleware يمكن الوصول إلى اللغة الحالية في أي مكان داخل Request Lifecycle.

مثل:

use Illuminate\Support\Facades\App;

$locale = App::currentLocale();

أو:

$locale = app()->getLocale();

إذا كانت اللغة الحالية عربية:

ar

وإذا كانت الإنجليزية:

en

إرجاع البيانات حسب اللغة من قاعدة البيانات

لنفترض أن جدول:

brands

يحتوي على:

id
name_ar
name_en

يمكن اختيار Column المناسب بناءً على اللغة الحالية.

مثلًا:

public function index()
{
    $locale = app()->getLocale();

    $nameColumn = match ($locale) {
        'en' => 'name_en',
        default => 'name_ar',
    };

    $brands = Brand::query()
        ->select([
            'id',
            $nameColumn.' as name',
        ])
        ->get();

    return BrandResource::collection(
        $brands
    );
}

لاحظ أننا لم نكتب:

'name_' . $request->lang

مباشرة.

بل قمنا بربط اللغة بقائمة Columns معروفة.

هذا يجعل الكود أكثر وضوحًا ويمنع استخدام قيم غير متوقعة من Request لبناء أسماء Columns.

استخدام اللغة داخل API Resource

يمكن أيضًا إبقاء جميع Columns داخل Model، ثم تحديد الحقل الذي سيظهر من خلال API Resource.

لنفترض أن لدينا:

name_ar
name_en

يمكن كتابة:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class BrandResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        $name = match (
            app()->getLocale()
        ) {
            'en' => $this->name_en,
            default => $this->name_ar,
        };


        return [
            'id' => $this->id,
            'name' => $name,
        ];
    }
}

بهذا يبقى شكل Response ثابتًا:

{
    "id": 1,
    "name": "..."
}

ولا يحتاج Frontend لمعرفة أن قاعدة البيانات تحتوي على:

name_ar
name_en

وهذا فصل جيد بين بنية قاعدة البيانات وAPI Contract.

استخدام Laravel Translation Files

Locale لا يستخدم فقط لجلب البيانات من قاعدة البيانات.

يمكن استخدامه أيضًا مع Laravel Translation System.

مثلًا إذا كان لدينا:

lang/en/api.php

lang/ar/api.php

يمكن أن يحتوي الملف العربي على:

<?php

return [
    'created' => 'تمت إضافة البيانات بنجاح.',
    'deleted' => 'تم حذف البيانات بنجاح.',
];

والإنجليزي:

<?php

return [
    'created' => 'Data created successfully.',
    'deleted' => 'Data deleted successfully.',
];

ثم داخل Controller:

return response()->json([
    'message' => __('api.created'),
]);

إذا كانت لغة Request:

ar

يحصل Client على:

{
    "message": "تمت إضافة البيانات بنجاح."
}

أما مع:

en

فيحصل على:

{
    "message": "Data created successfully."
}

ترجمة Validation Messages

ميزة تعيين Locale في Middleware قبل الوصول إلى Controller هي أن أجزاء أخرى من Laravel تستطيع استخدام اللغة الحالية أيضًا.

من بينها Validation Messages إذا كانت ملفات الترجمة اللازمة موجودة.

مثلًا عند فشل:

'name' => ['required']

يمكن إرجاع رسالة متوافقة مع Locale الحالي بدل كتابة Messages يدويًا في كل Controller.

وهذا يساعد في جعل API متعدد اللغات بصورة موحدة.

التعامل مع لغة غير مدعومة

ماذا لو أرسل Client:

Accept-Language: de

بينما التطبيق يدعم فقط:

ar
en

لدينا خياران شائعان.

استخدام اللغة الافتراضية

وهذا ما فعلناه في المثال:

if (! in_array(
    $locale,
    $supportedLocales,
    true
)) {
    $locale = $defaultLocale;
}

رفض Request

إذا أردنا سياسة أكثر صرامة يمكن إعادة:

400 Bad Request

مثلًا:

{
    "message": "Unsupported locale."
}

غالبًا استخدام Fallback يكون أفضل لتجربة المستخدم، لكن الاختيار يعتمد على API Contract.

ماذا عن لغة المستخدم المسجل؟

في التطبيقات الأكبر قد نقوم بتخزين لغة المستخدم داخل قاعدة البيانات.

مثل:

users.locale

وقيمتها:

ar

أو:

en

يمكن عندها بناء أولوية مثل:

  1. اللغة المرسلة في Request.
  2. لغة المستخدم المحفوظة في الحساب.
  3. اللغة الافتراضية للتطبيق.

لكن يجب تحديد سياسة واحدة واضحة حتى لا تتغير اللغة بشكل غير متوقع بين Requests.

Fallback Locale

من الجيد أن يحتوي التطبيق على لغة بديلة تستخدم عندما لا توجد ترجمة للنص المطلوب في اللغة الحالية.

مثلًا قد تكون اللغة الحالية:

ar

لكن أحد Translation Keys غير موجود بالعربية.

يمكن عندها استخدام Fallback Locale مثل:

en

بحسب إعدادات Localization في التطبيق.

Fallback لا يعني أن اللغة المطلوبة غير صحيحة، بل هو آلية لمنع فقدان Translation String عند عدم توفرها في Locale الحالي.

اختبار أكثر من لغة باستخدام Postman

لاختبار اللغة العربية:

GET /api/v1/brands

Accept: application/json
Accept-Language: ar

قد نحصل على:

{
    "data": [
        {
            "id": 1,
            "name": "أبل"
        }
    ]
}

ثم نغير Header إلى:

Accept-Language: en

فنحصل على:

{
    "data": [
        {
            "id": 1,
            "name": "Apple"
        }
    ]
}

وإذا أردنا دعم الطريقة القديمة أيضًا يمكن اختبار:

GET /api/v1/brands?lang=en

استخدام اللغة من تطبيق الهاتف

بعد أن يختار المستخدم اللغة داخل تطبيق الهاتف، يفضل أن يقوم التطبيق بإضافة Header تلقائيًا إلى جميع Requests.

مثل:

Accept-Language: ar

ثم عند تغيير اللغة:

Accept-Language: en

بهذه الطريقة لا يحتاج مطور التطبيق إلى إضافة:

?lang=en

يدويًا إلى كل URL.

عادةً يتم إعداد HTTP Client في تطبيق الهاتف مرة واحدة ليضيف اللغة الحالية إلى Headers في جميع Requests.

أفضل الممارسات عند بناء API متعدد اللغات

استخدم قائمة محددة للغات

لا تقبل أي Locale يرسله Client بدون Validation أو Whitelist.

استخدم Accept-Language

لأنه Header مخصص لتفضيلات اللغة في HTTP، ويمكن إبقاء Query Parameter للتوافق مع تطبيقات قديمة عند الحاجة.

اجعل API Response ثابتًا

يفضل أن يعيد API:

{
    "name": "Apple"
}

بدل جعل Client يتعامل مع:

{
    "name_ar": "أبل",
    "name_en": "Apple"
}

إذا كان هدف Endpoint هو إرجاع لغة واحدة فقط.

لا تجعل Frontend يعرف بنية قاعدة البيانات

API Resource مناسب جدًا لإخفاء طريقة تخزين الترجمات.

لا تستخدم Locale غير موثوق لبناء SQL

تجنب بناء:

name_$locale

من قيمة Request غير متحقق منها.

استخدم Mapping أو Whitelist واضحة.

فرق بين ترجمة البيانات وترجمة الرسائل

هناك فرق بين:

Brand Name

المخزن في قاعدة البيانات، وبين:

"تمت العملية بنجاح"

وهي Application Message يمكن إدارتها باستخدام ملفات Translation في Laravel.

لا تخزن اللغة في Session إذا كان API Stateless ولا تحتاجها

في REST API يمكن تحديد اللغة لكل Request من Header، خصوصًا لتطبيقات الهاتف وClients الخارجية.

مثال عملي متكامل

1. إنشاء Middleware

php artisan make:middleware SetLocale

2. كتابة Middleware

<?php

namespace App\Http\Middleware;

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

class SetLocale
{
    public function handle(
        Request $request,
        Closure $next
    ): Response {

        $supportedLocales = [
            'ar',
            'en',
        ];

        $defaultLocale = 'ar';

        $locale =
            $request->header('Accept-Language')
            ?? $request->query('lang')
            ?? $defaultLocale;


        $locale = strtolower(
            substr($locale, 0, 2)
        );


        if (! in_array(
            $locale,
            $supportedLocales,
            true
        )) {
            $locale = $defaultLocale;
        }


        App::setLocale($locale);


        return $next($request);
    }
}

3. تسجيل Middleware

داخل:

bootstrap/app.php

نضيف:

use App\Http\Middleware\SetLocale;
use Illuminate\Foundation\Configuration\Middleware;

ثم:

->withMiddleware(
    function (Middleware $middleware): void {

        $middleware->alias([
            'locale' => SetLocale::class,
        ]);

    }
)

4. حماية Routes باستخدام Middleware

Route::prefix('v1')
    ->middleware('locale')
    ->group(function () {

        Route::apiResource(
            'brands',
            BrandController::class
        );

    });

5. إعداد BrandResource

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class BrandResource extends JsonResource
{
    public function toArray(
        Request $request
    ): array {

        $name = match (
            app()->getLocale()
        ) {
            'en' => $this->name_en,
            default => $this->name_ar,
        };


        return [
            'id' => $this->id,
            'name' => $name,
        ];
    }
}

6. Controller

public function index()
{
    return BrandResource::collection(
        Brand::query()->get()
    );
}

7. طلب باللغة العربية

GET /api/v1/brands

Accept: application/json
Accept-Language: ar

Response:

{
    "data": [
        {
            "id": 1,
            "name": "أبل"
        },
        {
            "id": 2,
            "name": "سامسونج"
        }
    ]
}

8. طلب باللغة الإنجليزية

GET /api/v1/brands

Accept: application/json
Accept-Language: en

Response:

{
    "data": [
        {
            "id": 1,
            "name": "Apple"
        },
        {
            "id": 2,
            "name": "Samsung"
        }
    ]
}

ملخص الدرس

المفهومالوصف
Localeاللغة الحالية المستخدمة أثناء تنفيذ Request.
App::setLocale()تعيين لغة التطبيق الحالية.
App::currentLocale()الحصول على Locale الحالي.
Accept-LanguageHTTP Header يستخدم لتحديد اللغة المفضلة لدى Client.
SetLocale MiddlewareMiddleware يحدد لغة التطبيق قبل تنفيذ Controller.
Supported Localesقائمة اللغات التي يسمح API باستخدامها.
Fallback Localeلغة بديلة عند عدم توفر Translation.
API Resourceطبقة مناسبة لإرجاع الحقل المترجم دون كشف بنية قاعدة البيانات.
lang Query Parameterطريقة بديلة يمكن دعمها للتوافق مع Clients القديمة.
bootstrap/app.phpالمكان الحديث لتسجيل Middleware aliases.

الخلاصة

دعم أكثر من لغة في Laravel API يبدأ بتحديد اللغة التي يريدها Client لكل Request.

بدل كتابة منطق اللغة داخل كل Controller، أنشأنا Middleware مسؤولًا عن قراءة:

Accept-Language

ثم تعيين Locale باستخدام:

App::setLocale()

وبعد ذلك يستطيع Controller وAPI Resources وValidation ونظام الترجمة في Laravel التعامل مع اللغة الحالية.

كما أبقينا إمكانية دعم:

?lang=ar
?lang=en

إذا كان لدينا Client قديم يعتمد عليها، لكن استخدام Accept-Language يجعل API أكثر تنظيمًا ولا يتطلب تغيير URL لكل لغة.

وتعلمنا كذلك كيفية إرجاع الحقل الصحيح من قاعدة البيانات حسب اللغة، مع التأكيد على ضرورة استخدام قائمة محددة من Locales وعدم استخدام قيمة يرسلها المستخدم مباشرة لبناء أسماء Columns أو Queries.

وأخيرًا، استخدام API Resources يسمح لنا بالإبقاء على API Contract ثابتًا:

{
    "name": "..."
}

بينما يتولى Backend اختيار القيمة العربية أو الإنجليزية وفق اللغة الحالية.