كيفية تحميل الترجمات من الملفات في TanStack Start v1

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

المشكلة

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

الحل

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

الخطوات

1. أنشئ ملفات الترجمة لكل لغة

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

{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "{count, plural, =0 {No items} one {One item} other {# items}}"
}

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

2. تحميل الترجمات باستخدام دالة على الخادم

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

import { createServerFn } from "@tanstack/react-start";
import * as fs from "node:fs";

export const getMessages = createServerFn({ method: "GET" }).handler(
  async ({ request }) => {
    const url = new URL(request.url);
    const locale = url.searchParams.get("locale") || "en";
    const filePath = `app/translations/${locale}.json`;
    const content = await fs.promises.readFile(filePath, "utf-8");
    return JSON.parse(content);
  },
);

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

3. أنشئ أداة مساعدة لتحديد لغة المستخدم

عرّف أداة صغيرة تستخرج اللغة من الطلب أو تعتمد على لغة افتراضية، لتتمكن من إعادة استخدامها عبر جميع المسارات.

export function getLocaleFromRequest(request: Request): string {
  const url = new URL(request.url);
  const localeParam = url.searchParams.get("locale");
  if (localeParam) return localeParam;
  const acceptLanguage = request.headers.get("accept-language");
  if (acceptLanguage) {
    const match = acceptLanguage.split(",")[0].split("-")[0];
    return match || "en";
  }
  return "en";
}

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

4. حمّل الرسائل داخل محمّل المسار (route loader)

استخدم محمّل المسار لجلب الرسائل للغة الحالية قبل العرض، لتصبح متاحة لشجرة المكونات.

import { createFileRoute } from "@tanstack/react-router";
import { getMessages, getLocaleFromRequest } from "../lib/i18n";

export const Route = createFileRoute("/")({
  loader: async ({ context }) => {
    const locale = getLocaleFromRequest(context.request);
    const messages = await getMessages({ data: { locale } });
    return { locale, messages };
  },
  component: HomePage,
});

function HomePage() {
  const { locale, messages } = Route.useLoaderData();
  return (
    <div>
      <p>{messages.welcome}</p>
    </div>
  );
}

يستدعي محمّل المسار الدالة الخادمية لاسترجاع الرسائل، ويمكن للمكون الوصول إليها عبر useLoaderData. يعمل هذا النمط مع توليد الصفحات على الخادم (SSR) وأيضًا أثناء تنقل العميل بين الصفحات.

5. مرر الرسائل إلى react-intl

قم بتغليف شجرة المكونات الخاصة بك بـ IntlProvider، مع تمرير اللغة والرسائل المحملة لتتمكن جميع المكونات الفرعية من الوصول للترجمات.

import { IntlProvider } from "react-intl";

function HomePage() {
  const { locale, messages } = Route.useLoaderData();
  return (
    <IntlProvider locale={locale} messages={messages}>
      <AppContent />
    </IntlProvider>
  );
}

function AppContent() {
  return (
    <div>
      <FormattedMessage id="welcome" />
    </div>
  );
}

يوفر لك IntlProvider إمكانية الوصول إلى اللغة والرسائل لجميع مكونات react-intl وخطافاته. يمكن الآن للمكونات الرجوع إلى الرسائل باستخدام FormattedMessage أو useIntl، وسيتم عرض الترجمة الصحيحة حسب اللغة المحملة.

6. استدعِ الرسائل بالمفتاح داخل المكونات

استخدم مكون FormattedMessage من مكتبة react-intl أو الخطاف useIntl لعرض النصوص المترجمة، بالاستناد إلى المفاتيح التي عرفتها في ملفات JSON.

import { FormattedMessage, useIntl } from "react-intl";

function UserGreeting({ name }: { name: string }) {
  const intl = useIntl();
  const title = intl.formatMessage({ id: "greeting" }, { name });
  return (
    <div>
      <h1 title={title}>
        <FormattedMessage id="greeting" values={{ name }} />
      </h1>
      <p>
        <FormattedMessage id="itemCount" values={{ count: 5 }} />
      </p>
    </div>
  );
}

يستخدم FormattedMessage لعرض النص المترجم بشكل مضمَّن، بينما يعيد useIntl().formatMessage نصًا للاستخدام في السمات أو في منطق جافاسكريبت. كلاهما يقبل values من أجل الاستبدال، ويدعم صياغة رسائل ICU للجمع والتنسيق.