المؤلف:
    إنشاء:2026-09-02آخر تحديث:2026-09-02

    كيف تختبر الترجمات دون كتابة اختبارات هشة

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

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

    الأنماط مستقلة عن المكتبة المستخدمة

    يعمل كل نمط موضح أدناه على أي بيئة i18n. استبدل الموفر بـ I18nextProvider أو NextIntlClientProvider أو IntlProvider وستظل الاختبارات متطابقة تماماً، لأنها تؤكد على المخرجات المعروضة بدلاً من واجهة برمجة التطبيقات لمكتبة معينة.

    أدوات التغطية قابلة للنقل أيضاً: مع إضافة Sync JSON الموجهة إلى كتالوجاتك الحالية، أو محول التوافق الذي يربط استيراداتك الحالية، يعمل تأكيد التغطية مباشرة على ملفات JSON الموجودة لديك بالفعل.

    حدد ما تختبره بالفعل

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

    ما يستحق الاختبار فعلياً هو الأمور الميكانيكية:

    يستحق الاختبار لا يستحق الاختبار
    كل لغة مطلوبة تحتوي على قيمة ما إذا كانت الصياغة اللغوية جيدة
    وصول اللغة الصحيحة إلى المكون البرمجي النص المطابق لكل تسمية أو زر
    صيغ الجمع تعمل لكل تصنيف لغوي ما إذا كان المترجم أدى عمله بدقة
    لغات RTL تضبط الاتجاه وانعكاس الواجهة كل نص في كل لغة
    التواريخ والأرقام المنسقة تتبع اللغة صحة التنفيذ الداخلي لـ Intl

    فحص التغطية يتبع لاختبار واحد موجه بالبيانات، وليس لاختبارات المكونات الفردية. تمت تغطية هذا بالتفصيل في اكتشاف الترجمات المفقودة؛ يتناول هذا المقال بقية الجوانب.

    العرض داخل موفر (Provider) والتأكيد عبر الدور (Role)

    النمط الأساسي هو تركيب المكون داخل موفر لغة والاستعلام عبر الدور (Role) أو معرّف الاختبار (Test ID) بدلاً من النص الحرفي.

    CartSummary.test.tsx
    import { render, screen } from "@testing-library/react";
    import { IntlayerProvider } from "react-intlayer/client";
    import { CartSummary } from "./CartSummary";
    
    test("يعرض عنوان الملخص بالفرنسية", () => {
      render(
        <IntlayerProvider locale="fr-FR">
          <CartSummary />
        </IntlayerProvider>
      );
    
      expect(screen.getByRole("heading")).toBeInTheDocument();
    });
    

    الاستعلام عبر getByRole("heading") يظل صامداً عند تعديل النص. أما getByText("Récapitulatif") فيفشل فور تعديل الصياغة. لا تستخدم النصوص الحرفية إلا عندما يكون النص نفسه هو محل الاختبار، وهو أمر نادر.

    بالنسبة لسمات مثل aria-label، ستحتاج إلى النص الخام بدلاً من عنصر قابل للعرض. في React، توفر عناصر useIntlayer حقلاً باسم .value لهذا الغرض.

    جعل الاختبارات متعددة اللغات عبر المعاملات (Parameterization)

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

    direction.test.tsx
    import { getHTMLTextDir } from "intlayer";
    import { render } from "@testing-library/react";
    import { IntlayerProvider } from "react-intlayer/client";
    
    describe.each(["en", "fr", "ja", "ar"])("locale %s", (locale) => {
      it("يعرض المحتوى دون التراجع إلى المفتاح الخام", () => {
        const { container } = render(
          <IntlayerProvider locale={locale}>
            <CartSummary />
          </IntlayerProvider>
        );
    
        // ظهور المفتاح يعني فشل العثور على الترجمة.
        expect(container.textContent).not.toMatch(/^[a-z]+(\.[a-z]+)+$/);
      });
    
      it("يضبط اتجاه النص الصحيح", () => {
        expect(getHTMLTextDir(locale)).toBe(locale === "ar" ? "rtl" : "ltr");
      });
    });
    

    التأكيد الأول فوز عام وغير مكلف: إذا فشل العثور على الترجمة وعرضت المكتبة المفتاح، فسيحتوي الـ DOM على نص بصيغة cart.summary.title. يكتشف هذا فئة كاملة من الأخطاء دون الحاجة لتحديد أي نص بعينه.

    التوطين الزائف (Pseudolocalization) يكتشف ما تخفيه الكتالوجات

    أضف لغة تجريبية تقوم بتحويل كل نص، على سبيل المثال تحويل Checkout إلى [!!! Çĥéçķöũţ !!!]. ثم اعرض الصفحة بتلك اللغة.

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

    من الأفضل تشغيل هذا كفحص بصري أو من البداية للنهاية (E2E) بدلاً من اختبار وحدة، لأن المشكلة تظهر للعين مباشرة.

    صيغ الجمع تحتاج اختباراً لكل تصنيف، لا لكل لغة

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

    plural.test.ts
    // تختبر العربية zero, one, two, few, many, other.
    describe.each([0, 1, 2, 3, 11, 100])("العدد %i", (count) => {
      it("ينتج نصاً غير فارغ بالعربية", () => {
        expect(formatItems(count, "ar")).not.toBe("");
      });
    });
    

    اختر أعداداً تغطي كل تصنيف من تصنيفات CLDR لأصعب لغة لديك بدلاً من اختبار 1 و 2 في كل مكان. تخبرك Intl.PluralRules بالتصنيف الذي يقع فيه الرقم، مما يتيح لك استنتاج مجموعة العينات دون تخمين. المزيد عن التصنيفات في مقال تنسيق رسائل ICU.

    فخ لقطات الشاشة (Snapshots)

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

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

    اختبر آلية التفاوض واختيار اللغة، لا مجرد العرض

    خطأ i18n الأكثر شيوعاً في الإنتاج ليس نصاً مفقوداً. بل هو اختيار لغة خاطئة: يطلب الرابط /fr/، بينما يقرأ العميل navigator.language، ويحدث تعارض.

    اختبر ترتيب اختيار اللغة مباشرة، كدالة نقية، بمعزل عن أي مكون واجهة:

    locale-resolution.test.ts
    it("يفضل الرابط على التفضيل المخزن", () => {
      expect(resolveLocale({ url: "/fr/about", stored: "de", header: "ja" })).toBe(
        "fr"
      );
    });
    
    it("يتراجع إلى الترويسة عندما لا يحتوي الرابط على بادئة", () => {
      expect(resolveLocale({ url: "/about", stored: null, header: "ja" })).toBe(
        "ja"
      );
    });
    

    هذا هو الاختبار الأكثر قيمة في i18n والذي تفتقده معظم المشاريع البرمجية، وهو لا يحتاج إلى DOM على الإطلاق.

    ما يجب تشغيله وأين

    • الوحدة (Unit): اختيار اللغة، أدوات التنسيق، تصنيفات الجمع. سريع، دون DOM.
    • المكونات (Component): عرض واحد قائم على الموفر لكل لغة، للتأكد من الأدوار وعدم ظهور مفاتيح خام.
    • التغطية (Coverage): اختبار موجه بالبيانات للتأكد من عدم وجود لغات مطلوبة مفقودة.
    • البصري أو E2E: فحص التوطين الزائف وصفحة RTL، لأن تلك الأخطاء بصرية بحتة.

    احتفظ بالثلاثة الأولى في خط الأنابيب (CI) مع كل commit. الاختبار الأخير غير مكلف في البناء الليلي ومكلف مع كل دفع للكود.

    أخطاء شائعة

    • التأكيد على النصوص الحرفية في كل مكان. يضمن حذف مجموعة الاختبارات خلال أشهر قليلة.
    • أخذ لقطات شاشة لمكونات مترجمة. يعطل المترجمون البناء ويوافق المراجعون دون تدقيق.
    • اختبار اللغة الافتراضية فقط. اللغة الوحيدة التي يستحيل أن تكون مفقودة.
    • اختبار 1 و 2 فقط للجموع. يتجاهل كافة الفئات التي لا توجد في الإنجليزية.
    • محاكاة مكتبة i18n بشكل كامل (Mocking). حينها تختبر فقط ما إذا كان الـ Mock يعيد نصوصاً.
    • عدم اختبار منطق التفاوض واختيار اللغة. المشكلة الأكثر شيوعاً في بيئات الإنتاج والأسهل اختباراً.

    للمزيد من التفاصيل

    التعليقات

    لا توجد تعليقات بعد. كن أول من يشارك أفكاره.

    مقالات ذات صلة

    آخر المقالات