تعدد اللغات
يتضمن FORGE نظام تدويل (i18n) شامل يدعم لغات متعددة وتخطيطات من اليمين لليسار (RTL) ومحتوى قاعدة بيانات قابل للترجمة وسير عمل ترجمة كامل للواجهة الأمامية. يغطي النظام كلاً من سلاسل واجهة المستخدم الثابتة والمحتوى الديناميكي المُدار من المستخدم.
نظرة عامة
يتكون نظام تعدد اللغات من ثلاث طبقات:
┌─────────────────────────────────────────────────┐
│ نظام تعدد اللغات │
├─────────────────────────────────────────────────┤
│ │
│ 1. جدول اللغات │
│ تعريف اللغات المتاحة والاتجاهات │
│ والإعدادات الافتراضية │
│ │
│ 2. جدول الترجمات │
│ سلاسل واجهة المستخدم الثابتة (النصوص، │
│ الرسائل، أخطاء التحقق) │
│ │
│ 3. عمود الترجمات JSONB │
│ المحتوى الديناميكي على النماذج القابلة │
│ للترجمة (المحتويات، القوائم، البحث) │
│ │
└─────────────────────────────────────────────────┘مخطط قاعدة البيانات
جدول اللغات
يُعرّف جدول languages جميع اللغات المتاحة:
| العمود | النوع | الوصف |
|---|---|---|
id | UUID | المفتاح الأساسي |
code | VARCHAR(10) | رمز اللغة ISO (مثل en، ar، fr) |
name | VARCHAR(100) | الاسم بالإنجليزية (مثل "Arabic") |
native_name | VARCHAR(100) | الاسم الأصلي (مثل "العربية") |
direction | VARCHAR(3) | اتجاه النص: ltr أو rtl |
is_default | BOOLEAN | ما إذا كانت هذه اللغة الافتراضية |
is_active | BOOLEAN | ما إذا كانت اللغة مُفعّلة |
created_at | TIMESTAMP | طابع زمني لإنشاء السجل |
updated_at | TIMESTAMP | طابع زمني لآخر تحديث |
جدول الترجمات
يخزن جدول translations سلاسل واجهة المستخدم الثابتة:
| العمود | النوع | الوصف |
|---|---|---|
id | UUID | المفتاح الأساسي |
group_name | VARCHAR(100) | مجموعة الترجمة (مثل auth، validation) |
key | VARCHAR(255) | مفتاح الترجمة (مثل login_button) |
translations | JSONB | القيم لجميع اللغات |
created_at | TIMESTAMP | طابع زمني لإنشاء السجل |
updated_at | TIMESTAMP | طابع زمني لآخر تحديث |
بنية JSONB للترجمات
{
"en": "Login",
"ar": "تسجيل الدخول",
"fr": "Connexion"
}النماذج القابلة للترجمة
النماذج التي تدعم الترجمات (المحتويات، القوائم، البحث) تستخدم عمود translations من نوع JSONB لتخزين نسخ حسب اللغة لحقولها:
{
"en": {
"title": "About Us",
"summary": "Learn about our company"
},
"ar": {
"title": "من نحن",
"summary": "تعرف على شركتنا"
}
}نصيحة
نهج JSONB يُبقي الترجمات مع سجلاتها الأصلية، متجنباً استعلامات JOIN الثقيلة. عند جلب سجل، تحل الخلفية اللغة الصحيحة بناءً على سياق الطلب وتُرجع الترجمة ذات الصلة فقط.
واجهة الإدارة
محرر الترجمة
توفر لوحة الإدارة محرر ترجمة جنباً إلى جنب لسلاسل واجهة المستخدم الثابتة:
- تصفية المجموعة -- تصفية الترجمات حسب المجموعة (auth، validation، common، إلخ.)
- البحث -- البحث عن الترجمات بالمفتاح أو القيمة
- العرض جنباً إلى جنب -- تحرير الترجمات لجميع اللغات في وقت واحد
- مؤشر المفقود -- السلاسل غير المترجمة تُميّز
تبويبات ترجمة النماذج
للنماذج القابلة للترجمة (المحتويات، القوائم، البحث)، تتضمن نماذج الإنشاء/التحرير تبويبات لغات. كل تبويب يعرض الحقول القابلة للترجمة لتلك اللغة.
┌──────────────────────────────────────────────┐
│ [English] [العربية] [Français] │
├──────────────────────────────────────────────┤
│ │
│ العنوان: [ من نحن ] │
│ │
│ الملخص: [ تعرف على شركتنا ] │
│ │
│ التفاصيل: ┌──────────────────────────────┐ │
│ │ محتوى محرر النص الغني... │ │
│ └──────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘تكامل الواجهة الأمامية
مزود i18n
تُحمّل الواجهة الأمامية الترجمات عبر مزود i18n يُغلّف التطبيق:
// components/providers/i18n-provider.tsx
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [locale, setLocale] = useState("en");
const [translations, setTranslations] = useState({});
useEffect(() => {
fetchTranslations(locale).then(setTranslations);
}, [locale]);
return (
<I18nContext.Provider value={{ locale, setLocale, translations }}>
{children}
</I18nContext.Provider>
);
}خطاف useTranslation
الوصول للترجمات في أي مكان في مكوناتك:
function LoginPage() {
const { t } = useTranslation();
return (
<div>
<h1>{t("auth.login_title")}</h1>
<button>{t("auth.login_button")}</button>
</div>
);
}تحل دالة t() المفتاح من ترجمات اللغة الحالية. إذا لم يُوجد المفتاح في اللغة النشطة، تتراجع إلى اللغة الافتراضية.
مكون LanguageSwitcher
مكون جاهز للتبديل بين اللغات النشطة:
// components/language-switcher.tsx
export function LanguageSwitcher() {
const { locale, setLocale, languages } = useTranslation();
return (
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.native_name}
</option>
))}
</select>
);
}دعم RTL
يدعم FORGE بشكل كامل اللغات من اليمين لليسار مثل العربية والعبرية:
اتجاه HTML
تُعيّن خاصية dir على عنصر <html> بناءً على اللغة النشطة:
<!-- لغة LTR (الإنجليزية) -->
<html lang="en" dir="ltr">
<!-- لغة RTL (العربية) -->
<html lang="ar" dir="rtl">اتجاه CSS
يتكيف CSS تلقائياً عندما تتغير خاصية dir. استخدم الخصائص المنطقية لتخطيطات متسقة:
/* Use logical properties instead of physical ones */
.sidebar {
margin-inline-start: 1rem; /* margin-left in LTR, margin-right in RTL */
padding-inline-end: 0.5rem; /* padding-right in LTR, padding-left in RTL */
}كتل الكود
مقتطفات الكود والمحتوى التقني تُعرض دائماً من اليسار لليمين بغض النظر عن اتجاه الصفحة:
/* Code blocks remain LTR in RTL layouts */
pre, code {
direction: ltr;
text-align: left;
}نصيحة
يدعم Tailwind CSS الـ RTL عبر متغير rtl:. على سبيل المثال، rtl:space-x-reverse يعكس التباعد الأفقي في وضع RTL.
أوامر CLI
يوفر FORGE أوامر CLI لإدارة اللغات:
# Add a new language
forge lang:add ar --name "Arabic" --native "العربية" --direction rtl
# Remove a language
forge lang:remove fr
# List all languages
forge lang:list
# Set the default language
forge lang:default enمرجع الأوامر
| الأمر | الوصف |
|---|---|
forge lang:add | إضافة لغة جديدة بالرمز والاسم والاتجاه |
forge lang:remove | إزالة لغة وترجماتها |
forge lang:list | عرض جميع اللغات المُعدّة |
forge lang:default | تعيين اللغة الافتراضية |
تحذير
إزالة لغة لا تحذف بيانات ترجمة JSONB الموجودة من سجلات النماذج. تبقى الترجمات في قاعدة البيانات لكنها لم تعد قابلة للوصول من خلال الواجهة أو API. هذا بالتصميم لمنع فقدان البيانات العرضي.
تفضيلات لغة المستخدم
يمكن لكل مستخدم تخزين لغة مفضلة في ملفه الشخصي. تُستخدم هذه التفضيلات للاتصالات المخصصة مثل رسائل SMS والبريد الإلكتروني والإشعارات الفورية.
مخطط قاعدة البيانات
يتضمن جدول users عمود language_id الذي يشير إلى جدول languages:
-- Users table (simplified)
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255),
mobile VARCHAR(20),
name VARCHAR(255),
language_id UUID REFERENCES languages(id) ON DELETE SET NULL,
-- ... other fields
);عند تسجيل مستخدم جديد، يُعيّن له تلقائياً اللغة الافتراضية للتطبيق. يمكن للمستخدمين تغيير تفضيلاتهم في أي وقت عبر API الملف الشخصي.
نقطة نهاية API الملف الشخصي
تحديث تفضيل لغة المستخدم:
# Change language to Arabic
curl -X PUT https://api.myapp.test/api/v1/profile/language \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"language_code": "ar"}'نص الطلب:
| الحقل | النوع | مطلوب | الوصف |
|---|---|---|---|
language_code | string | نعم | رمز اللغة ISO (مثل en، ar) |
الاستجابة:
{
"success": true,
"data": {
"message": "Language updated successfully",
"language_code": "ar"
}
}سلسلة التراجع
عند تحديد لغة المستخدم، يتبع النظام سلسلة التراجع التالية:
language_id للمستخدم → اللغة الافتراضية للتطبيق → الإنجليزية (مُثبتة)
↓ ↓ ↓
الخيار الأول إذا لم يُعيّن التراجع النهائي
المستخدم تفضيل إذا كانت DB غير متاحةرسائل SMS المترجمة
تُرسل رسائل SMS (رموز OTP، التحقق الثنائي، الإشعارات) بلغة المستخدم المفضلة.
مفاتيح ترجمة SMS
تُخزّن رسائل SMS في جدول translations بمجموعة sms:
| المجموعة | المفتاح | الوصف |
|---|---|---|
sms | otp_verification | رمز OTP لتسجيل الدخول بالجوال |
sms | otp_2fa | رمز التحقق الثنائي لتسجيل الدخول بكلمة المرور |
أمثلة الترجمات:
// sms.otp_verification
{
"en": "Your verification code is: {{code}}. Valid for {{minutes}} minutes.",
"ar": "رمز التحقق الخاص بك هو: {{code}}. صالح لمدة {{minutes}} دقائق."
}
// sms.otp_2fa
{
"en": "Your 2FA code is: {{code}}. Valid for {{minutes}} minutes.",
"ar": "رمز التحقق الثنائي: {{code}}. صالح لمدة {{minutes}} دقائق."
}العناصر النائبة في الرسائل
تدعم قوالب SMS العناصر النائبة التالية:
| العنصر النائب | الوصف | مثال |
|---|---|---|
| رمز OTP أو التحقق | 764067 |
| مدة الصلاحية بالدقائق | 10 |
كيف يعمل
عند إرسال OTP:
- مستخدم موجود: جلب
language_idللمستخدم من قاعدة البيانات، ربط معlanguagesللحصول على الرمز - مستخدم جديد: استخدام اللغة الافتراضية للتطبيق (متغير البيئة
DEFAULT_LANGUAGE) - جلب الترجمة: استعلام جدول
translationsلمفتاحsms.otp_verification - استبدال العناصر النائبة: تعويض
وبالقيم الفعلية - الإرسال: تسليم الرسالة المترجمة عبر مزود SMS المُعدّ
┌─────────────────────────────────────────────────────────────┐
│ تدفق إرسال OTP │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. يطلب المستخدم OTP │
│ ↓ │
│ 2. البحث عن تفضيل لغة المستخدم │
│ (أو استخدام الافتراضي للمستخدمين الجدد) │
│ ↓ │
│ 3. جلب قالب الرسالة المترجم من DB │
│ ↓ │
│ 4. استبدال العناصر النائبة {{code}} و {{minutes}} │
│ ↓ │
│ 5. الإرسال عبر مزود SMS (Twilio, Unifonic, Slack, إلخ.) │
│ │
└─────────────────────────────────────────────────────────────┘تكوين البيئة
تعيين اللغة الافتراضية لتطبيقك:
# .env
DEFAULT_LANGUAGE=ar # المستخدمون الجدد سيتلقون SMS بالعربية افتراضياًيجب أن تتطابق هذه القيمة مع أحد رموز اللغات المُعرّفة في جدول languages.
الاختبار باستخدام Slack
أثناء التطوير، استخدم مزود SMS الخاص بـ Slack لرؤية الرسائل المترجمة في قناة Slack:
SMS_DRIVER=slack
SMS_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
SMS_SLACK_CHANNEL=#sms-testإضافة قوالب SMS جديدة
لإضافة رسالة SMS مترجمة جديدة:
- إضافة مفتاح الترجمة:
INSERT INTO translations (id, group_name, key, translations, created_at, updated_at)
VALUES (
gen_random_uuid(),
'sms',
'password_reset',
'{"en": "Your password reset code is: {{code}}", "ar": "رمز إعادة تعيين كلمة المرور: {{code}}"}',
NOW(),
NOW()
);- استخدام خدمة SmsMessages في الكود:
// In your service
let message = sms_messages
.get_translation("sms", "password_reset", &user_lang)
.await
.unwrap_or("Your password reset code is: {{code}}".to_string())
.replace("{{code}}", &code);
sms_provider.send(&mobile, &message).await?;