Интеграция с Vue 3

Интеграция библиотеки интернационализации в приложение на Vue 3 обычно строится вокруг связки:

  • i18next
  • reactive-обёртки для Vue
  • плагина i18next-vue
  • backend-модуля для загрузки переводов
  • language detector для автоматического определения языка

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

npm install i18next i18next-vue

Дополнительно часто используются:

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

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

src/
├── locales/
│   ├── ru/
│   │   └── common.json
│   └── en/
│       └── common.json
├── i18n/
│   └── index.js
├── App.vue
└── main.js

Создание конфигурации i18next

Файл src/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

Основные параметры конфигурации

Параметр Назначение
fallbackLng Язык по умолчанию
debug Режим отладки
ns Пространства имён
defaultNS Namespace по умолчанию
backend.loadPath Путь к JSON-файлам
interpolation Настройки подстановки

Подключение i18next к Vue 3

Файл main.js:

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

import i18next from './i18n'
import I18NextVue from 'i18next-vue'

const app = createApp(App)

app.use(I18NextVue, { i18next })

app.mount('#app')

После подключения глобально становятся доступны:

  • $t
  • $i18next

Создание файлов переводов

Русский перевод

locales/ru/common.json

{
  "title": "Главная страница",
  "welcome": "Добро пожаловать",
  "description": "Многоязычное приложение Vue 3"
}

Английский перевод

locales/en/common.json

{
  "title": "Home page",
  "welcome": "Welcome",
  "description": "Vue 3 multilingual application"
}

Использование переводов в шаблонах

Компонент:

<template>
  <div>
    <h1>{{ $t('title') }}</h1>
    <p>{{ $t('welcome') }}</p>
    <p>{{ $t('description') }}</p>
  </div>
</template>

Метод $t() возвращает перевод по ключу.


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

Во Vue 3 чаще применяется Composition API.

<script setup>
import { useTranslation } from 'i18next-vue'

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

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

Что возвращает useTranslation

Свойство Назначение
t Функция перевода
i18next Экземпляр i18next
ready Состояние загрузки

Переключение языка

Изменение языка вручную

<script setup>
import { useTranslation } from 'i18next-vue'

const { i18next } = useTranslation()

const switchToRussian = () => {
  i18next.changeLanguage('ru')
}

const switchToEnglish = () => {
  i18next.changeLanguage('en')
}
</script>

<template>
  <div>
    <button @click="switchToRussian">
      Русский
    </button>

    <button @click="switchToEnglish">
      English
    </button>
  </div>
</template>

Автоматическое определение языка

Плагин i18next-browser-languagedetector определяет язык на основе:

  • браузера
  • localStorage
  • cookie
  • query-параметров
  • HTML-атрибута lang

Пример конфигурации:

i18next
  .use(LanguageDetector)
  .init({
    detection: {
      order: [
        'querystring',
        'cookie',
        'localStorage',
        'navigator'
      ],

      caches: ['localStorage']
    }
  })

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

В крупных приложениях переводы разделяются на модули.

Структура:

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

Конфигурация:

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

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

<template>
  <div>
    {{ $t('login', { ns: 'auth' }) }}
  </div>
</template>

Lazy loading переводов

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

Пример backend-конфигурации:

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

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


Интерполяция значений

i18next поддерживает динамические значения.

Файл переводов

{
  "greeting": "Привет, {{name}}!"
}

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

<template>
  <div>
    {{ $t('greeting', { name: 'Алексей' }) }}
  </div>
</template>

Результат:

Привет, Алексей!

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

Файл переводов

{
  "content": "Нажмите <strong>сюда</strong>"
}

Вывод HTML

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

Опасности v-html

Использование v-html может приводить к XSS-уязвимостям. Нельзя вставлять непроверенный пользовательский HTML.


Плюрализация

i18next поддерживает множественные формы.

Файл переводов

{
  "item_one": "{{count}} элемент",
  "item_few": "{{count}} элемента",
  "item_many": "{{count}} элементов"
}

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

<template>
  <div>
    {{ $t('item', { count: 1 }) }}
    {{ $t('item', { count: 3 }) }}
    {{ $t('item', { count: 10 }) }}
  </div>
</template>

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

Контекст используется для разделения вариантов.

Пример переводов

{
  "friend_male": "Друг",
  "friend_female": "Подруга"
}

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

t('friend', {
  context: 'female'
})

