Как загружать переводы из файлов в 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 context. Дочерние маршруты рендерятся через 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 в объекте messages, который передаёт IntlProvider, и отображает соответствующую переведённую строку. Когда локаль меняется и загрузчик снова получает другие сообщения, все компоненты автоматически показывают новые переводы без изменений в коде.