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

—

от автора

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

  • Перевести личный кабинет пользователя на русский и английский (в перспективе и на другие языки).

  • При этом, определять язык пользователя при первом заходе в ЛК и давать его изменить.

  • Запоминать выбранный язык при перезагрузке страницы.

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

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

Содержание

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

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

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

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

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

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

  • Имеет поддержку типизации «из коробки» (дружит TS типы и даже кое-как с автокомплишном).

  • «Дружит» с React Server Component в Next.js (для Next.js 13+).

  • Поддерживает lazy loading (разделение переводов по чанкам/файлам) для ускорения страниц.

  • Всё выше делается просто относительной других популярных библиотек.

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

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

i18next

react-intl

@lingui/react

Популярность
(звёзд на GitHub)

🟠 7.9k

🟢14.4k

🟠4.8k

Типизация

🟢

🟠(нужно повозиться)

🔴

Server Side Components

🟢

🟠(нужно повозиться)

🟢

Lazy loading

🟢 (через namespaces)

🟢(нужно повозиться с динамическим импортом)

🟠(нужно повозиться)

Субъективное удобство

🟢 (вызов всего через t('...'))

🔴(все переводы нужно оборачивать в компонент)

🔴(все переводы нужно оборачивать в компонент и нет нормальной типизации)

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

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Делали ли вы локализацию в проекте?

100% Да8
0% Нет0
0% Не моя зона ответственности0

Проголосовали 8 пользователей. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/articles/873984/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *