بناء موقع ثنائي اللغة بدون مكتبة ترجمة
لماذا كتبت طبقة القواميس بنفسي بدل الاعتماد على next-intl
كل الدروس تنصحك بتثبيت مكتبة ترجمة في اللحظة التي تحتاج فيها إلى لغة ثانية. جرّبت ذلك، ثم أزلتها بعد يومين وكتبت ما يقارب ستين سطراً خاصة بي بدلاً منها. هذا ما حصلت عليه فعلياً من ذلك القرار، والأجزاء التي لن أكررها.
01ما احتجته فعلياً
قائمة متطلباتي كانت قصيرة. لغتان، الإنكليزية والعربية. محتوى يعيش داخل نظام إدارة الإصدارات، لا في نظام إدارة محتوى خارجي. أمان في الأنواع، بحيث يكون المفتاح المفقود خطأً في البناء لا فراغاً في الإنتاج. وتصيير من جهة الخادم، لأن موقعاً شخصياً يرسل حزمة ترجمة إلى المتصفح لنصوص لا تتغير أبداً هو موقع يهدر عرض النطاق على الجميع. معظم مكتبات الترجمة مبنية لمشكلة أصعب بكثير من مشكلتي: عشرات اللغات، وتسليم الملفات إلى مترجمين محترفين، وقواعد جمع في لغات تملك ستة أشكال للجمع، وتبديل اللغة أثناء التشغيل في تطبيق أحادي الصفحة. لم يكن لدي أي من ذلك. وما حصلت عليه مقابل تلك الميزات كان مزوّداً اضطررت لتوصيله مرتين، ومفهوم فضاءات أسماء لم أكن بحاجة إليه، وأنواع TypeScript مولّدة لا مستنتجة، أي خطوة بناء إضافية تفصل بيني وبين اقتراح الإكمال التلقائي. هذا التعارض ليس خطأ المكتبة. أنا كنت أدفع ثمن حل عام لمشكلة خاصة، والمشكلة الخاصة كانت تتسع لملف واحد.
02شكل الحل
النظام بأكمله هو ملف JSON واحد لكل لغة، ووحدة واحدة تحمّلهما. كل قاموس يُستورد ديناميكياً، فلا تدخل حزمة الخادم إلا اللغة النشطة. والملف الإنكليزي هو مصدر الحقيقة للأنواع: أستنتج نوع القاموس من القيمة المُعادة لاستيراده، ما يعني أن كل مفتاح أضيفه إلى الإنكليزية يصبح موجوداً في نظام الأنواع فوراً دون أي خطوة توليد. وإذا انحرفت العربية فلن يلتقط TypeScript ذلك مباشرة، لذلك أضفت فحص تطابق صغيراً يمشي على الملفين ويقارن مسارات المفاتيح بعد تسطيحها. مكوّنات الخادم تستدعي دالة غير متزامنة تعيد القاموس كاملاً، ثم تفكّك منه القسم الذي تحتاجه. ومكوّنات العميل تقرأ الكائن نفسه عبر مزوّد سياق يملؤه التخطيط مرة واحدة. لا يوجد خطاف يجلب البيانات، ولا حدود تعليق، ولا حالة تحميل. الترجمات مجرد خصائص تصل مصادفةً على هيئة سياق. وأكثر قطعة تبيّن أنها مفيدة كانت أصغرها: دالة تنسيق تستبدل العناصر النائبة ذات القوسين المزدوجين بالقيم، بحيث يستطيع النص القالبي أن يحمل اسماً أو عدداً دون دمج نصوص متناثر عبر المكوّنات.
03التوجيه ومقطع اللغة
اللغة تعيش في الرابط بوصفها المقطع الأول من المسار. كل صفحة تقع تحت مقطع ديناميكي، والوسيط يتكفّل بمن يصل دون مقطع لغة. وترتيب الحسم أهم مما يبدو: الكوكي الصريح يفوز أولاً، لأن الزائر الذي بدّل اللغة عن قصد لا يجب أن يُتجاوز قراره في زيارته التالية. وإن لم يوجد، يُفاوَض ترويسة Accept-Language مقابل قائمة اللغات المدعومة. والإنكليزية هي البديل الافتراضي. أعيد التوجيه بحالة دائمة بدل إعادة الكتابة، لأنني أريد الرابط المعياري ظاهراً في شريط العنوان وفي أي شيء ينسخه الزائر إلى رسالة. إعادة الكتابة كانت ستبقي المسار المجرّد في الشريط وتقدّم منه محتوى مترجماً بصمت، ما يخلق رابطين لصفحة واحدة ويشتّت ترتيبهما في محركات البحث. والتفصيل الذي أخطأت فيه أول مرة كان سلسلة الاستعلام. إعادة التوجيه إلى المسار المسبوق باللغة دون حمل معاملات الاستعلام كانت تسقط المرشحات وكلمات البحث بصمت كلما شارك أحدهم رابطاً. سطر واحد لإصلاحه، وعشرون دقيقة لملاحظته.
04ما لن أكرره
لهذا النهج سقف، وأنا أراه من هنا. في اللحظة التي تصل فيها لغة ثالثة، تتوقف الصيانة اليدوية لملفات JSON المتوازية عن كونها ممتعة، ويتحوّل سكربت التطابق إلى بوابة بناء حقيقية بدل أن يكون وسيلة راحة. وإذا احتاج شخص غير مبرمج يوماً إلى تحرير النصوص، فإن JSON داخل مستودع واجهة عدائية، وسأنقل المحتوى حينها خلف شيء يملك نموذج إدخال. أما الجمع فهو نقطة الضعف الصادقة. دالة التنسيق عندي تستبدل ولا تفعل شيئاً آخر، لذا فإن أي نص يتغير شكله مع العدد يحتاج مفتاحاً منفصلاً لكل حالة، والعربية تملك حالات أكثر من الإنكليزية. لقد تجنّبت المشكلة بدل أن أحلّها، وهذا مقبول في موقع شخصي وغير مقبول في منتج. أما ما سأحتفظ به في أي مشروع بهذا الحجم فهو المبدأ الكامن تحته. استعمل المكتبة حين تكون المشكلة فعلاً مشكلة المكتبة. لغتان من النصوص التعريفية الثابتة ليست تلك المشكلة، والستون سطراً لم تحتج صيانة تُذكر منذ الأسبوع الذي كتبتها فيه.
الخلاصة
كتابة نظام ترجمة خاص نصيحة سيئة كقاعدة عامة، وكانت القرار الصحيح هنا. السؤال الحاسم لم يكن يوماً هل المكتبة جيدة، بل هل مشكلتي هي الشكل الذي بُنيت المكتبة من أجله. اكتب ما تحتاجه فعلاً قبل أن تثبّت أي شيء، وغالباً ما تكون الإجابة واضحة.