← Курс/Интернационализация с vue-i18n#253 из 257+25 XP

Интернационализация с vue-i18n

Зачем нужна интернационализация

**Интернационализация (i18n)** — это подготовка приложения к работе на разных языках. Вместо жёсткого текста в шаблонах вы используете ключи, которые переводятся в нужную строку в зависимости от выбранной локали. **vue-i18n** — официальная библиотека i18n для Vue.

npm install vue-i18n

Настройка

// src/i18n/index.ts
import { createI18n } from 'vue-i18n'

const messages = {
  ru: {
    hello: 'Привет, {name}!',
    nav: {
      home: 'Главная',
      about: 'О нас',
    },
    items: 'нет товаров | {n} товар | {n} товара | {n} товаров',
  },
  en: {
    hello: 'Hello, {name}!',
    nav: {
      home: 'Home',
      about: 'About',
    },
    items: 'no items | {n} item | {n} items',
  },
}

export const i18n = createI18n({
  legacy: false,       // используем Composition API
  locale: 'ru',        // язык по умолчанию
  fallbackLocale: 'en', // запасной язык
  messages,
})
// main.ts
import { createApp } from 'vue'
import { i18n } from './i18n'
import App from './App.vue'

createApp(App).use(i18n).mount('#app')

useI18n в компонентах

import { useI18n } from 'vue-i18n'

const { t, locale, availableLocales } = useI18n()

// Простой перевод
t('nav.home')          // 'Главная'

// С интерполяцией
t('hello', { name: 'Alice' })  // 'Привет, Alice!'

// Смена языка
locale.value = 'en'    // реактивно — все t() немедленно обновятся

$t() в шаблонах

<template>
  <nav>
    <a href="/">{{ $t('nav.home') }}</a>
    <a href="/about">{{ $t('nav.about') }}</a>
  </nav>

  <p>{{ $t('hello', { name: user.name }) }}</p>

  <select v-model="$i18n.locale">
    <option value="ru">Русский</option>
    <option value="en">English</option>
  </select>
</template>

Плурализация (множественное число)

Русский язык имеет три формы числа. vue-i18n поддерживает это через |:

const messages = {
  ru: {
    // 0 | 1 | 2-4 | 5+
    apples: 'нет яблок | {n} яблоко | {n} яблока | {n} яблок',
  },
  en: {
    apples: 'no apples | {n} apple | {n} apples',
  },
}

// Использование:
t('apples', 0)   // нет яблок
t('apples', 1)   // 1 яблоко
t('apples', 3)   // 3 яблока
t('apples', 5)   // 5 яблок
t('apples', 21)  // 21 яблоко (автоматически!)

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

const { d, n } = useI18n()

// Дата
d(new Date(), 'short')     // '04.03.2026' (для ru)
d(new Date(), 'long')      // '4 марта 2026 г.'

// Число
n(1234567.89, 'currency')  // '1 234 567,89 ₽' (для ru)
n(0.75, 'percent')         // '75%'
// Конфигурация форматов в createI18n:
datetimeFormats: {
  ru: {
    short: { year: 'numeric', month: '2-digit', day: '2-digit' },
    long:  { year: 'numeric', month: 'long', day: 'numeric' },
  },
},
numberFormats: {
  ru: {
    currency: { style: 'currency', currency: 'RUB' },
    percent:  { style: 'percent' },
  },
},

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

// Загружаем перевод только когда он нужен
async function setLocale(locale) {
  if (!i18n.global.availableLocales.includes(locale)) {
    const messages = await import(`./locales/${locale}.json`)
    i18n.global.setLocaleMessage(locale, messages.default)
  }
  i18n.global.locale.value = locale
}

Примеры

Реализация мини-библиотеки i18n с поддержкой вложенных ключей, интерполяции и плурализации — как работает vue-i18n под капотом

// ============================================
// Мини-реализация i18n (как работает vue-i18n)
// ============================================

class I18n {
  constructor(options) {
    this.messages = options.messages || {}
    this.locale = options.locale || 'en'
    this.fallbackLocale = options.fallbackLocale || 'en'
  }

  // Получить сообщение по вложенному ключу: 'nav.home' -> messages.nav.home
  _getMessage(key, locale) {
    const parts = key.split('.')
    let current = this.messages[locale]
    if (!current) return null

    for (const part of parts) {
      if (current === null || current === undefined) return null
      current = current[part]
    }
    return current || null
  }

