Локализуем React (NextJS, TypeScript) сайт на несколько языков с помощью i18next
- пятница, 17 января 2025 г. в 00:00:10
У меня появилась задача в проекте:
Перевести личный кабинет пользователя на русский и английский (в перспективе и на другие языки).
При этом, определять язык пользователя при первом заходе в ЛК и давать его изменить.
Запоминать выбранный язык при перезагрузке страницы.
Сделать так, чтобы в проектах была типизация файлов с переводами (чтобы нельзя было забыть добавить один из языков).
Как я это делал — расскажу в статье.
Сначала я решил задачу, просто закинув все переводы в несколько .ts
-файлов с общим интерфейсом и выбирая язык через Redux. Всё работало, но было ощущение, что это я переизобрел велосипед.
Хотелось чего-то более стандартного и популярного на рынке: по-любому эту задачу кто-то уже решил более качественно. Да и всё-таки онбординг новых разработчиков никто не отменял. Поэтому было принято решение: выбрать популярную библиотеку и перенести переводы на неё.
Для решения задачи я выбрал i18next.
Почему именно i18next?
Имеет поддержку типизации "из коробки" (дружит TS типы и даже кое-как с автокомплишном).
"Дружит" с React Server Component в Next.js (для Next.js 13+).
Поддерживает lazy loading (разделение переводов по чанкам/файлам) для ускорения страниц.
Всё выше делается просто относительной других популярных библиотек.
Глобально, сейчас из этого всего в проекте нужна только типизация. Но закладываю серверные компоненты и разделение кода на будущее. Проект планирует расширять, и эти возможности пригодятся для SEO.
Для наглядности сделал таблицу, где сравнил три самые популярные библиотеки:
i18next | react-intl | @lingui/react | |
Популярность | 🟠 7.9k | 🟢14.4k | 🟠4.8k |
Типизация | 🟢 | 🟠(нужно повозиться) | 🔴 |
Server Side Components | 🟢 | 🟠(нужно повозиться) | 🟢 |
Lazy loading | 🟢 (через namespaces) | 🟢(нужно повозиться с динамическим импортом) | 🟠(нужно повозиться) |
Субъективное удобство | 🟢 (вызов всего через | 🔴(все переводы нужно оборачивать в компонент) | 🔴(все переводы нужно оборачивать в компонент и нет нормальной типизации) |
*оценка может быть субъективной из расчёта на конкретный проект, на объективность не претендую.
*i18n - расшифровывается как "internationalization".
У меня есть Telegram-канал, где я собираю ссылки на свои статьи про Full-Stack разработку, развитие SaaS-продуктов и управление IT-проектами.
Для примера будем делать одну страницу с переключателем языка и несколькими текстовыми полями:
Для начала создадим новый проект на Next.js с TypeScript шаблоном.
Выполняем команды:
npx create-next-app@latest my-multilang-app --typescript
Я сразу добавил ESLint, TailwindCSS и Turbopack:
Появляется структура:
my-multilang-app/
├─ app/
│ ├─ page.tsx
│ └─ ...
├─ public/
├─ ...
└─ package.json
И сразу добавляем библиотеки i18next:
npm install i18next react-i18next i18next-browser-languagedetector
react-i18next
— адаптер для React.i18next-browser-languagedetector
— плагин для определения языка в браузере.
Создаём тип переводов и сами переводы в папке i18n:
i18n/
├─ translations/en_translation.json
├─ translations/ru_translation.json
├─ translations/TranslationTypes.ts
└─ i18n.ts
Разумеется, можно назвать файлы и папки по-другому, главное, чтобы была понятная структура. Я выбрал нейминг, стандартный для Feature Sliced Design (но FSD мы здесь не используем).
Далее сами файлы:
i18n/translations/TranslationTypes.ts
export interface TranslationTypes {
// Используем схему componentName.field
page: {
hello: string;
changeLanguage: string;
dashboardTitle: string;
profile: string;
};
}
i18n/translations/en_translation.json
{
"page": {
"hello": "Hello, {{name}}!",
"changeLanguage": "Change language to Russian",
"dashboardTitle": "User Dashboard",
"profile": "My profile"
}
}
i18n/translations/ru_translation.json
{
"page": {
"hello": "Привет, {{name}}!",
"changeLanguage": "Переключить язык на английский",
"dashboardTitle": "Личный кабинет",
"profile": "Мой профиль"
}
}
Чтобы включить типизацию, нужно воспользоваться встроенным механизмом декларации типов i18next
. Создадим файл resources.d.ts
(или i18n.d.ts
) в корне проекта или в папке types
, где пропишем:
import "i18next";
import { TranslationTypes } from "@/i18n/translations/TranslationTypes";
declare module "i18next" {
interface CustomTypeOptions {
resources: TranslationTypes;
}
}
Теперь при использовании useTranslation
и t
в нашем коде TypeScript будет подсказывать, какие ключи перевода у нас существуют.
Добавим файл i18n.ts
в папку /i18n
:
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import { TranslationTypes } from "./translations/TranslationTypes";
import en from "./translations/en_translation.json";
import ru from "./translations/ru_translation.json";
// Если забудем добавить поле в один из языков,
// здесь появится TypeScript ошибка
const resources: Record<string, { translation: TranslationTypes }> = {
en: { translation: en },
ru: { translation: ru },
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
detection: {
order: ["localStorage", "navigator"],
caches: ["localStorage"],
lookupLocalStorage: "i18nextLng",
},
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;
Обратите внимание, что i18next-browser-languagedetector
смотрит, какой язык установлен в браузере, а также может работать с cookie/localStorage. Это решает задачу "запоминать язык при перезагрузке страницы".
Здесь мы указываем логику, в которой сначала пытаемся брать язык из localStorage, а затем из браузера:
order: ["localStorage", "navigator"],
i18next умеет сам выбирать нужный язык в зависимости от настройки браузера (ru, en, sp и другие). Нам нужно только указать нужный файл для языка:
const resources: Record<string, { translation: TranslationTypes }> = {
en: { translation: en },
ru: { translation: ru },
};
...
...
// Если нужного языка нет, берём английский
fallbackLng: "en",
...
...
Чтобы пользователь мог переключать язык, создадим компонент выбора языка:
LanguageSwitcher.tsx
:
"use client";
import { useTranslation } from "react-i18next";
export default function LanguageSwitcher() {
const { i18n } = useTranslation();
const changeLanguage = async (lang: "en" | "ru") => {
await i18n.changeLanguage(lang);
};
return (
<div className="mt-4 space-x-2">
<button
onClick={() => changeLanguage("en")}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
EN
</button>
<button
onClick={() => changeLanguage("ru")}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
RU
</button>
</div>
);
}
Теперь мы можем переиспользовать этот компонент на любой странице или в header'e.
Если у вас локализация распространяется и на API, вам нужно прокидывать язык в запросы. Его можно брать из нашего файла i18n
и добавлять в заголовок. Например, в fetch-запросе:
await fetch('/api/user', {
headers: {
'Authorization': getAccessToken(),
'Accept-Language': i18n.language, // берем текущий язык
}
});
Полный пример страницы с переводами выглядит вот так:
app/page.tsx
"use client";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { useTranslation } from "react-i18next";
import "../i18n/i18n";
export default function HomePage() {
const { t } = useTranslation();
return (
<main className="p-8 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
{t("page.dashboardTitle")}
</h1>
<div className="space-y-4">
<p className="text-lg text-gray-700">
{t("page.hello", { name: "John" })}
</p>
<p className="text-lg text-gray-700">{t("page.profile")}</p>
</div>
{/* Кнопка для переключения языка */}
<div className="mt-8">
<LanguageSwitcher />
</div>
</main>
);
}
Как результат смены языка в LanguageSwitcher
будут меняться все надписи, а при перезагрузке страницы сохранится последний выбранный язык:
Конкретно в этом примере мы используем "use client"
для упрощения. В следующей статье я покажу, как использовать i18next
с SSR'ом.
Итого, при смене языка у нас меняются все тексты на странице:
Заголовок «Личный кабинет» <> «User Dashboard»
Приветствие «Привет, John!» <> «Hello, John!»
Кнопка для профиля «Мой профиль» <> «My profile»
А при перезагрузке приложения язык остаётся выбранным, так как i18next-browser-languagedetector
сохраняет язык в localStorage'e.
Чтобы добавить новые языки (испанский, китайский и т.д.) нужно расширить ресурс в i18n.ts
и добавить новые файлы с переводами (например, es_translation.json
, zh_translation.json
). Типизация подскажет, не забыли ли мы какие-то поля.
P.S. Напомню, что у меня есть Telegram-канал, где я собираю ссылки на свои статьи про Full-Stack разработку, развитие SaaS-продуктов и управление IT-проектами.
Если остались вопросы, пишите в комментариях!