У меня появилась задача в проекте:
Перевести личный кабинет пользователя на русский и английский (в перспективе и на другие языки).
При этом, определять язык пользователя при первом заходе в ЛК и давать его изменить.
Запоминать выбранный язык при перезагрузке страницы.
Сделать так, чтобы в проектах была типизация файлов с переводами (чтобы нельзя было забыть добавить один из языков).
Как я это делал — расскажу в статье.
Содержание
Первоначальное решение
Сначала я решил задачу, просто закинув все переводы в несколько .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 будет подсказывать, какие ключи перевода у нас существуют.
Инициализируем i18next
Добавим файл 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
Если у вас локализация распространяется и на 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-проектами.
Если остались вопросы, пишите в комментариях!
