Composition API и i18next

Интеграция i18next с Vue 3 через Composition API позволяет строить гибкую систему интернационализации без привязки к Options API. Основная идея заключается в создании composable-функций, предоставляющих доступ к текущему языку, переводам и механизмам смены локали.

Composition API особенно хорошо сочетается с i18next, поскольку библиотека изначально построена вокруг независимых экземпляров, событий и реактивного обновления состояния.


Установка зависимостей

Базовая установка:

npm install i18next

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

npm install i18next-browser-languagedetector
npm install i18next-http-backend

Структура проекта

Одна из наиболее удобных структур:

src/
├── i18n/
│   ├── index.js
│   ├── locales/
│   │   ├── en/
│   │   │   └── common.json
│   │   └── ru/
│   │       └── common.json
│   └── composables/
│       └── useTranslation.js

Инициализация i18next

Файл i18n/index.js:

import i18next from 'i18next'
import Backend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'

i18next
  .use(Backend)
  .use(LanguageDetector)
  .init({
    fallbackLng: 'en',

    debug: true,

    ns: ['common'],
    defaultNS: 'common',

    interpolation: {
      escapeValue: false
    },

    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json'
    }
  })

export default i18next

Пример файлов переводов

locales/en/common.json

{
  "welcome": "Welcome",
  "profile": "Profile",
  "logout": "Logout"
}

locales/ru/common.json

{
  "welcome": "Добро пожаловать",
  "profile": "Профиль",
  "logout": "Выйти"
}

Создание composable для переводов

Composition API предполагает перенос логики в composable-функции.

Файл useTranslation.js:

import { ref, onMounted, onUnmounted } from 'vue'
import i18next from '../index'

const currentLanguage = ref(i18next.language)

export function useTranslation() {

  const updateLanguage = (lng) => {
    currentLanguage.value = lng
  }

  onMounted(() => {
    i18next.on('languageChanged', updateLanguage)
  })

  onUnmounted(() => {
    i18next.off('languageChanged', updateLanguage)
  })

  const t = (key, options = {}) => {
    return i18next.t(key, options)
  }

  const changeLanguage = async (lng) => {
    await i18next.changeLanguage(lng)
  }

  return {
    t,
    currentLanguage,
    changeLanguage
  }
}

Использование composable в компоненте

<script setup>
import { useTranslation } from '@/i18n/composables/useTranslation'

const {
  t,
  currentLanguage,
  changeLanguage
} = useTranslation()
</script>

<template>
  <div>
    <h1>{{ t('welcome') }}</h1>

    <p>{{ currentLanguage }}</p>

    <button @click="changeLanguage('en')">
      English
    </button>

    <button @click="changeLanguage('ru')">
      Русский
    </button>
  </div>
</template>

Реактивность и обновление интерфейса

i18next сам по себе не является реактивным. Vue не отслеживает внутренние изменения библиотеки автоматически. Именно поэтому требуется:

  • подписка на событие languageChanged;
  • хранение текущего языка в ref;
  • принудительное создание реактивной зависимости.

Без этого интерфейс не будет обновляться после смены языка.


Использование computed-переводов

Иногда полезно оборачивать переводы в computed.

import { computed } from 'vue'
import { useTranslation } from './useTranslation'

export function usePageTitle() {

  const { t } = useTranslation()

  const title = computed(() => t('profile'))

  return {
    title
  }
}

Namespace в Composition API

i18next поддерживает разделение переводов по namespace.

Пример:

locales/
├── en/
│   ├── common.json
│   └── auth.json

Инициализация:

i18next.init({
  ns: ['common', 'auth'],
  defaultNS: 'common'
})

Использование:

t('login', { ns: 'auth' })

Либо:

t('auth:login')

Создание composable с namespace

import i18next from '../index'

export function useNamespaceTranslation(namespace) {

  const t = (key, options = {}) => {
    return i18next.t(`${namespace}:${key}`, options)
  }

  return {
    t
  }
}

Использование:

const { t } = useNamespaceTranslation('auth')

Ленивая загрузка переводов

При больших приложениях все переводы не загружаются сразу.

Динамическая подгрузка namespace

await i18next.loadNamespaces(['dashboard'])

После этого namespace становится доступен:

t('dashboard:title')

Асинхронная загрузка языков

await i18next.changeLanguage('de')

Поскольку загрузка файлов может быть сетевой операцией, смена языка является асинхронной.


Состояние загрузки локализации

Composition API позволяет удобно организовать индикаторы загрузки.

import { ref } from 'vue'
import i18next from '../index'

