Как загружать переводы из файлов в 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 для множественных форм и форматирования.