  // Интерполяция: 'Привет, {name}!' + {name: 'Alice'} -> 'Привет, Alice!'
  _interpolate(str, params) {
    if (!params) return str
    return str.replace(/\{(\w+)\}/g, (_, key) => {
      return key in params ? params[key] : `{${key}}`
    })
  }

  // Плурализация для русского языка
  _pluralizeRu(forms, n) {
    const parts = forms.split('|').map(s => s.trim())
    if (n === 0 && parts.length >= 1) return parts[0]

    const abs = Math.abs(n)
    const mod10 = abs % 10
    const mod100 = abs % 100

    let idx
    if (mod100 >= 11 && mod100 <= 19) {
      idx = parts.length >= 4 ? 3 : parts.length - 1  // много
    } else if (mod10 === 1) {
      idx = 1  // один
    } else if (mod10 >= 2 && mod10 <= 4) {
      idx = parts.length >= 3 ? 2 : parts.length - 1  // несколько
    } else {
      idx = parts.length >= 4 ? 3 : parts.length - 1  // много
    }

    return parts[Math.min(idx, parts.length - 1)]
  }

  // Плурализация для английского
  _pluralizeEn(forms, n) {
    const parts = forms.split('|').map(s => s.trim())
    if (n === 0 && parts.length >= 1) return parts[0]
    return n === 1 ? (parts[1] || parts[0]) : (parts[2] || parts[1] || parts[0])
  }

  // Основной метод перевода
  t(key, paramsOrCount, params) {
    // Определяем плурализацию
    const isPluralCall = typeof paramsOrCount === 'number'
    const count = isPluralCall ? paramsOrCount : undefined
    const interpolateParams = isPluralCall ? (params || {}) : paramsOrCount

    // Ищем сообщение: сначала в текущей локали, потом в fallback
    const msg =
      this._getMessage(key, this.locale) ||
      this._getMessage(key, this.fallbackLocale)

    if (msg === null || msg === undefined) {
      console.warn(`[i18n] Ключ не найден: "${key}"`)
      return key
    }

    // Применяем плурализацию если передано число
    let result = msg
    if (isPluralCall && typeof msg === 'string' && msg.includes('|')) {
      const pluralFn = this.locale === 'ru' ? this._pluralizeRu : this._pluralizeEn
      result = pluralFn.call(this, msg, count)
      // Подставляем {n} в параметры
      const finalParams = { n: count, ...(interpolateParams || {}) }
      return this._interpolate(result, finalParams)
    }

    return this._interpolate(result, interpolateParams)
  }

  // Смена языка
  setLocale(locale) {
    if (!this.messages[locale]) {
      console.warn(`[i18n] Нет сообщений для локали: "${locale}"`)
      return
    }
    this.locale = locale
    console.log(`  Язык изменён на: ${locale}`)
  }
}

// ============================================
// Демонстрация
// ============================================

const i18n = new I18n({
  locale: 'ru',
  fallbackLocale: 'en',
  messages: {
    ru: {
      hello: 'Привет, {name}!',
      nav: {
        home: 'Главная',
        about: 'О нас',
        settings: 'Настройки',
      },
      apples: 'нет яблок | {n} яблоко | {n} яблока | {n} яблок',
      items: 'нет элементов | {n} элемент | {n} элемента | {n} элементов',
    },
    en: {
      hello: 'Hello, {name}!',
      nav: {
        home: 'Home',
        about: 'About',
        settings: 'Settings',
      },
      apples: 'no apples | {n} apple | {n} apples',
      onlyInEn: 'This key only exists in English',
    },
  },
})

console.log('=== Русская локаль ===')
console.log(i18n.t('hello', { name: 'Alice' }))
console.log(i18n.t('nav.home'))
console.log(i18n.t('nav.about'))

console.log('\n=== Плурализация (русский) ===')
for (const n of [0, 1, 2, 3, 5, 11, 21, 101]) {
  console.log(`  apples(${n}): ${i18n.t('apples', n)}`)
}

console.log('\n=== Смена на английский ===')
i18n.setLocale('en')
console.log(i18n.t('hello', { name: 'Bob' }))
console.log(i18n.t('nav.home'))

console.log('\n=== Плурализация (английский) ===')
for (const n of [0, 1, 2, 5]) {
  console.log(`  apples(${n}): ${i18n.t('apples', n)}`)
}

console.log('\n=== Fallback (ключ только в en) ===')
i18n.setLocale('ru')
console.log(i18n.t('onlyInEn'))  // fallback к 'en'

console.log('\n=== Несуществующий ключ ===')
console.log(i18n.t('nav.unknown'))  // вернёт ключ