Как перестать бояться и начать локализовать: интернационализация TypeScript приложений
Вы когда-нибудь задумывались, почему одни проекты взлетают до международного уровня, а другие так и остаются "русскоязычными стартапами"? Чаще всего ответ прост: локализация. Да, та самая страшная аббревиатура i18n, от которой у многих разработчиков начинается нервный тик. Но давайте честно: сделать интернационализацию TypeScript приложения — это не rocket science. Это просто набор правильных подходов и инструментов. Сегодня мы разберем это на практике, с живыми примерами кода, без воды и заумных теорий.
Почему TypeScript — идеальный кандидат для i18n?
TypeScript даёт нам то, чего так не хватает обычному JavaScript: статическую типизацию. Когда речь заходит о переводах, это становится суперсилой. Представьте: вы меняете ключ перевода, а TypeScript ещё до запуска приложения кричит: "Эй, ты забыл обновить перевод в испанской локали!". Согласитесь, это лучше, чем ловить баги в продакшене.
Более того, современные библиотеки для i18n в TypeScript позволяют генерировать типы прямо из файлов с переводами. Это значит, что автодополнение в IDE будет работать как часы. Никаких магических строк, никаких опечаток. Только чистый, безопасный код.
Выбираем библиотеку: i18next vs react-intl
На рынке два главных игрока: i18next и react-intl (часть FormatJS). Оба отлично работают с TypeScript, но подходы разные.
- i18next — швейцарский нож интернационализации. Поддерживает плюрализацию, интерполяцию, контексты, ленивую загрузку. Отлично подходит для больших проектов.
- react-intl — более легковесный, заточен под React. Использует компонентный подход (
<FormattedMessage>) и мощную систему ICU MessageFormat.
В этой статье мы будем использовать i18next с официальным React-адаптером react-i18next. Почему? Потому что это самый популярный стек, и он даёт максимальный контроль.
Пошаговая настройка: от нуля до первого перевода
Давайте сразу к делу. Установим зависимости:
npm install i18next react-i18next i18next-browser-languagedetector
Теперь создадим конфигурационный файл. Это сердце нашей интернационализации TypeScript приложения.
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import enTranslation from './locales/en/translation.json';
import ruTranslation from './locales/ru/translation.json';
i18n
.use(LanguageDetector) // автоопределение языка браузера
.use(initReactI18next) // интеграция с React
.init({
resources: {
en: { translation: enTranslation },
ru: { translation: ruTranslation },
},
fallbackLng: 'en', // если перевод не найден
interpolation: {
escapeValue: false, // React уже экранирует
},
});
export default i18n;
Обратите внимание на fallbackLng. Это критически важно для юзер-экспириенса: если пользователь зашёл с браузером на суахили, мы покажем ему английскую версию, а не пустой экран.
Структура файлов переводов
Создадим папку locales и внутри неё подпапки для каждого языка. Вот пример файла en/translation.json:
{
"welcome": "Welcome to our application!",
"description": "This is a {{framework}} app with i18n support.",
"items": {
"zero": "No items",
"one": "One item",
"other": "{{count}} items"
}
}
А вот его русский аналог:
{
"welcome": "Добро пожаловать в наше приложение!",
"description": "Это приложение на {{framework}} с поддержкой i18n.",
"items": {
"zero": "Нет элементов",
"one": "Один элемент",
"other": "{{count}} элемента"
}
}
Типизация переводов: магия TypeScript
Теперь самое интересное. Чтобы TypeScript знал о наших переводах, нужно создать типы. Есть два пути: ручной (для перфекционистов) и автоматический (для прагматиков).
Автоматический путь: Используем пакет i18next-resources-for-ts или typesafe-i18n. Но мы пойдём ручным — так вы лучше поймёте механику.
// types/i18n.d.ts
import en from '../locales/en/translation.json';
type TranslationKeys = keyof typeof en;
declare module 'i18next' {
interface CustomTypeOptions {
resources: {
translation: typeof en;
};
}
}
Теперь, когда вы будете писать t(', IDE будет предлагать только существующие ключи. Никаких опечаток. Красота!
Использование в React-компонентах
Подключим наш конфиг в корне приложения:
// App.tsx
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n';
import MainPage from './pages/MainPage';
function App() {
return (
<I18nextProvider i18n={i18n}>
<MainPage />
</I18nextProvider>
);
}
А теперь сам компонент с переводами:
// pages/MainPage.tsx
import { useTranslation } from 'react-i18next';
const MainPage: React.FC = () => {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('description', { framework: 'React' })}</p>
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('ru')}>Русский</button>
</div>
</div>
);
};
Видите { framework: 'React' }? Это интерполяция. В файле перевода у нас есть плейсхолдер {{framework}}, который заменяется на переданное значение. Удобно для динамических текстов.
Работа с плюрализацией
Русский язык — настоящий вызов для плюрализации. В английском есть one/other, а у нас — zero/one/few/many/other. i18next справляется на ура