انتقل إلى المحتوى
Avence Labs
العودة إلى المدونة
TypeScript

جعل الترجمة الناقصة خطأ برمجيًا وقت البناء، لا مفاجأة وقت التشغيل

نُشر في 27 أبريل 20265 دقيقة قراءة

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

الحلول المعتادة، ولماذا تجاوزناها

الحلول الشائعة هي سكريبت linter يقارن مجموعات المفاتيح عبر ملفات JSON (يعمل في CI، يكتشف الانحراف بعد التزام واحد متأخر)، أو احتياطي وقت التشغيل يعرض بصمت النص الإنجليزي عند غياب الترجمة (يخفي الخطأ بدلًا من إصلاحه، والنص المفقود "مؤقتًا" له طريقة للبقاء مفقودًا). كلاهما يعامل شكل القاموس كبيانات يجب فحصها، لا كنوع يجب فرضه.

ما فعلناه بدلًا من ذلك

القواميس كائنات TypeScript عادية، واحد لكل لغة، والملف الإنجليزي هو المرجعي:

const en = {
  hero: { headline: "...", body: "..." },
  nav: { items: ["Home", "About", "Contact"] },
} as const;

كل لغة أخرى تُكتب باستخدام satisfies Dictionary، حيث يُشتق Dictionary من الكائن الإنجليزي نفسه باستخدام نوع مُعيَّن تكراري:

type Widen<T> = T extends string ? string
  : T extends number ? number
  : T extends object ? { readonly [K in keyof T]: Widen<T[K]> }
  : T;

export type Dictionary = Widen<typeof en>;

يمرّ Widen عبر بنية القاموس الإنجليزي ويستبدل كل سلسلة حرفية بنوع string العام — فالترجمة حرة في استخدام كلمات مختلفة تمامًا، بينما شكل الكائن الذي تعيش فيه مقفل ليطابق الإنجليزية تمامًا، مفتاحًا بمفتاح، ومستوى تداخل بمستوى.

لماذا تهم حالة الصفوف الثابتة (tuple)

يعمل النوع المُعيَّن على المصفوفات كما يعمل على الكائنات، مما يعني أنه يحافظ على الطول، لا أسماء المفاتيح فقط. nav.items أعلاه هو صف ثابت من ثلاث سلاسل في الملف الإنجليزي، لذا Dictionary["nav"]["items"] هو أيضًا صف ثابت من ثلاثة — وليس string[]. إذا أدرج ملف مترجَم عنصرين أو أربعة، يبلّغ TypeScript عند موقع الاستدعاء بالضبط: Source has 2 element(s) but target requires 3، أثناء next build، قبل شحن أي شيء. نعتمد على هذا باستمرار: قوائم التنقل، قوائم الميزات، وصفوف الإحصائيات كلها مؤلَّفة كصفوف ثابتة الطول تحديدًا حتى لا يستطيع مترجم إسقاط أو تكرار إدخال دون أن يكتشفه البناء.

المقايضة

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