FrontendArabicEngineering

بناء تطبيقات Next.js ثنائية اللغة: RTL وnext-intl والطباعة العربية كما يجب

دليل عملي لبناء تطبيقات Next.js بالعربية والإنجليزية مع اتجاه صفحة صحيح، وتوجيه محلي، وطباعة عربية مريحة.

BahrTech Team
٨ مايو ٢٠٢٦ · 4 دقيقة قراءة

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

دعم RTL والثنائية اللغوية ليسا طبقة ترجمة تضيفها في نهاية المشروع. إنهما قرارات معمارية تتراكم عبر كل ميزة تشحنها.

اجعل اللغة والاتجاه قيماً صريحة من البداية

لا تستنتج اتجاه النص من إعدادات المتصفح أو من محتوى الحرف. اكتب قائمة محددة بالمناطق المدعومة ودالة تربط كل منطقة باتجاهها بشكل صريح.

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

export function getDirection(locale: Locale): "ltr" | "rtl" {
  return locale === "ar" ? "rtl" : "ltr";
}

export function getDateLocale(locale: Locale): string {
  return locale === "ar" ? "ar-EG" : "en-US";
}

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

اعمل مع next-intl كبنية منتج لا كإضافة مؤخرة

توفر next-intl ترجمات آمنة من حيث الأنواع، وتوجيهاً محلياً، ودعماً لمكونات الخادم. المفتاح هو التعامل مع مفاتيح الترجمة كعقود مستقرة تعبر عن لغة المنتج، لا كنصوص مبعثرة حسب الميزة.

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";
export const routing = defineRouting({
  locales: ["ar", "en"],
  defaultLocale: "ar",
});
// src/app/[locale]/layout.tsx
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { getDirection, type Locale } from "@/lib/i18n";

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: Locale }>;
}) {
  const { locale } = await params;
  const messages = await getMessages();
  return (
    <html lang={locale} dir={getDirection(locale)}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

وضع dir على عنصر <html> يعني أن كل سلوك المتصفح الافتراضي يتقلب تلقائياً: أشرطة التمرير، وحلقات التركيز، والتبطين الافتراضي لـ <input> و<select>، ومحاذاة النص. المكونات التي تقاوم ذلك عادة تستخدم إزاحات left/right ثابتة بدلاً من خصائص CSS المنطقية.

خصائص Tailwind المنطقية هي الإصلاح الذي تفوته أغلب الفرق

أكثر مشاكل RTL شيوعاً هي أدوات الاتجاه الثابتة: pl-4، mr-2، left-0، text-left. في تخطيط RTL تبقى padding-left على اليسار. لا تنقلب تلقائياً.

استبدل أدوات الاتجاه بالمنطقية:

استبدلبـ
pl-4، pr-4ps-4، pe-4
ml-2، mr-2ms-2، me-2
left-0، right-0start-0، end-0
text-left، text-righttext-start، text-end
border-l، border-rborder-s، border-e

هذا لا يخص العربية وحدها. الفكرة أن تكتب كود تخطيط يعمل في أي اتجاه نص دون تجاوزات per-locale. للعثور على النقاط الحرجة في قاعدة كود قائمة، grep سريع على pl-، pr-، ml-، mr-، left-، right- في ملفات المكونات يكشفها.

الطباعة العربية تحتاج قراراتها الخاصة

الرموز العربية أكثر كثافة بصرياً من اللاتينية عند نفس font-size. عنوان بـ text-4xl بالإنجليزية قد يبدو مزدحماً وصعب القراءة بالعربية عند الحجم نفسه. التعديلات الشائعة:

  • ارتفاع السطر: النص العربي عادة يحتاج leading- أوسع قليلاً من مقابله اللاتيني.
  • تباعد الحروف: الخط العربي متصل. لا تستخدم tracking-tight أو tracking-widest على نص عربي — يكسر التحام الحروف.
  • اختيار الخط: حمّل خطاً عربياً مخصصاً للشاشات (Noto Sans Arabic أو Cairo أو Tajawal) وطبقه فقط على عناصر [lang="ar"].
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+Arabic:wght@400;600;700&display=swap");

:lang(ar) {
  font-family: "Noto Sans Arabic", sans-serif;
  line-height: 1.75;
  letter-spacing: 0;
}

المحتوى المختلط: أرقام وأسعار وأسماء منتجات

واجهات اللغة العربية كثيراً ما تحتوي أرقاماً وأسماء منتجات تبقى بالخط اللاتيني حتى داخل نص RTL. المتصفحات تتعامل مع هذا عبر خوارزمية Unicode Bidi، لكن قد تظهر تشوهات في العرض. استخدم dir="ltr" على عناصر inline يجب أن تقرأ دائماً من اليسار:

<span dir="ltr">{price.toLocaleString("ar-EG")} ج.م</span>

تنسيق الأرقام بـ ar-EG يخرج الأرقام الشرقية (٣٢٠) افتراضياً. حدد بوضوح هل يتوقع مستخدموك الأرقام الشرقية أم الغربية (320)، ثم التزم بالقرار.

البيانات الوصفية وصور OG وSEO بحاجة إلى وعي بالمنطقة

أنشئ <title> و<meta name="description"> من نصوص الترجمة حتى تفهرس محركات البحث اللغة الصحيحة. أضف hreflang بديلاً حتى يعرف Google أن كلا الرابطين موجودان:

export async function generateMetadata({ params }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "Metadata" });
  return {
    title: t("title"),
    description: t("description"),
    alternates: {
      canonical: `/${locale}`,
      languages: { ar: "/ar", en: "/en" },
    },
  };
}

صور OG المحلية مهمة أيضاً. صورة 1200×630 بنص لاتيني تبدو غريبة في مشاركات السوشيال العربية. أنشئ صور OG ديناميكياً باستخدام ImageResponse من Next.js مع العنوان المترجم.

اختبار RTL قبل أن يكتشفه المستخدمون

الفجوة التي تسببت في الأزمة قبل الإطلاق التي ذكرناها كانت غياب اختبار RTL. ثلاث عادات تمنع ذلك:

  1. أضف dir="rtl" إلى الديكوراتور العام في Storybook حتى تنقلب قصص المكونات تلقائياً.
  2. أضف اختبار تكامل واحد بـ RTL locale لكل ميزة تمس واجهة حساسة للتخطيط.
  3. اختبر على جهاز Android فعلي مضبوط على اللغة العربية لا فقط في DevTools. بعض سلوكيات flex وgrid تختلف دقيقاً بين محاكاة Chrome والعرض الفعلي للنظام.

تطبيق Next.js ثنائي اللغة هو نظام منتج واحد، لا نسختان من نفس الواجهة بنصوص مختلفة. اللغة والاتجاه والطباعة والبيانات الوصفية متطلبات أساسية منذ أول مسار. أنجزتَ هذا الأساس بشكل صحيح وكل ميزة تضيفها بعده ستعمل في اللغتين دون عمل إضافي.

للجانب الذكاء الاصطناعي في منتجات عربية، اقرأ روبوتات محادثة بالذكاء الاصطناعي تفهم العربية فعلاً.

الوسوم
FrontendArabicEngineering