Как загружать переводы из файлов в TanStack Start v1

Отделяйте переводимый контент от кода

Проблема

Жёсткое кодирование строк, отображаемых пользователю, непосредственно в компонентах создаёт сильную связь между содержимым и кодом. Каждый раз, когда строка изменяется или добавляется новый язык, разработчикам приходится находить и изменять исходные файлы, а затем повторно развёртывать приложение. Такой подход делает процессы перевода зависимыми от циклов разработки и не позволяет нетехническим членам команды обновлять текст. По мере увеличения количества поддерживаемых языков условная логика для выбора правильной строки становится громоздкой и подверженной ошибкам. В результате замедляется итерация, увеличиваются затраты на обслуживание, а кодовая база засоряется переводимым содержимым, которое должно находиться в другом месте.

Решение

Отделите все переводимые строки от кода приложения, сохраняя их во внешних JSON-файлах, по одному на каждую локаль. Определите стабильный ключ для каждого сообщения и используйте эти ключи в компонентах вместо буквального текста. Во время выполнения загружайте соответствующий файл перевода в зависимости от локали пользователя и предоставляйте эти сообщения через IntlProvider из react-intl. Это отделяет содержимое от кода: переводчики могут работать напрямую с JSON-файлами, изменения текста не требуют модификации кода, а добавление нового языка сводится к добавлению нового файла без изменения компонентов.

Шаги

1. Создайте файлы перевода для каждой локали

Организуйте переводы в виде JSON-файлов в выделенной директории, по одному файлу на каждую локаль, содержащему пары ключ-значение для всех сообщений.

{
"welcome": "С возвращением",
"greeting": "Привет, {name}",
"itemCount": "{count, plural, =0 {Нет элементов} one {Один элемент} other {# элементов}}"
}

Сохраните это как app/translations/ru.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. Теперь компоненты могут ссылаться на сообщения по ключу, используя FormattedMessage или 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 для множественных форм и форматирования.