Форматирование дат и чисел

i18next поддерживает Intl API.

Конфигурация

i18next.init({
  interpolation: {
    format(value, format, lng) {
      if (format === 'currency') {
        return new Intl.NumberFormat(lng, {
          style: 'currency',
          currency: 'USD'
        }).format(value)
      }

      return value
    }
  }
})

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

{
  "price": "Цена: {{value, currency}}"
}
<template>
  <div>
    {{ $t('price', { value: 1999 }) }}
  </div>
</template>

Реактивность смены языка

После вызова:

i18next.changeLanguage('ru')

все компоненты Vue автоматически перерисовываются.

Это достигается через реактивную интеграцию i18next-vue.


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

Иногда перевод требуется в обычных JavaScript-модулях.

import i18next from '@/i18n'

const message = i18next.t('welcome')

console.log(message)

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

Пример структуры

/ru/about
/en/about

Пример конфигурации маршрутов:

const routes = [
  {
    path: '/:lang/about',
    component: AboutPage
  }
]

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

router.beforeEach((to, from, next) => {
  const lang = to.params.lang

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

  next()
})

Локализация заголовков страниц

router.afterEach((to) => {
  document.title = i18next.t(to.meta.title)
})

Маршрут:

{
  path: '/about',
  component: AboutPage,
  meta: {
    title: 'about.title'
  }
}

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

Переводы могут использоваться внутри store.

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

export const useUserStore = defineStore('user', {
  actions: {
    showMessage() {
      console.log(i18next.t('welcome'))
    }
  }
})

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

При асинхронной загрузке переводов полезен Suspense.

<Suspense>
  <template #default>
    <AppContent />
  </template>

  <template #fallback>
    Loading...
  </template>
</Suspense>

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

Конфигурация

i18next.init({
  saveMissing: true,

  missingKeyHandler(lng, ns, key) {
    console.warn(`Отсутствует перевод: ${key}`)
  }
})

Резервные языки

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

Если перевод отсутствует на швейцарском немецком, будет использоваться французский или итальянский.


Кэширование переводов

Пример через localStorage:

detection: {
  caches: ['localStorage']
}

Дополнительно можно использовать chained backend.


SSR и Nuxt 3

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

Типичная проблема:

Hydration mismatch

Причина — различие языков между сервером и клиентом.

Базовый принцип SSR

  • язык определяется на сервере
  • переводы загружаются заранее
  • клиент получает уже готовое состояние

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

Создание типа ключей

export type TranslationKeys =
  | 'title'
  | 'welcome'
  | 'description'

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

function translate(key: TranslationKeys) {
  return i18next.t(key)
}

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

Новый формат pluralization:

{
  "cart": {
    "one": "{{count}} товар",
    "few": "{{count}} товара",
    "many": "{{count}} товаров"
  }
}

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

t('cart', { count: 5 })

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

Основные рекомендации

Разделение namespace

Не следует хранить все переводы в одном JSON-файле.

Lazy loading

Загрузка только необходимых переводов уменьшает initial bundle.

Отключение debug в production

debug: process.env.NODE_ENV === 'development'

Предзагрузка популярных языков

preload: ['en', 'ru']

Частые ошибки

Неверный путь к translation-файлам

404 locales/en/common.json

Причины:

  • неправильный loadPath
  • неверная структура директорий
  • отсутствие файлов

Отсутствие namespace

Ошибка:

missingKey

Причина:

t('login')

при отсутствии namespace auth.


Потеря реактивности

Проблема:

const title = i18next.t('title')

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

Правильно:

<template>
  {{ t('title') }}
</template>

Конфликт SSR и browser detector

На сервере отсутствуют:

  • window
  • navigator
  • localStorage

Необходимо разделять серверную и клиентскую конфигурацию.


Архитектура крупного multilingual-приложения

Типичная структура:

src/
├── i18n/
│   ├── index.js
│   ├── config.js
│   ├── detectors/
│   ├── formatters/
│   └── plugins/
├── locales/
│   ├── en/
│   ├── ru/
│   └── de/
└── modules/
    ├── auth/
    ├── dashboard/
    └── profile/

Практические принципы

  • отдельный namespace для каждого модуля
  • единый fallback language
  • lazy loading
  • типизация ключей
  • автоматическая проверка отсутствующих переводов
  • отсутствие хардкод-строк в компонентах
  • хранение UI-текстов только в переводах