اللغة في الرابط، لا في المتصفح
في الصفحات العامة اجعل اللغة جزءًا من الرابط: /ar/... و/en/.... الرابط هو المصدر الوحيد للحقيقة. محرك البحث يرى صفحتين منفصلتين، والرابط المنسوخ يفتح بلغته، وزر التبديل ينقلك إلى الصفحة نفسها باللغة الأخرى دون أن يقفز بك إلى أعلى الصفحة.
أي بادئة غير معروفة تُحوَّل إلى اللغة الافتراضية، والصفحة الرئيسية بلا بادئة تُحوَّل أيضًا. أما localStorage فاتركه لصفحات الإدارة وتسجيل الدخول التي لا تحتاجها محركات البحث. ولا تنسَ روابط hreflang بين النسختين، مع x-default.
export const langResolver: ResolveFn<AppLang> = async (route, state) => {
const translations = inject(TranslationCatalogService);
const router = inject(Router);
const lang = parseLang(route.paramMap.get('lang'));
if (!lang) {
// Unknown locale: rewrite the first segment to the default one.
const rest = state.url.split('/').filter((s) => s.length > 0);
if (rest.length > 0) rest[0] = DEFAULT_LANG;
else rest.push(DEFAULT_LANG);
return new RedirectCommand(router.parseUrl(`/${rest.join('/')}`), { replaceUrl: true });
}
await translations.switchTo(lang);
return lang;
}; محلّل المسار: لغة معروفة أو تحويل اقلب الاتجاه بعد وصول الكلمات
إذا قلبت dir قبل أن يصل ملف الترجمة، يرى المستخدم واجهة مقلوبة بنصوص اللغة السابقة لجزء من الثانية. الترتيب الصحيح: حمّل الكلمات أولًا، ثم بدّل اللغة، ثم اقلب الاتجاه. وإذا ضغط المستخدم زر التبديل مرتين بسرعة، فالطلب الأحدث هو الذي يفوز، والأقدم يُلغى بصمت.
اضبط dir وlang على عنصر html عبر Renderer2 والمستند المحقون، لا عبر document مباشرة، حتى يعمل ذلك على الخادم أيضًا. ومكونات Angular Material تقرأ الاتجاه من Directionality في CDK، فاستبدلها بخدمة تتبع لغة التطبيق، وإلا ظلت القوائم والنوافذ المنبثقة تفتح من الجهة الخطأ.
async switchTo(lang: AppLang): Promise<boolean> {
this.latest = lang;
await this.ensure(lang); // lazy-load the dictionary chunk
if (this.latest !== lang) return false; // a newer toggle won
await firstValueFrom(this.translate.use(lang));
this.localization.setLanguage(lang);
return true;
}
// App root: one effect sets <html dir lang>, in the browser and on the server.
effect(() => {
const lang = this.localization.language();
const root = this.document.documentElement;
this.renderer.setAttribute(root, 'dir', lang === 'ar' ? 'rtl' : 'ltr');
this.renderer.setAttribute(root, 'lang', lang);
}); الكلمات أولًا، والاتجاه أخيرًا خصائص CSS المنطقية أولًا
margin-inline-start بدل margin-left، وpadding-inline بدل padding-right، وinset-inline-end بدل right. بهذا تنقلب الواجهة وحدها مع dir دون ملف أنماط ثانٍ. في Amlaak نحو 153 استخدامًا للخصائص المنطقية مقابل نحو 12 للخصائص الفيزيائية.
أبقِ [dir=rtl] لما لا مقابل منطقيًا له: transform وtransform-origin والحركات. وقاعدة واحدة للأيقونات: اقلب فقط ما يدل على اتجاه، كالأسهم وعلامات التالي والسابق. علامة الصح وزر التشغيل والبحث تبقى كما هي.
[dir='rtl'] .flip-rtl {
transform: scaleX(-1);
}
.card-cta {
margin-inline-start: auto; /* right in English, left in Arabic */
padding-inline: 1rem 0.75rem;
} صنف واحد للأيقونات التي تشير إلى اتجاه خط عربي لا يثقل الصفحة الإنجليزية
قسّم الخط بـunicode-range إلى مجموعة عربية ومجموعة لاتينية، فلا تنزّل الصفحة الإنجليزية الحروف العربية أبدًا. وحمّل مسبقًا (preload) المجموعة التي تحتاجها الشاشة الأولى فقط: في Alreef يُحمَّل خط Tajawal مسبقًا لأن النص العربي هو أكبر ما يُرسم أولًا، وفي Amlaak يرسل الخادم تلميحات تحميل مختلفة لكل لغة.
تفصيل صغير: اسم اللغة الأخرى على زر التبديل يُكتب بخط النظام، وإلا جرّ الزر وحده ملف الخط الآخر إلى كل صفحة. وتذكر أن letter-spacing يفصل الحروف العربية المتصلة، فألغه في الصفحات العربية.
الأرقام والتواريخ
ar-SA وحدها تعطيك التقويم الهجري والأرقام الهندية في كثير من المتصفحات. إن كان العمل بالميلادي وبالأرقام اللاتينية، فاطلب ذلك صراحة، وحدد المنطقة الزمنية بدل الاعتماد على جهاز المستخدم.
const date = new Intl.DateTimeFormat('ar-SA-u-ca-gregory-nu-latn', {
dateStyle: 'medium',
timeZone: 'Asia/Riyadh',
}).format(new Date());
const compact = new Intl.NumberFormat('ar-u-nu-latn', { notation: 'compact' }).format(1250000); ميلادي، بأرقام لاتينية، بتوقيت الرياض النص المختلط
جملة تجمع العربية والأرقام والإنجليزية تتبع خوارزمية الاتجاه الثنائي، والنتيجة أحيانًا مفاجئة. عبارة النطاق في ترقيم الصفحات بـAngular Material (من 1 إلى 15 من أصل 44) تُبنى بلصق أرقام بجانب كلمات، وفي واجهة عربية تخرج مبعثرة. الحل في Alreef: ترجم العبارة كاملة بقالب فيه متغيرات، بدل لصق الأرقام بجانب كلمات مترجمة.
وحين يكتب المستخدمون أنفسهم، كما في المحادثات، لا تفرض اتجاه الواجهة على المحتوى. في Project Pavilion تأخذ كل رسالة اتجاهها من الحروف الغالبة فيها، ويُسمح بخاصية dir في HTML المنقّى حتى لا يضيع الاتجاه في الطريق.
export function detectContentDirection(text: string | null | undefined): 'rtl' | 'ltr' | null {
if (!text || !text.trim()) return null;
const arabic = text.match(ARABIC_G)?.length ?? 0;
if (arabic === 0) return 'ltr';
const latin = text.match(LATIN_G)?.length ?? 0;
return arabic >= latin ? 'rtl' : 'ltr';
} اتجاه الرسالة من محتواها مع العرض على الخادم
الخادم يعرف اللغة من الرابط، فليكتب dir وlang الصحيحين في HTML من البداية. لا شيء أسوأ من صفحة عربية تُرسم من اليسار ثم تقفز.
وإن كانت الصفحات مولّدة مسبقًا بلغة واحدة، كما في Alreef بالعربية، فزائر اختار الإنجليزية سيرى العربية لحظة قبل أن يتبدل كل شيء. الحل هناك: سطر صغير في index.html يقرأ الاختيار المحفوظ ويخفي الصفحة حتى تُرسم بالإنجليزية، مع مهلة احتياطية ثلاث ثوانٍ، وبصمة السطر مثبتة في سياسة CSP. وهو حل التفافي له ثمن: يتأخر أول رسم للصفحة عند هؤلاء الزوار، والعرض على الخادم لكل لغة يتجنبه.
قبل الإطلاق
- هل لكل صفحة عامة رابط بلغتها، مع hreflang بين النسختين؟
- هل يتبدل الاتجاه بعد وصول الكلمات، وهل يفوز آخر ضغط على زر التبديل؟
- هل تفتح قوائم Material ونوافذها من الجهة الصحيحة؟
- هل بقي margin-left أو right دون سبب؟
- هل تنقلب الأسهم وحدها، لا علامة الصح ولا زر التشغيل؟
- هل تنزّل الصفحة الإنجليزية الخط العربي؟ افتح أدوات المطور وتأكد.
- هل التواريخ ميلادية أو هجرية عن قصد، وبأي أرقام؟
- هل يتطابق عدد مفاتيح الترجمة في اللغتين، ويفشل البناء إن لم يتطابق؟