نجاح الشاشة الرئيسية بالعربية لا يعني أن التعريب اكتمل. تظهر المشكلات عادة بعد استعادة لغة مخزنة، أو في رقم داخل حقل، أو عند وصول خطأ من الخادم، أو عندما يغير المستخدم اللغة والتطبيق مفتوح.
هذه الحالات تحتاج عقد بيانات واختبارات، لا مجموعة استبدالات موزعة داخل المكونات.
لا تستخدم forceRTL لتبديل لغة الإنتاج
توثيق React Native يصف I18nManager.forceRTL() كأداة للتطوير والاختبار، وينصح بتجنبها في تطبيقات الإنتاج. التغيير الكامل لا يطبق إلا عند بدء التطبيق التالي، وإجبار إعادة التشغيل يقطع ما كان المستخدم يفعله.
افصل تغيير النص عن تغيير اتجاه الواجهة:
const direction = language === "ar" ? "rtl" : "ltr";
await i18n.changeLanguage(language);
await localeStorage.set(language);
مرر direction إلى جذر التخطيط وNavigationContainer والمكونات التي تحتاجه. إذا كان جزء أصلي لا يستطيع تغيير اتجاهه أثناء التشغيل، وثق القيد للمستخدم وطبقه في بدء التشغيل التالي. لا تخفِ إعادة تشغيل إجبارية خلف زر يبدو فورياً.
في التطوير فقط يمكن استخدام forceRTL() لفحص شاشة تحت RTL بغض النظر عن لغة الجهاز. أعده إلى قيمته الطبيعية بعد الاختبار حتى لا يؤثر في جلسات لاحقة، لأن الإعداد محفوظ بين مرات التشغيل.
تحقق من اللغة المخزنة
قد تتغير قائمة اللغات المدعومة بين نسخ التطبيق. لا تمرر قيمة التخزين مباشرة إلى i18next:
const supported = new Set(["ar", "en"]);
export function resolveLanguage(value: unknown) {
return typeof value === "string" && supported.has(value) ? value : "ar";
}
اختبر قيمة فارغة، وقيمة قديمة مثل ar_QA إذا كان التطبيق انتقل إلى ar-QA، وبيانات تخزين تالفة. لا تجعل التطبيق يبقى على شاشة تحميل لأن تهيئة اللغة فشلت.
نسق الأرقام للعرض ولا تغير البيانات
احتفظ بالأموال والكميات كأرقام، ثم نسقها عند العرض:
const formatter = new Intl.NumberFormat("ar-QA", {
style: "currency",
currency: "QAR",
});
formatter.format(1250.5);
شكل الأرقام قرار منتج مستقل عن اتجاه الشاشة. بعض المنتجات العربية تستخدم الأرقام العربية، وأخرى تستخدم اللاتينية. تستطيع تحديد نظام الترقيم باستخدام امتداد locale عندما يكون ذلك مطلوباً:
new Intl.NumberFormat("ar-QA-u-nu-arab").format(1250);
new Intl.NumberFormat("ar-QA-u-nu-latn").format(1250);
لا تحول كل رقم في نص باستخدام regex عند العرض. ذلك قد يغير رقم هاتف أو معرف طلب أو جزءاً من رابط، وقد يفقد الفواصل العشرية والتجميع الصحيحة.
طبع مدخل المستخدم قبل التحليل
قد يكتب المستخدم أرقاماً عربية أو فارسية داخل حقل كمية. طبع المحارف عند حدود الإدخال، ثم تحقق من القيمة:
const arabicIndicDigits = "٠١٢٣٤٥٦٧٨٩";
const easternArabicDigits = "۰۱۲۳۴۵۶۷۸۹";
export function normalizeDigits(input: string) {
return input
.replace(/[٠-٩]/g, (digit) => String(arabicIndicDigits.indexOf(digit)))
.replace(/[۰-۹]/g, (digit) => String(easternArabicDigits.indexOf(digit)));
}
ثم عالج الفاصل العشري وفق قواعد الحقل، لا باستبدال عالمي غير موثق. مثال المبلغ يحتاج سياسة مختلفة عن رقم الهاتف. الدالة التالية تقبل أرقاماً عشرية بسيطة بعلامة اختيارية، وترفض النص الفارغ وفواصل التجميع وأي محارف إضافية:
export const INVALID_LOCALIZED_NUMBER = "INVALID_LOCALIZED_NUMBER";
const decimalNumberPattern = /^[+-]?(?:\d+|\d+\.\d+|\.\d+)$/;
export function parseLocalizedNumber(input: string) {
const normalized = normalizeDigits(input).trim();
if (normalized === "" || !decimalNumberPattern.test(normalized)) {
throw new Error(INVALID_LOCALIZED_NUMBER);
}
const value = Number(normalized);
if (!Number.isFinite(value)) {
throw new Error(INVALID_LOCALIZED_NUMBER);
}
return value;
}
استخدم رمز خطأ ثابتاً في التطوير والإنتاج، ولا تضع المدخل الخام في الرسالة أو السجل لأنه قد يحتوي بيانات شخصية. اختبر الصفر منفصلاً عن القيمة الفارغة، واختبر أن الخطأ لا يعيد النص الحساس:
import {
INVALID_LOCALIZED_NUMBER,
parseLocalizedNumber,
} from "./parseLocalizedNumber";
describe("parseLocalizedNumber", () => {
it.each([
["0", 0],
["٠", 0],
["۱۲.۵", 12.5],
["-3.25", -3.25],
])("parses %s", (input, expected) => {
expect(parseLocalizedNumber(input)).toBe(expected);
});
it.each(["", " ", "12foo", "1,000"])("rejects %j", (input) => {
expect(() => parseLocalizedNumber(input)).toThrow(INVALID_LOCALIZED_NUMBER);
});
it("does not expose the raw input", () => {
const privateInput = "رقم-عميل-123";
try {
parseLocalizedNumber(privateInput);
throw new Error("Expected parsing to fail");
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe(INVALID_LOCALIZED_NUMBER);
expect((error as Error).message).not.toContain(privateInput);
}
});
});
أرسل اللغة إلى الخادم ضمن عقد واضح
يمكن إرسال Accept-Language مع الطلب:
const response = await fetch(url, {
headers: {
"Accept-Language": language,
"Content-Type": "application/json",
},
});
لكن الترويسة لا تضمن أن الخادم سيترجم. اتفق على اللغات المدعومة والسقوط عند غياب الترجمة.
الأفضل لأخطاء الأعمال أن يعيد الخادم رمزاً ثابتاً وبيانات، ويختار العميل النص المناسب:
{
"code": "ORDER_ALREADY_PAID",
"details": {
"orderId": "QA-2048"
}
}
const errorKeyByCode = {
ORDER_ALREADY_PAID: "errors.orderAlreadyPaid",
ORDER_NOT_FOUND: "errors.orderNotFound",
} as const;
export function errorKey(code: string) {
const key = errorKeyByCode[code as keyof typeof errorKeyByCode];
if (!key) {
throw new Error(`Unknown API error code: ${code}`);
}
return key;
}
في الإنتاج يمكن عرض رسالة عامة للرمز غير المعروف مع تسجيل منقح، لكن يجب أن يفشل الاختبار والتطوير عند ظهور رمز بلا معالجة. لا تعرض نص خطأ تقني من الخادم للمستخدم لمجرد أنه عربي.
أدخل اللغة في مفاتيح التخزين المؤقت
إذا كانت استجابة /articles تختلف حسب Accept-Language، فلا تخزن اللغتين تحت المفتاح نفسه:
const cacheKey = ["articles", language];
ينطبق ذلك على React Query أو أي طبقة cache أخرى. عند تغيير اللغة، أعد طلب البيانات المحلية أو أبطل المفتاح السابق وفق حاجة المنتج. وإلا قد تعرض الشاشة عناوين عربية بعد تحول عناصر التحكم إلى الإنجليزية.
لا تنس التواريخ والمنطقة الزمنية
التاريخ المخزن كسلسلة غامضة مثل 08/09/2026 لا يحدد هل الشهر 8 أم 9. استخدم قيمة زمنية موحدة من الخادم، ونسقها للعرض باستخدام locale والمنطقة الزمنية المطلوبة:
const formatted = new Intl.DateTimeFormat("ar-QA", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "Asia/Qatar",
}).format(new Date(isoTimestamp));
لا تفترض أن لغة المستخدم تحدد منطقته الزمنية. قد يستخدم شخص العربية خارج قطر أو الإنجليزية داخلها.
النص المختلط يحتاج حالات اختبار
اختبر جملة عربية تحتوي:
- بريداً إلكترونياً.
- رابطاً يبدأ بـ
https://. - رقم هاتف دولياً.
- كود طلب لاتينياً.
- أقواساً وعلامة نقطتين.
استخدم textAlign: 'auto' كنقطة بداية. على iOS توجد writingDirection, لكنها ليست خاصية مشتركة لكل المنصات. إذا تعذر عرض قيمة حساسة بوضوح، ضعها في عنصر Text منفصل باتجاه ومحاذاة معروفين، ولا تدخل أحرف تحكم Unicode متناثرة من دون اختبار وفهم.
مصفوفة اختبار تمنع الرجوع
اختبر المسارات التالية على iOS و Android:
- أول تثبيت بلغة جهاز عربية ثم إنجليزية.
- لغة تطبيق مخزنة تختلف عن لغة الجهاز.
- تغيير اللغة أثناء وجود modal أو شاشة تنقل عميقة.
- إغلاق التطبيق وفتحه بعد تغيير اللغة.
- الأعداد 0 و1 و2 و3 و11 و100 لصيغ الجمع.
- إدخال أرقام عربية وفارسية ولاتينية.
- مبلغ سالب وقيمة عشرية ورقم هاتف.
- استجابة API مترجمة وغير مترجمة ورمز خطأ غير معروف.
- بيانات cache موجودة قبل تغيير اللغة.
- رابط عميق يفتح شاشة قبل اكتمال تهيئة اللغة.
اكتب الاختبارات حول الدوال الصغيرة مثل resolveLanguage وnormalizeDigits وerrorKey. ثم أضف اختبار واجهة لمسار تغيير اللغة. لقطة شاشة وحدها لا تكشف خطأ cache أو رمز API مفقود.