Как загружать переводы из файлов в React Router v7

Отделение переводимого контента от кода

Проблема

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

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

Решение

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

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

Шаги

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

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

{
"welcome.title": "С возвращением",
"welcome.subtitle": "Продолжите с того места, где остановились",
"nav.home": "Главная",
"nav.about": "О нас",
"nav.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, и отображает соответствующую переведенную строку. Когда локаль изменяется и загрузчик запускается снова с другими сообщениями, все компоненты автоматически отображают новые переводы без изменений в коде.