export function useLanguageSwitcher() {

  const loading = ref(false)

  const switchLanguage = async (lng) => {
    loading.value = true

    try {
      await i18next.changeLanguage(lng)
    } finally {
      loading.value = false
    }
  }

  return {
    loading,
    switchLanguage
  }
}

Интерполяция переменных

i18next поддерживает шаблонные переменные.

common.json

{
  "hello": "Hello, {{name}}"
}

Использование:

t('hello', {
  name: 'Alex'
})

Результат:

Hello, Alex

Интерполяция объектов

{
  "userInfo": "Name: {{user.name}}, Age: {{user.age}}"
}
t('userInfo', {
  user: {
    name: 'John',
    age: 28
  }
})

Форматирование через formatter

i18next.init({
  interpolation: {
    format(value, format) {

      if (format === 'uppercase') {
        return value.toUpperCase()
      }

      return value
    }
  }
})

Перевод:

{
  "title": "{{name, uppercase}}"
}

Pluralization

i18next содержит встроенную систему множественных форм.

{
  "item_one": "{{count}} item",
  "item_other": "{{count}} items"
}

Использование:

t('item', { count: 1 })
t('item', { count: 10 })

Русские множественные формы

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

  • one
  • few
  • many
  • other

Пример:

{
  "message_one": "{{count}} сообщение",
  "message_few": "{{count}} сообщения",
  "message_many": "{{count}} сообщений"
}

Контекстные переводы

Контекст позволяет учитывать пол, роль или другие параметры.

{
  "welcome_male": "Добро пожаловать",
  "welcome_female": "Добро пожаловать"
}
t('welcome', {
  context: 'male'
})

HTML внутри переводов

{
  "content": "<strong>Important</strong> message"
}

Использование:

<div v-html="t('content')"></div>

Подход требует осторожности из-за XSS-рисков.


Escape значений

По умолчанию браузерный вывод экранируется Vue, однако i18next тоже имеет механизм escape.

interpolation: {
  escapeValue: true
}

Во Vue обычно устанавливают:

escapeValue: false

Поскольку Vue уже выполняет защиту при обычном выводе через {{ }}.


Смена языка через глобальное состояние

Иногда composable объединяют с Pinia.

Store локализации

import { defineStore } from 'pinia'
import i18next from '@/i18n'

export const useLocaleStore = defineStore('locale', {

  state: () => ({
    current: i18next.language
  }),

  actions: {

    async setLanguage(lng) {
      await i18next.changeLanguage(lng)
      this.current = lng
    }
  }
})

Использование watch

Composition API позволяет реагировать на смену языка.

watch(currentLanguage, (newLang) => {
  console.log('Language changed:', newLang)
})

Использование provide/inject

Иногда экземпляр i18next внедряется через Dependency Injection.

main.js

import { createApp } from 'vue'
import App from './App.vue'
import i18next from './i18n'

const app = createApp(App)

app.provide('i18next', i18next)

app.mount('#app')

Использование

import { inject } from 'vue'

const i18next = inject('i18next')

SSR и Composition API

При серверном рендеринге необходимо:

  • создавать отдельный экземпляр i18next на каждый запрос;
  • избегать глобального singleton-состояния;
  • синхронизировать язык между сервером и клиентом.

Пример фабрики:

import i18next from 'i18next'

export function createI18nInstance() {

  const instance = i18next.createInstance()

  instance.init({
    fallbackLng: 'en'
  })

  return instance
}

Использование Suspense

Vue 3 поддерживает Suspense, что удобно для асинхронной загрузки переводов.

<Suspense>
  <DashboardPage />
</Suspense>

Компонент может ожидать загрузку namespace:

await i18next.loadNamespaces(['dashboard'])

Локализация маршрутов

Composition API удобно комбинируется с Vue Router.

const routes = [
  {
    path: '/ru/profile',
    component: Profile
  },
  {
    path: '/en/profile',
    component: Profile
  }
]

Синхронизация языка с URL

watch(route, async (to) => {

  const lang = to.params.lang

  if (lang) {
    await i18next.changeLanguage(lang)
  }
})

Хранение языка в localStorage

const savedLanguage = localStorage.getItem('language')

if (savedLanguage) {
  i18next.changeLanguage(savedLanguage)
}

Сохранение:

await i18next.changeLanguage('ru')

localStorage.setItem('language', 'ru')

Автоопределение языка браузера

i18next-browser-languagedetector умеет определять язык через:

  • navigator.language;
  • cookie;
  • localStorage;
  • query string;
  • path;
  • subdomain.

Настройка:

detection: {
  order: ['localStorage', 'navigator'],
  caches: ['localStorage']
}

