Локализуем React (NextJS, TypeScript) сайт на несколько языков с помощью i18next

У меня появилась задача в проекте:

  • Перевести личный кабинет пользователя на русский и английский (в перспективе и на другие языки).
  • При этом, определять язык пользователя при первом заходе в ЛК и давать его изменить.
  • Запоминать выбранный язык при перезагрузке страницы.
  • Сделать так, чтобы в проектах была типизация файлов с переводами (чтобы нельзя было забыть добавить один из языков).

Как я это делал — расскажу в статье.

Первоначальное решение

Сначала я решил задачу, просто закинув все переводы в несколько .ts-файлов с общим интерфейсом и выбирая язык через Redux. Всё работало, но было ощущение, что это я переизобрел велосипед.

Хотелось чего-то более стандартного и популярного на рынке: по-любому эту задачу кто-то уже решил более качественно. Да и всё-таки онбординг новых разработчиков никто не отменял. Поэтому было принято решение: выбрать популярную библиотеку и перенести переводы на неё.

Выбор библиотеки переводов

Для решения задачи я выбрал i18next.

Почему именно i18next?

  • Имеет поддержку типизации "из коробки" (дружит TS типы и даже кое-как с автокомплишном).
  • "Дружит" с React Server Component в Next.js (для Next.js 13+).
  • Поддерживает lazy loading (разделение переводов по чанкам/файлам) для ускорения страниц.
  • Всё выше делается просто относительной других популярных библиотек.

Глобально, сейчас из этого всего в проекте нужна только типизация. Но закладываю серверные компоненты и разделение кода на будущее. Проект планирует расширять, и эти возможности пригодятся для SEO.

Для наглядности сделал таблицу, где сравнил три самые популярные библиотеки:

Кликабельно
Кликабельно

*оценка может быть субъективной из расчёта на конкретный проект, на объективность не претендую.

*i18n - расшифровывается как "internationalization".

Минутка самопиара

У меня есть Telegram-канал, где я собираю ссылки на свои статьи про Full-Stack разработку, развитие SaaS-продуктов и управление IT-проектами.

Шаблон проекта

Для примера будем делать одну страницу с переключателем языка и несколькими текстовыми полями:

Страница для примера

Для начала создадим новый проект на Next.js с TypeScript шаблоном.

Выполняем команды:

npx create-next-app@latestmy-multilang-app--typescript

Я сразу добавил ESLint, TailwindCSS и Turbopack:

Локализуем React (NextJS, TypeScript) сайт на несколько языков с помощью i18next

Появляется структура:

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-проектами.

Если остались вопросы, пишите в комментариях!

Делали ли вы локализацию в проекте?
Да
Нет
Не моя зона ответственности
Начать дискуссию