Как загружать переводы из файлов в 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. Теперь компоненты могут получать сообщения по ключу через FormattedMessage или useIntl, и будет отображаться корректный перевод в зависимости от выбранной локали.
6. Ссылайтесь на сообщения по ключу в компонентах
Используйте компонент react-intl FormattedMessage или хук 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 возвращает строку для использования в атрибутах или JavaScript-логике. Оба принимают values для интерполяции и поддерживают синтаксис ICU для плюрализации и форматирования.