دليل React Native للعربية (1): إعداد الترجمة والاتجاه

إعداد i18next و react-i18next بصورة قابلة للاختبار، وفصل لغة المحتوى عن اتجاه واجهة React Native.

كل المقالات
دليل React Native للعربية (1): إعداد الترجمة والاتجاه

التعريب في React Native له مستويان مختلفان. الأول هو لغة النصوص والتواريخ والأرقام. الثاني هو اتجاه التخطيط الأصلي على iOS و Android. تغيير اللغة في i18next لا يقلب واجهة React Native تلقائياً، وتغيير I18nManager لا يترجم النصوص.

فصل المستويين منذ البداية يمنع كثيراً من أخطاء زر اللغة وإعادة التشغيل.

ثبت أقل مجموعة تحتاجها

npm install i18next react-i18next

تعتمد الإصدارات الحالية من i18next على Intl.PluralRules لصيغ الجمع. تحقق من بيئة React Native التي يدعمها مشروعك قبل إضافة polyfill. لا تضف intl-pluralrules تلقائياً إلى كل تطبيق؛ الإصدارات الحديثة من المحركات قد توفر المطلوب بالفعل.

أنشئ ملفين بسيطين للترجمة. محتوى locales/ar.json:

{
  "common": {
    "save": "حفظ",
    "cancel": "إلغاء"
  },
  "cart": {
    "items_zero": "السلة فارغة",
    "items_one": "عنصر واحد",
    "items_two": "عنصران",
    "items_few": "{{count}} عناصر",
    "items_many": "{{count}} عنصراً",
    "items_other": "{{count}} عنصر"
  }
}

ومحتوى locales/en.json:

{
  "common": {
    "save": "Save",
    "cancel": "Cancel"
  },
  "cart": {
    "items_one": "{{count}} item",
    "items_other": "{{count}} items"
  }
}

صيغة لاحقات الجمع الحالية هي صيغة JSON v4. لا تضبط خيار compatibilityJSON على الإصدار الثالث في إعداد جديد؛ دليل ترقية i18next يوضح أن خيار التوافق يقبل v4 فقط في الإصدارات الحالية.

هيئ i18next مرة واحدة

// i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import ar from "./locales/ar.json";
import en from "./locales/en.json";

export const supportedLanguages = ["ar", "en"] as const;
export type AppLanguage = (typeof supportedLanguages)[number];

export function isAppLanguage(value: unknown): value is AppLanguage {
  return (
    typeof value === "string" &&
    supportedLanguages.includes(value as AppLanguage)
  );
}

i18n.use(initReactI18next).init({
  resources: {
    ar: { translation: ar },
    en: { translation: en },
  },
  supportedLngs: supportedLanguages,
  lng: "ar",
  fallbackLng: "en",
  interpolation: {
    escapeValue: false,
  },
  returnNull: false,
});

export default i18n;

escapeValue: false مناسب لنصوص React لأن React يعالج إخراج النص. لا تستخدم هذه المعلومة ذريعة لإدخال HTML قادم من مصدر غير موثوق.

استورد ملف التهيئة قبل عرض المكونات:

// index.js
import "./src/i18n";
import { AppRegistry } from "react-native";
import App from "./src/App";
import { name as appName } from "./app.json";

AppRegistry.registerComponent(appName, () => App);

وفي المكون:

import { Text, Pressable } from "react-native";
import { useTranslation } from "react-i18next";

export function SaveButton({ onPress }: { onPress: () => void }) {
  const { t } = useTranslation();

  return (
    <Pressable accessibilityRole="button" onPress={onPress}>
      <Text>{t("common.save")}</Text>
    </Pressable>
  );
}

لا تثق في اللغة المخزنة دون تحقق

قد تحتوي مساحة التخزين قيمة قديمة أو غير صالحة بعد تحديث التطبيق. اقرأها، تحقق منها، ثم اختر لغة احتياطية:

const stored = await localeStorage.get();
const initialLanguage: AppLanguage = isAppLanguage(stored) ? stored : "ar";

await i18n.changeLanguage(initialLanguage);

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

عند تغيير اللغة، انتظر اكتمال العملية قبل حفظ الاختيار أو إظهار نجاح زائف:

export async function changeAppLanguage(language: AppLanguage) {
  await i18n.changeLanguage(language);
  await localeStorage.set(language);
}

تعامل مع المفاتيح الناقصة كخطأ تطوير

عرض اسم المفتاح للمستخدم يخفي المشكلة حتى تصل إلى الإنتاج. أنشئ فحصاً يستخدم في الاختبارات والأجزاء الحساسة:

export function assertTranslation(key: string, language: AppLanguage) {
  if (!i18n.exists(key, { lng: language, fallbackLng: false })) {
    throw new Error(`Missing translation: ${language}.${key}`);
  }
}

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

test("rejects a key that exists only in the fallback language", () => {
  i18n.addResource(
    "en",
    "translation",
    "test.onlyInEnglish",
    "English fallback",
  );

  expect(() => assertTranslation("test.onlyInEnglish", "ar")).toThrow(
    "Missing translation: ar.test.onlyInEnglish",
  );
});

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

افهم ما يفعله I18nManager

يوفر I18nManager.isRTL حالة اتجاه التخطيط الأصلي. تعتمد النتيجة على إعدادات التطبيق والجهاز، وعلى android:supportsRTL في Android واللغات المعلنة في مشروع iOS.

import { I18nManager } from "react-native";

I18nManager.allowRTL(true);

تذكر أن تغيير allowRTL() يطبق عند بدء التطبيق التالي، وليس فوراً. أما forceRTL() فتوثيق React Native يصفه كأداة للتطوير والاختبار، وينصح بتجنبه في الإنتاج لأنه يحتاج إعادة تشغيل كاملة ويقدم تجربة سيئة.

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

export const directionFor = (language: AppLanguage) =>
  language === "ar" ? "rtl" : "ltr";

لا تخلط اللغة بالمنطقة

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

const amount = new Intl.NumberFormat("ar-QA", {
  style: "currency",
  currency: "QAR",
}).format(1250);

لا تخزن القيمة المنسقة في قاعدة البيانات. خزن الرقم والعملة، ثم نسق العرض حسب اللغة والمنطقة.

اختبارات البداية

قبل الانتقال إلى تفاصيل التخطيط، تحقق من الآتي:

  1. يبدأ التطبيق بلغة صالحة عند أول تثبيت.
  2. تعاد اللغة المخزنة بعد إغلاق التطبيق وفتحه.
  3. تعمل اللغة الاحتياطية عند غياب قيمة ترجمة.
  4. تفشل اختبارات التطوير عند غياب مفتاح مطلوب.
  5. تعمل صيغ الجمع العربية مع 0 و1 و2 و3 و11 و100.
  6. يطابق اتجاه التنقل اتجاه المحتوى المختار.
  7. لا يستعمل كود الإنتاج forceRTL() لإجبار إعادة التشغيل.

المراجع الرسمية