الانتقال إلى المحتوى

من الفريق

لماذا نخزّن الترجمات في عمود JSON لا في جدول ترجمات

نمط جدول الترجمات الموازي هو النصيحة الافتراضية للمحتوى متعدد اللغات. لكنه في موقع تسويقي يكلّفك ربطًا في كل قراءة ولا يمنحك شيئًا يُذكر.

قراءة 4 دقيقة

الطريقة المعتادة لجعل نماذج Eloquent متعددة اللغات هي جدول موازٍ: posts مع post_translations، صف لكل لغة، يُربطان عند القراءة. وهو النمط الذي تلجأ إليه معظم الحزم، وهو صحيح لبعض أنماط الاستخدام.

نحن اخترنا الاتجاه الآخر. الأعمدة القابلة للترجمة في هذا المشروع من نوع jsonb تحتوي {"en": "…", "ar": "…"} عبر حزمة spatie/laravel-translatable.

ما يكلّفه الربط فعلًا

الموقع التسويقي يقرأ العدد نفسه من الصفوف باستمرار ويكتبها بضع مرات أسبوعيًا. وكل صفحة قوائم — القائمة الكبرى بخمس وعشرين خدمة، وفهرس المدونة، وفهرس الأعمال — تتحوّل إلى ربط مع جدول فيه صفّان لكل سجل دون فائدة، لأنك تريد دائمًا لغة واحدة بعينها وتعرفها قبل الاستعلام.

ومع عمود JSON، يصبح جلب صفحة بلغة ما استعلامًا بسيطًا من جدول واحد. صف واحد لكل كيان، بلا روابط، والصف الذي خزّنته مؤقتًا هو الكيان كاملًا بكل اللغات.

ما تتنازل عنه

هذه مقايضة حقيقية لا مكسبًا مجانيًا. لا يمكنك فهرسة محتوى الترجمة أو الاستعلام داخله بتكلفة منخفضة. فجملة WHERE title->>'ar' LIKE '%…%' لن تستفيد من فهرس B-tree عادي، ويحتاج البحث النصي عبر الترجمات إلى فهرس GIN أو خدمة بحث مخصّصة.

وهذا لا يهمّنا بعد: البحث في الموقع مرحلة لاحقة وسيمرّ عبر فهرس بحث لا عبر LIKE على عمود jsonb على أي حال. أما لو كانت الميزة الأساسية لتطبيقك هي البحث في محتوى المستخدمين بأربعين لغة، لكان الجدول الموازي الخيار الأفضل.

المعرّفات تبقى بلغة واحدة

استثناء واحد مقصود: slug غير قابل للترجمة. فالمعرّف اللاتيني الواحد يبقي التوجيه وأزواج hreflang بسيطة، واللغة موجودة أصلًا في بادئة المسار — /ar/services/ai-automation. أما المعرّفات العربية لكل لغة فتفيد البحث العربي قليلًا وتعقّد كل عملية توجيه كثيرًا، وهو قرار يستحق المراجعة فقط حين تبرّر الزيارات العضوية العربية تكلفته.

كيف يبدو الأمر للمحرّر

قرار التخزين ينسحب إلى لوحة الإدارة، فيجب معالجته هناك لا تركه تفصيلًا في قاعدة البيانات. فكل حقل قابل للترجمة في هذا المشروع يظهر بتبويب لكل لغة، والإنجليزية أولًا لأنها اللغة الاحتياطية.

وأخطر ما يجب الاحتراس منه محدّد: يفتح المحرّر خدمة، ويعدّل التبويب الإنجليزي وحده، ويحفظ — فتختفي القيمة العربية، لأن النموذج أرسل كائن JSON كاملًا وفيه مفتاح ناقص. يحدث ذلك بصمت، ويكتشفه زائر عربي بعد أسابيع، وهو السبب الوحيد لوجود اختبار عندنا يتحقّق من أن حفظ لغة واحدة لا يمسّ الأخرى.

الرجوع إلى لغة احتياطية قرار منتج لا سلوك افتراضي

القيمة العربية الناقصة تعود إلى الإنجليزية. وهذا هو السلوك الصحيح — فصفحة بلغة غير مطلوبة أفضل من صفحة فارغة — لكن له نتيجة: الموقع المترجَم نصفيًا يبدو مكتملًا. لا خطأ يظهر، ولا شيء يبدو معطّلًا، والفجوة غير مرئية لمن لا يقرأ بتلك اللغة.

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

ما تعطيه لك طبقة الاستعلام في المقابل

الشيء الذي يفترض الناس فقدانه مع عمود JSON هو القدرة على الترشيح بحسب ترجمة، وهذا صحيح جزئيًا فقط. فدالة whereJsonContainsLocale وما شابهها تغطّي المطابقة التامة لكل لغة، وهو ما يكفي لمعظم ترشيح لوحة الإدارة: جد كل خدمة بلا متن عربي، أو اعرض الصفحات المنشورة بلغة واحدة فقط.

أما ما لا يعمل فعلًا فهو البحث النصي المرتّب عبر الترجمات. وهذا يحتاج فهرس بحث أيًّا كان شكل التخزين الذي اخترته، فليس فارقًا حقيقيًا بين المقاربتين — بل قطعة بنية تحتية منفصلة في الحالتين.

القاعدة العملية

اختر عمود JSON حين تقرأ كيانات كاملة بلغة واحدة معروفة وتكتب نادرًا. واختر الجدول الموازي حين تستعلم عبر الترجمات، أو تحتاج فهارس لكل لغة، أو لديك من اللغات ما يجعل حملها كلها في كل صف إهدارًا حقيقيًا.