← React/Интернационализация: i18n в React#293 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

Интернационализация: i18n в React

Что такое i18n

Интернационализация (i18n) — процесс подготовки приложения к работе на разных языках и в разных регионах. Название i18n: первая буква "i", потом 18 букв, потом "n".

Локализация (l10n) — адаптация под конкретную локаль: переводы, форматы дат, валюты, числа, RTL.

Типичные задачи i18n:

  • Переводы строк
  • Форматирование дат: 01/31/2024 (en-US) vs 31.01.2024 (ru-RU)
  • Форматирование чисел: 1,000.50 (en) vs 1 000,50 (ru)
  • Множественные числа: "1 яблоко", "2 яблока", "5 яблок"
  • RTL (Right-to-Left): арабский, иврит
  • react-i18next

    Самая популярная библиотека для i18n в React:

    npm install react-i18next i18next
    npm install i18next-http-backend i18next-browser-languagedetector
    // i18n.ts — конфигурация
    import i18n from 'i18next'
    import { initReactI18next } from 'react-i18next'
    import LanguageDetector from 'i18next-browser-languagedetector'
    
    i18n
      .use(LanguageDetector)      // определяет язык браузера
      .use(initReactI18next)       // интеграция с React
      .init({
        fallbackLng: 'en',         // язык по умолчанию
        debug: process.env.NODE_ENV === 'development',
        resources: {
          en: {
            translation: {
              welcome: 'Welcome, {{name}}!',
              items_one: '{{count}} item',
              items_other: '{{count}} items',
            }
          },
          ru: {
            translation: {
              welcome: 'Добро пожаловать, {{name}}!',
              items_one: '{{count}} элемент',
              items_few: '{{count}} элемента',
              items_many: '{{count}} элементов',
            }
          }
        }
      })

    useTranslation хук

    import { useTranslation } from 'react-i18next'
    
    function Greeting({ name, itemCount }) {
      const { t, i18n } = useTranslation()
    
      return (
        <div>
          {/* Простой перевод */}
          <h1>{t('welcome', { name })}</h1>
    
          {/* Множественное число */}
          <p>{t('items', { count: itemCount })}</p>
    
          {/* Смена языка */}
          <button onClick={() => i18n.changeLanguage('ru')}>RU</button>
          <button onClick={() => i18n.changeLanguage('en')}>EN</button>
    
          {/* Текущий язык */}
          <span>Язык: {i18n.language}</span>
        </div>
      )
    }

    Пространства имён (namespaces)

    // Разделяем переводы по файлам
    // locales/ru/common.json
    { "save": "Сохранить", "cancel": "Отмена" }
    
    // locales/ru/dashboard.json
    { "title": "Панель управления", "stats": "Статистика" }
    
    // Использование
    const { t } = useTranslation(['common', 'dashboard'])
    t('save')              // из common
    t('dashboard:title')   // из dashboard

    Интерполяция и форматирование

    // Переменные
    t('greeting', { name: 'Алексей' })
    // "Привет, Алексей!"
    
    // Форматирование через i18next-icu или встроенное
    t('price', { price: 1999.99, formatParams: {
      price: { currency: 'RUB', style: 'currency', locale: 'ru-RU' }
    }})
    // "1 999,99 ₽"
    
    // Дата
    t('date', { date: new Date(), formatParams: {
      date: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
    }})

    Множественное число

    Правила очень разные в разных языках:

    // Английский: one / other
    // 1 apple, 2 apples
    
    // Русский: one / few / many (+ дополнительные правила)
    // 1 яблоко, 2 яблока, 5 яблок, 11 яблок (исключение!)
    // 21 яблоко, 22 яблока, 25 яблок
    
    // В файле переводов
    {
      "apples_one":   "{{count}} яблоко",
      "apples_few":   "{{count}} яблока",
      "apples_many":  "{{count}} яблок",
      "apples_other": "{{count}} яблока"  // дробные числа
    }
    
    // Использование — i18next сам выберет форму!
    t('apples', { count: 1 })   // "1 яблоко"
    t('apples', { count: 3 })   // "3 яблока"
    t('apples', { count: 11 })  // "11 яблок"
    t('apples', { count: 21 })  // "21 яблоко"

    RTL поддержка

    // Определяем направление по локали
    const rtlLanguages = ['ar', 'he', 'fa', 'ur']
    const isRTL = rtlLanguages.includes(i18n.language)
    
    // В HTML
    document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
    document.documentElement.lang = i18n.language
    
    // В CSS — используем логические свойства
    .card {
      margin-inline-start: 16px;  /* вместо margin-left */
      padding-inline-end: 8px;    /* вместо padding-right */
    }

    Загрузка переводов

    // Ленивая загрузка локалей (не встраивать всё в бандл!)
    import Backend from 'i18next-http-backend'
    
    i18n.use(Backend).init({
      backend: {
        loadPath: '/locales/{{lng}}/{{ns}}.json',
      }
    })
    
    // Suspense для ожидания загрузки
    function App() {
      return (
        <Suspense fallback="Загрузка...">
          <MyApp />
        </Suspense>
      )
    }

    Примеры

    Полноценная i18n-система на ванильном JS: переводы, функция t(), интерполяция переменных, правила множественного числа и переключение локали

    // Реализуем минималистичную i18n систему:
    // переводы, t(), плюрализация и смена языка.
    
    // --- Правила множественного числа ---
    
    const pluralRules = {
      en: (count) => count === 1 ? 'one' : 'other',
      ru: (count) => {
        const mod10 = count % 10
        const mod100 = count % 100
        if (mod100 >= 11 && mod100 <= 19) return 'many'
        if (mod10 === 1) return 'one'
        if (mod10 >= 2 && mod10 <= 4) return 'few'
        return 'many'
      },
      de: (count) => count === 1 ? 'one' : 'other',
    }
    
    // --- Переводы ---
    
    const translations = {
      en: {
        greeting: 'Hello, {{name}}!',
        farewell: 'Goodbye, {{name}}!',
        items_one: '{{count}} item',
        items_other: '{{count}} items',
        price: 'Price: USD {{amount}}',
      },
      ru: {
        greeting: 'Привет, {{name}}!',
        farewell: 'До свидания, {{name}}!',
        items_one: '{{count}} элемент',
        items_few: '{{count}} элемента',
        items_many: '{{count}} элементов',
        price: 'Цена: {{amount}} ₽',
      },
      de: {
        greeting: 'Hallo, {{name}}!',
        farewell: 'Auf Wiedersehen, {{name}}!',
        items_one: '{{count}} Element',
        items_other: '{{count}} Elemente',
      },
    }
    
    // --- I18n движок ---
    
    function createI18nEngine(defaultLocale = 'en') {
      let currentLocale = defaultLocale
    
      // Интерполяция: заменить {{variable}} на значения
      function interpolate(str, params) {
        return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
          return params[key] !== undefined ? params[key] : match
        })
      }
    
      // Функция перевода
      function t(key, params = {}) {
        const locale = currentLocale
        const dict = translations[locale] || translations['en']
    
        // Если передан count — ищем форму множественного числа
        if (params.count !== undefined) {
          const rule = pluralRules[locale] || pluralRules['en']
          const form = rule(params.count)
          const pluralKey = key + '_' + form
          const str = dict[pluralKey] || dict[key + '_other'] || dict[key]
          return str ? interpolate(str, params) : key
        }
    
        const str = dict[key]
        return str ? interpolate(str, params) : key
      }
    
      return {
        t,
        setLocale(locale) {
          if (!translations[locale]) {
            console.warn('Локаль не найдена:', locale)
            return
          }
          currentLocale = locale
          console.log('Локаль изменена на:', locale)
        },
        getLocale() { return currentLocale },
        getSupportedLocales() { return Object.keys(translations) },
      }
    }
    
    // --- Тесты ---
    
    const i18n = createI18nEngine('en')
    const { t, setLocale, getLocale } = i18n
    
    console.log('=== English ===')
    console.log(t('greeting', { name: 'Alex' }))     // Hello, Alex!
    console.log(t('items', { count: 1 }))             // 1 item
    console.log(t('items', { count: 5 }))             // 5 items
    
    console.log('
    === Русский ===')
    setLocale('ru')
    console.log(t('greeting', { name: 'Алексей' }))  // Привет, Алексей!
    console.log(t('price', { amount: 999 }))          // Цена: 999 ₽
    
    // Проверка всех форм множественного числа
    const counts = [1, 2, 5, 11, 21, 22, 25]
    counts.forEach(n => console.log(n + ': ' + t('items', { count: n })))
    
    console.log('
    === Deutsch ===')
    setLocale('de')
    console.log(t('greeting', { name: 'Hans' }))
    console.log(t('items', { count: 1 }))
    console.log(t('items', { count: 3 }))
    
    console.log('
    Текущая локаль:', getLocale())
    console.log('Доступные локали:', i18n.getSupportedLocales())

    Интернационализация: i18n в React

    Что такое i18n

    Интернационализация (i18n) — процесс подготовки приложения к работе на разных языках и в разных регионах. Название i18n: первая буква "i", потом 18 букв, потом "n".

    Локализация (l10n) — адаптация под конкретную локаль: переводы, форматы дат, валюты, числа, RTL.

    Типичные задачи i18n:

  • Переводы строк
  • Форматирование дат: 01/31/2024 (en-US) vs 31.01.2024 (ru-RU)
  • Форматирование чисел: 1,000.50 (en) vs 1 000,50 (ru)
  • Множественные числа: "1 яблоко", "2 яблока", "5 яблок"
  • RTL (Right-to-Left): арабский, иврит
  • react-i18next

    Самая популярная библиотека для i18n в React:

    npm install react-i18next i18next
    npm install i18next-http-backend i18next-browser-languagedetector
    // i18n.ts — конфигурация
    import i18n from 'i18next'
    import { initReactI18next } from 'react-i18next'
    import LanguageDetector from 'i18next-browser-languagedetector'
    
    i18n
      .use(LanguageDetector)      // определяет язык браузера
      .use(initReactI18next)       // интеграция с React
      .init({
        fallbackLng: 'en',         // язык по умолчанию
        debug: process.env.NODE_ENV === 'development',
        resources: {
          en: {
            translation: {
              welcome: 'Welcome, {{name}}!',
              items_one: '{{count}} item',
              items_other: '{{count}} items',
            }
          },
          ru: {
            translation: {
              welcome: 'Добро пожаловать, {{name}}!',
              items_one: '{{count}} элемент',
              items_few: '{{count}} элемента',
              items_many: '{{count}} элементов',
            }
          }
        }
      })

    useTranslation хук

    import { useTranslation } from 'react-i18next'
    
    function Greeting({ name, itemCount }) {
      const { t, i18n } = useTranslation()
    
      return (
        <div>
          {/* Простой перевод */}
          <h1>{t('welcome', { name })}</h1>
    
          {/* Множественное число */}
          <p>{t('items', { count: itemCount })}</p>
    
          {/* Смена языка */}
          <button onClick={() => i18n.changeLanguage('ru')}>RU</button>
          <button onClick={() => i18n.changeLanguage('en')}>EN</button>
    
          {/* Текущий язык */}
          <span>Язык: {i18n.language}</span>
        </div>
      )
    }

    Пространства имён (namespaces)

    // Разделяем переводы по файлам
    // locales/ru/common.json
    { "save": "Сохранить", "cancel": "Отмена" }
    
    // locales/ru/dashboard.json
    { "title": "Панель управления", "stats": "Статистика" }
    
    // Использование
    const { t } = useTranslation(['common', 'dashboard'])
    t('save')              // из common
    t('dashboard:title')   // из dashboard

    Интерполяция и форматирование

    // Переменные
    t('greeting', { name: 'Алексей' })
    // "Привет, Алексей!"
    
    // Форматирование через i18next-icu или встроенное
    t('price', { price: 1999.99, formatParams: {
      price: { currency: 'RUB', style: 'currency', locale: 'ru-RU' }
    }})
    // "1 999,99 ₽"
    
    // Дата
    t('date', { date: new Date(), formatParams: {
      date: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }
    }})

    Множественное число

    Правила очень разные в разных языках:

    // Английский: one / other
    // 1 apple, 2 apples
    
    // Русский: one / few / many (+ дополнительные правила)
    // 1 яблоко, 2 яблока, 5 яблок, 11 яблок (исключение!)
    // 21 яблоко, 22 яблока, 25 яблок
    
    // В файле переводов
    {
      "apples_one":   "{{count}} яблоко",
      "apples_few":   "{{count}} яблока",
      "apples_many":  "{{count}} яблок",
      "apples_other": "{{count}} яблока"  // дробные числа
    }
    
    // Использование — i18next сам выберет форму!
    t('apples', { count: 1 })   // "1 яблоко"
    t('apples', { count: 3 })   // "3 яблока"
    t('apples', { count: 11 })  // "11 яблок"
    t('apples', { count: 21 })  // "21 яблоко"

    RTL поддержка

    // Определяем направление по локали
    const rtlLanguages = ['ar', 'he', 'fa', 'ur']
    const isRTL = rtlLanguages.includes(i18n.language)
    
    // В HTML
    document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
    document.documentElement.lang = i18n.language
    
    // В CSS — используем логические свойства
    .card {
      margin-inline-start: 16px;  /* вместо margin-left */
      padding-inline-end: 8px;    /* вместо padding-right */
    }

    Загрузка переводов

    // Ленивая загрузка локалей (не встраивать всё в бандл!)
    import Backend from 'i18next-http-backend'
    
    i18n.use(Backend).init({
      backend: {
        loadPath: '/locales/{{lng}}/{{ns}}.json',
      }
    })
    
    // Suspense для ожидания загрузки
    function App() {
      return (
        <Suspense fallback="Загрузка...">
          <MyApp />
        </Suspense>
      )
    }

    Примеры

    Полноценная i18n-система на ванильном JS: переводы, функция t(), интерполяция переменных, правила множественного числа и переключение локали

    // Реализуем минималистичную i18n систему:
    // переводы, t(), плюрализация и смена языка.
    
    // --- Правила множественного числа ---
    
    const pluralRules = {
      en: (count) => count === 1 ? 'one' : 'other',
      ru: (count) => {
        const mod10 = count % 10
        const mod100 = count % 100
        if (mod100 >= 11 && mod100 <= 19) return 'many'
        if (mod10 === 1) return 'one'
        if (mod10 >= 2 && mod10 <= 4) return 'few'
        return 'many'
      },
      de: (count) => count === 1 ? 'one' : 'other',
    }
    
    // --- Переводы ---
    
    const translations = {
      en: {
        greeting: 'Hello, {{name}}!',
        farewell: 'Goodbye, {{name}}!',
        items_one: '{{count}} item',
        items_other: '{{count}} items',
        price: 'Price: USD {{amount}}',
      },
      ru: {
        greeting: 'Привет, {{name}}!',
        farewell: 'До свидания, {{name}}!',
        items_one: '{{count}} элемент',
        items_few: '{{count}} элемента',
        items_many: '{{count}} элементов',
        price: 'Цена: {{amount}} ₽',
      },
      de: {
        greeting: 'Hallo, {{name}}!',
        farewell: 'Auf Wiedersehen, {{name}}!',
        items_one: '{{count}} Element',
        items_other: '{{count}} Elemente',
      },
    }
    
    // --- I18n движок ---
    
    function createI18nEngine(defaultLocale = 'en') {
      let currentLocale = defaultLocale
    
      // Интерполяция: заменить {{variable}} на значения
      function interpolate(str, params) {
        return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
          return params[key] !== undefined ? params[key] : match
        })
      }
    
      // Функция перевода
      function t(key, params = {}) {
        const locale = currentLocale
        const dict = translations[locale] || translations['en']
    
        // Если передан count — ищем форму множественного числа
        if (params.count !== undefined) {
          const rule = pluralRules[locale] || pluralRules['en']
          const form = rule(params.count)
          const pluralKey = key + '_' + form
          const str = dict[pluralKey] || dict[key + '_other'] || dict[key]
          return str ? interpolate(str, params) : key
        }
    
        const str = dict[key]
        return str ? interpolate(str, params) : key
      }
    
      return {
        t,
        setLocale(locale) {
          if (!translations[locale]) {
            console.warn('Локаль не найдена:', locale)
            return
          }
          currentLocale = locale
          console.log('Локаль изменена на:', locale)
        },
        getLocale() { return currentLocale },
        getSupportedLocales() { return Object.keys(translations) },
      }
    }
    
    // --- Тесты ---
    
    const i18n = createI18nEngine('en')
    const { t, setLocale, getLocale } = i18n
    
    console.log('=== English ===')
    console.log(t('greeting', { name: 'Alex' }))     // Hello, Alex!
    console.log(t('items', { count: 1 }))             // 1 item
    console.log(t('items', { count: 5 }))             // 5 items
    
    console.log('
    === Русский ===')
    setLocale('ru')
    console.log(t('greeting', { name: 'Алексей' }))  // Привет, Алексей!
    console.log(t('price', { amount: 999 }))          // Цена: 999 ₽
    
    // Проверка всех форм множественного числа
    const counts = [1, 2, 5, 11, 21, 22, 25]
    counts.forEach(n => console.log(n + ': ' + t('items', { count: n })))
    
    console.log('
    === Deutsch ===')
    setLocale('de')
    console.log(t('greeting', { name: 'Hans' }))
    console.log(t('items', { count: 1 }))
    console.log(t('items', { count: 3 }))
    
    console.log('
    Текущая локаль:', getLocale())
    console.log('Доступные локали:', i18n.getSupportedLocales())

    Задание

    Создай React-приложение с переключателем языка. Используй Context API для хранения текущей локали и функции перевода t(). Компонент LanguageSwitcher позволяет переключаться между языками. Компонент Greeting отображает приветствие на выбранном языке.

    Подсказка

    В I18nProvider: используй translations[locale] для словаря и dict[key] для получения строки. В интерполяции проверяй params[varName]. Передай value в Provider. В LanguageSwitcher: onClick={() => setLocale(lang). В Greeting: используй t("greeting", { name }), t("welcome"), t("items", { count: itemCount }).

    Загружаем среду выполнения...
    Загружаем AI-помощника...