التعامل مع API في Laravel – الجزء السابع: دعم أكثر من لغة في Laravel API
معظم التطبيقات الحديثة لا تعمل بلغة واحدة فقط، وخصوصًا تطبيقات الهواتف والمواقع التي تستهدف مستخدمين من عدة دول أو مناطق.
قد يختار المستخدم مثلًا اللغة العربية من داخل تطبيق الهاتف، وعندها يجب أن يقوم Backend بإرجاع أسماء المنتجات والتصنيفات والرسائل باللغة العربية.
أما إذا قام المستخدم بتغيير لغة التطبيق إلى الإنجليزية، فيجب أن يعيد API البيانات باللغة الإنجليزية.
في هذا الجزء من سلسلة Laravel API لن نتحدث بالتفصيل عن إنشاء ملفات الترجمة في Laravel، بل سنركز على نقطة محددة:
كيف نحدد لغة Request القادم من Frontend أو تطبيق الهاتف، وكيف نجعل Laravel API يتعامل مع هذه اللغة؟
سنقوم بذلك باستخدام Middleware يقرأ اللغة من HTTP Request ثم يقوم بتعيين Locale المناسب قبل وصول الطلب إلى Controller.
جدول المحتويات
- المشكلة التي نريد حلها
- ما هو Locale في Laravel؟
- كيف يرسل Client اللغة؟
- استخدام Accept-Language
- استخدام lang كـQuery Parameter
- إنشاء Language Middleware
- تحديد اللغات المسموحة
- تعيين لغة التطبيق
- تسجيل Middleware
- تطبيق Middleware على API Routes
- تطبيق Middleware على API بالكامل
- الحصول على اللغة الحالية
- إرجاع البيانات حسب اللغة من قاعدة البيانات
- استخدام اللغة داخل API Resource
- استخدام Laravel Translation Files
- ترجمة Validation Messages
- التعامل مع لغة غير مدعومة
- لغة المستخدم المسجل
- Fallback Locale
- اختبار اللغات باستخدام Postman
- استخدامه من تطبيق الهاتف
- أفضل الممارسات
- مثال متكامل
- ملخص الدرس
- الخلاصة
المشكلة التي نريد حلها
لنفترض أن لدينا تطبيق هاتف يدعم لغتين:
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يمكن عندها بناء أولوية مثل:
- اللغة المرسلة في Request.
- لغة المستخدم المحفوظة في الحساب.
- اللغة الافتراضية للتطبيق.
لكن يجب تحديد سياسة واحدة واضحة حتى لا تتغير اللغة بشكل غير متوقع بين 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 SetLocale2. كتابة 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: arResponse:
{
"data": [
{
"id": 1,
"name": "أبل"
},
{
"id": 2,
"name": "سامسونج"
}
]
}8. طلب باللغة الإنجليزية
GET /api/v1/brands
Accept: application/json
Accept-Language: enResponse:
{
"data": [
{
"id": 1,
"name": "Apple"
},
{
"id": 2,
"name": "Samsung"
}
]
}ملخص الدرس
| المفهوم | الوصف |
|---|---|
| Locale | اللغة الحالية المستخدمة أثناء تنفيذ Request. |
| App::setLocale() | تعيين لغة التطبيق الحالية. |
| App::currentLocale() | الحصول على Locale الحالي. |
| Accept-Language | HTTP Header يستخدم لتحديد اللغة المفضلة لدى Client. |
| SetLocale Middleware | Middleware يحدد لغة التطبيق قبل تنفيذ 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 اختيار القيمة العربية أو الإنجليزية وفق اللغة الحالية.
what do you think if use a header? instead of request? i mean send \"x-localization\" :\"en\" for each request to to send as query, i think it is better than this . and from middleware we check it. then set Local, and please if i am wrong send me email