كيفية تحميل الترجمات من الملفات في React Router v7

فصل المحتوى القابل للترجمة عن الكود

المشكلة

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

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

الحل

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

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

الخطوات

1. إنشاء ملفات ترجمة لكل لغة محلية

قم بتنظيم ملفات الترجمة في دليل مخصص، مع ملف JSON واحد لكل لغة. قم بهيكلة كل ملف ككائن مسطح يربط معرفات الرسائل بالنصوص المترجمة.

{
"welcome.title": "Welcome back",
"welcome.subtitle": "Continue where you left off",
"nav.home": "Home",
"nav.about": "About",
"nav.contact": "Contact"
}

احفظ هذا كـ app/translations/en.json للغة الإنجليزية، ثم قم بإنشاء ملفات موازية مثل app/translations/es.json و app/translations/fr.json مع ترجمات للغات الأخرى. استخدم مفاتيح متسقة عبر جميع الملفات بحيث يشير نفس المعرف إلى الترجمة المناسبة في كل لغة محلية.

2. تحميل الترجمات في محمّل المسار

استخدم محمّل المسار لجلب ملف الترجمة للغة الحالية قبل العرض. هذا يضمن توفر الرسائل عند تحميل المكونات.

import type { Route } from "./+types/root";
import enMessages from "./translations/en.json";
import esMessages from "./translations/es.json";
import frMessages from "./translations/fr.json";

const messages: Record<string, Record<string, string>> = {
  en: enMessages,
  es: esMessages,
  fr: frMessages,
};

export async function loader({ request }: Route.LoaderArgs) {
  const url = new URL(request.url);
  const locale = url.searchParams.get("locale") || "en";

  return {
    locale,
    messages: messages[locale] || messages.en,
  };
}

يقوم المحمّل بقراءة اللغة من معلمة استعلام URL ويعيد كلاً من اللغة والرسائل المقابلة لها. يمكن للمكونات الوصول إلى هذه البيانات من خلال loaderData لتكوين مزود التدويل.

3. تكوين IntlProvider مع الرسائل المحمّلة

قم بتغليف تطبيقك بـ IntlProvider من react-intl، مع تمرير اللغة والرسائل من بيانات المحمّل.

import { IntlProvider } from "react-intl";
import { Outlet } from "react-router";
import type { Route } from "./+types/root";

export default function Root({ loaderData }: Route.ComponentProps) {
  return (
    <IntlProvider locale={loaderData.locale} messages={loaderData.messages}>
      <html lang={loaderData.locale}>
        <head>
          <meta charSet="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
        </head>
        <body>
          <Outlet />
        </body>
      </html>
    </IntlProvider>
  );
}

يجعل IntlProvider اللغة والرسائل متاحة لجميع المكونات الفرعية من خلال سياق React. تُعرض المسارات الفرعية من خلال Outlet وترث إمكانية الوصول إلى بيانات الترجمة.

4. الإشارة إلى الرسائل بواسطة المعرّف في المكونات

استبدل النصوص الثابتة بمكونات FormattedMessage التي تشير إلى معرّفات الرسائل من ملفات الترجمة الخاصة بك.

import { FormattedMessage } from "react-intl";

export default function Welcome() {
  return (
    <div>
      <h1>
        <FormattedMessage id="welcome.title" />
      </h1>
      <p>
        <FormattedMessage id="welcome.subtitle" />
      </p>
      <nav>
        <a href="/">
          <FormattedMessage id="nav.home" />
        </a>
        <a href="/about">
          <FormattedMessage id="nav.about" />
        </a>
        <a href="/contact">
          <FormattedMessage id="nav.contact" />
        </a>
      </nav>
    </div>
  );
}

يبحث كل مكون FormattedMessage عن id الخاص به في كائن الرسائل المقدم من IntlProvider ويعرض النص المترجم المقابل. عندما تتغير اللغة ويعمل المحمّل مرة أخرى برسائل مختلفة، تعرض جميع المكونات تلقائيًا الترجمات الجديدة دون تغييرات في الكود.