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

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

المشكلة

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

الحل

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

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

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. يمكن للمكونات الآن الإشارة إلى الرسائل بالمفتاح باستخدام {/* INLINE_CODE_PLACEHOLDER_7adb739316add72508559b72e5a4d135d */} أو useIntl، ويتم عرض الترجمة الصحيحة بناءً على اللغة المحملة.

6. الإشارة إلى الرسائل بالمفتاح في المكونات

استخدم مكون FormattedMessage أو خطاف useIntl من react-intl لعرض النصوص المترجمة، مع الإشارة إلى المفاتيح المحددة في ملفات 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 نصاً للاستخدام في السمات أو منطق JavaScript. كلاهما يقبل values للاستيفاء ويدعم صيغة رسائل ICU للجمع والتنسيق.