Обработка отсутствующих переводов

i18next.init({
  saveMissing: true
})

Либо:

missingKeyHandler(lng, ns, key) {
  console.warn(`Missing translation: ${key}`)
}

Fallback языки

fallbackLng: {
  'de-CH': ['fr', 'it'],
  default: ['en']
}

Типизация переводов в TypeScript

Composition API особенно хорошо раскрывается вместе с TypeScript.

Типизированный composable

type TranslationKey =
  | 'welcome'
  | 'profile'
  | 'logout'

export function useTranslation() {

  const t = (key: TranslationKey) => {
    return i18next.t(key)
  }

  return {
    t
  }
}

Генерация типов из JSON

В больших проектах типы генерируют автоматически.

Пример:

import common from './locales/en/common.json'

type TranslationSchema = typeof common

Создание plugin для Vue

Можно создать полноценный Vue plugin.

export default {

  install(app) {

    app.config.globalProperties.$t = i18next.t.bind(i18next)

    app.provide('i18next', i18next)
  }
}

Подключение:

app.use(i18nPlugin)

Использование в <script setup>

Composition API особенно удобен в script setup.

<script setup>
import { useTranslation } from '@/i18n/composables/useTranslation'

const { t } = useTranslation()
</script>

<template>
  <h1>{{ t('welcome') }}</h1>
</template>

Производительность

При работе с локализацией важно учитывать:

  • количество namespace;
  • размер JSON-файлов;
  • частоту вызова t;
  • ленивую загрузку;
  • кеширование переводов.

Антипаттерны

Вызов t() вне реактивного контекста

Плохо:

const title = t('profile')

После смены языка значение не обновится.

Лучше:

const title = computed(() => t('profile'))

Глобальный импорт во всех файлах

Плохо:

import i18next from '@/i18n'

в каждом компоненте.

Лучше использовать composable.


Один огромный JSON

Плохо:

common.json -> 5000 строк

Лучше:

auth.json
dashboard.json
profile.json

Тестирование composable

Пример теста:

import { useTranslation } from '@/i18n/composables/useTranslation'

describe('translation composable', () => {

  it('returns translated string', () => {

    const { t } = useTranslation()

    expect(t('welcome')).toBe('Welcome')
  })
})

Модульная архитектура локализации

Крупные приложения часто разделяют переводы по доменам:

modules/
├── auth/
│   ├── locales/
│   └── components/
├── dashboard/
│   ├── locales/
│   └── composables/

Каждый модуль содержит собственные namespace и composable.


Интеграция с динамическими компонентами

const component = computed(() => {

  if (currentLanguage.value === 'ru') {
    return RussianComponent
  }

  return EnglishComponent
})

Локализация мета-тегов

watchEffect(() => {
  document.title = t('pageTitle')
})

Использование ICU-формата

Через дополнительный плагин:

npm install i18next-icu

Подключение:

import ICU from 'i18next-icu'

i18next.use(ICU)

Пример:

{
  "items": "{count, plural, one {# item} other {# items}}"
}

События i18next

Подписка:

i18next.on('initialized', () => {
  console.log('i18next initialized')
})

Смена языка:

i18next.on('languageChanged', (lng) => {
  console.log(lng)
})

Ошибка загрузки:

i18next.on('failedLoading', (lng, ns, msg) => {
  console.error(msg)
})

Создание реактивного wrapper

Иногда создают полноценный реактивный слой:

import { reactive } from 'vue'

export const i18nState = reactive({
  language: i18next.language
})

i18next.on('languageChanged', (lng) => {
  i18nState.language = lng
})

Использование watchEffect

watchEffect(() => {
  console.log(t('welcome'))
})

watchEffect автоматически отслеживает реактивные зависимости.


Интеграция с Vite

Vite отлично работает с динамическим импортом переводов.

const messages = await import(`./locales/${lng}/common.json`)

Горячая перезагрузка переводов

Во время разработки переводы можно обновлять без перезапуска приложения:

i18next.reloadResources()

Миграция с Vue I18n на i18next

Основные различия:

Vue I18n i18next
Тесная интеграция с Vue Независимая библиотека
Vue-ориентированный API Универсальный API
Простая настройка Более гибкая архитектура
Меньше middleware Большая экосистема

Когда Composition API особенно полезен

Composition API раскрывает преимущества i18next в следующих сценариях:

  • крупные SPA;
  • модульные приложения;
  • SSR;
  • микрофронтенды;
  • динамическая загрузка переводов;
  • сложная реактивная логика;
  • повторно используемые composable;
  • строгая TypeScript-типизация.