vue-i18n-kit — локализация для Vue 3 с ICU-плюрализацией, lazy loading и CLI

28.03.2026
vue-i18n-kit — локализация для Vue 3 с ICU-плюрализацией, lazy loading и CLI

vue-i18n-kit — локализация для Vue 3 с ICU-плюрализацией, lazy loading и CLI

Знаете это чувство, когда открываешь новый Vue-проект и понимаешь, что сейчас снова будешь писать одно и то же? createI18n, обёртки для lazy loading, хранение локали в localStorage, обработка fallback, плюрализация — и всё это руками, как будто прошлого проекта не было.

Я сделал так несколько раз. Потом скопировал всё в общую папку и таскал оттуда. Потом подумал — ну и зачем? Оформил в пакет.

Встречайте: vue-i18n-kit — тонкий слой над vue-i18n, который берёт всю настроечную рутину на себя. Подключил плагин, используешь composables — и больше не думаешь о том, как это всё работает внутри.

Установка

bash Copy
npm install vue-i18n-kit vue vue-i18n

vue и vue-i18n — peer dependencies, в бандл не лезут. Всё честно.

Файлы локалей и главная боль плюрализации

Начнём с того, что больше всего раздражает в стандартном подходе. Обычно плюрализацию в vue-i18n пишут так:

Copy
"items": "0 товаров | {count} товар | {count} товара | {count} товаров"

Позиционный формат. Первый элемент — для нуля, второй — для единицы, третий — для нескольких. Звучит логично, пока не приходит задача добавить арабский язык с шестью формами или не выясняется, что у разных языков совершенно разная логика раскладки по позициям. Про «21 товар» вместо «21 товаров» вспоминаешь уже после деплоя.

В vue-i18n-kit плюрализация работает через ICU MessageFormat. Формы именованные, выбираются по CLDR-правилам для активной локали автоматически:

json Copy
// locales/en.json
{
  "buttons": {
    "submit": "Submit",
    "cancel": "Cancel"
  },
  "greeting": "Hello, {name}!",
  "items": "{count, plural, one {# item} other {# items}}"
}
json Copy
// locales/ru.json
{
  "buttons": {
    "submit": "Отправить",
    "cancel": "Отмена"
  },
  "greeting": "Привет, {name}!",
  "items": "{count, plural, one {# товар} few {# товара} many {# товаров} other {# товаров}}"
}

# — это плейсхолдер числа внутри формы. other — единственная обязательная категория, используется как fallback. Остальные — one, few, many, zero, two — выбираются движком автоматически по правилам CLDR. Для русского будет три формы, для арабского — шесть, для японского — достаточно одной, потому что в японском с числами всё просто и без затей.

Никакого «посчитай позиции». Написал формы — работает правильно везде.

Подключение

Один раз в main.ts, и больше к этому файлу не возвращаешься:

ts Copy
import { createApp } from 'vue'
import { createVueI18nPlugin } from 'vue-i18n-kit'
import App from './App.vue'

const app = createApp(App)

app.use(createVueI18nPlugin({
  defaultLocale: 'en',
  fallbackLocale: 'en',
  locales: {
    en: {
      messages: () => import('./locales/en.json'),
      meta: { display: 'English', flag: '🇬🇧' },
    },
    ru: {
      messages: () => import('./locales/ru.json'),
      meta: { display: 'Русский', flag: '🇷🇺' },
    },
  },
  persistLocale: true,
}))

app.mount('#app')

Каждая локаль может быть задана тремя способами — и их можно свободно смешивать в одном конфиге:

  • Объект — сразу в бандле, загружается синхронно
  • Функция () => import(...) — lazy, загружается только при переключении
  • LocaleDefinition — объект или функция плюс произвольные метаданные через поле meta

persistLocale: true — выбор пользователя живёт в localStorage и восстанавливается при следующем открытии страницы. Кажется, мелочь, но без этого всегда получаешь вопрос «а почему язык сбросился?».

Параметры плагина

Параметр Тип По умолчанию Описание
defaultLocale string Локаль при запуске
fallbackLocale string Используется когда ключ не найден в активной локали
locales Record<string, LocaleEntry> Карта локалей
persistLocale boolean false Сохранять выбор в localStorage
storageKey string 'vue3-i18n-locale' Ключ для localStorage
vueI18nOptions object Дополнительные опции напрямую в createI18n

useT — два метода на все случаи жизни

Основной composable. Возвращает t для простых переводов и tm для плюрализации.

ts Copy
import { useT } from 'vue-i18n-kit'

const { t, tm } = useT()

t(key, vars?) — стандартный перевод по ключу. Умеет вложенные ключи через точку и именованные плейсхолдеры.

ts Copy
t('buttons.submit')                   // → 'Submit'
t('greeting', { name: 'Alice' })      // → 'Hello, Alice!'

tm(key, vars) — плюрализация. Получает значение по ключу, обрабатывает как ICU-шаблон, выбирает форму через Intl.PluralRules.

ts Copy
tm('items', { count: 1  })   // → '1 item'
tm('items', { count: 5  })   // → '5 items'
tm('items', { count: 21 })   // → '21 items'

То же самое, но уже русская локаль активна — вызов не меняется, меняется результат:

ts Copy
tm('items', { count: 1  })   // → '1 товар'
tm('items', { count: 3  })   // → '3 товара'
tm('items', { count: 5  })   // → '5 товаров'
tm('items', { count: 11 })   // → '11 товаров'  ← исключение, не «11 товар»
tm('items', { count: 21 })   // → '21 товар'    ← и снова правильно

Это и есть главная ценность ICU + CLDR: написал один раз, работает для любой локали. Движок сам знает, что 11 — это «many» в русском, а 21 — снова «one».

tm также принимает шаблон напрямую, минуя файл локали — удобно для быстрых экспериментов или динамических строк:

ts Copy
tm('{n, plural, one {# result} other {# results}}', { n: 7 })
// → '7 results'

Несколько переменных в одном шаблоне тоже работают:

ts Copy
// locale: "{user} набрал {score} {score, plural, one {балл} few {балла} many {баллов} other {баллов}}"
tm('score_line', { user: 'Алекс', score: 21 })
// → 'Алекс набрал 21 балл'

Пример компонента — вывод переводов

Типичный компонент с текстом выглядит так:

vue Copy
<script setup lang="ts">
import { useT } from 'vue-i18n-kit'

const { t, tm } = useT()

const userName = 'Alice'
const itemCount = 5
</script>

<template>
  <div class="content">
    <h1>{{ t('page.title') }}</h1>
    <p>{{ t('greeting', { name: userName }) }}</p>
    <p>{{ tm('items', { count: itemCount }) }}</p>
    <button>{{ t('buttons.submit') }}</button>
  </div>
</template>

При смене локали всё обновляется само — t и tm реактивны, никаких ручных перерисовок.

useLocale — переключение и состояние

ts Copy
import { useLocale } from 'vue-i18n-kit'

const { locale, setLocale, isLoading, localeMeta } = useLocale()
Значение Тип Описание
locale Ref<string> Код активной локали
setLocale (lang: string) => Promise<void> Переключить локаль. Загружает JSON если ещё не загружен
isLoading Ref<boolean> true пока JSON тащится по сети
localeMeta ComputedRef<TMeta | undefined> Метаданные активной локали

setLocale не молчит о проблемах. Если передать незарегистрированный код — получишь нормальное сообщение, а не загадочный TypeError где-то глубоко:

Copy
[vue-i18n-kit] Locale "de" is not registered. Available locales: en, ru

Метаданные локалей — удобный селектор без отдельного словаря

Часто для селектора локали нужны человекочитаемые названия и флаги. Обычно их хранят отдельно — в константах, в самих же файлах локалей, или прямо в компоненте. LocaleDefinition решает это опрятнее: метаданные живут рядом с локалью прямо в конфиге.

ts Copy
locales: {
  en: {
    messages: () => import('./locales/en.json'),
    meta: { display: 'English', flag: '🇬🇧', author: 'Danil Lisin' },
  },
}

meta — произвольный объект, никаких ограничений на форму. Читается обратно типизированно через generic, без приведения типов руками:

ts Copy
interface AppLocaleMeta {
  display: string
  flag: string
  author?: string
}

const { localeMeta } = useLocale<AppLocaleMeta>()
localeMeta.value?.display  // string | undefined — и TypeScript это знает

Пример компонента — селектор локали

Вот как выглядит полноценный селектор с флагами, названиями и индикатором загрузки:

vue Copy
<script setup lang="ts">
import { useLocale, useAvailableLocales } from 'vue-i18n-kit'

interface LocaleMeta {
  display: string
  flag: string
}

const { locale, setLocale, isLoading, localeMeta } = useLocale<LocaleMeta>()
const { availableLocales } = useAvailableLocales<LocaleMeta>()

async function handleChange(code: string) {
  try {
    await setLocale(code)
  } catch (err) {
    console.error('Failed to load locale:', err)
  }
}
</script>

<template>
  <div class="locale-selector">
    <!-- Что сейчас активно -->
    <span class="current">
      {{ localeMeta?.flag }} {{ localeMeta?.display ?? locale }}
    </span>

    <!-- Список всех доступных локалей -->
    <select
      :value="locale"
      :disabled="isLoading"
      @change="handleChange(($event.target as HTMLSelectElement).value)"
    >
      <option
        v-for="loc in availableLocales"
        :key="loc.code"
        :value="loc.code"
      >
        {{ loc.meta?.flag }} {{ loc.meta?.display ?? loc.code }}
      </option>
    </select>

    <span v-if="isLoading" class="loader">Loading…</span>
  </div>
</template>

useAvailableLocales возвращает все зарегистрированные локали в порядке объявления. Локали без meta тоже попадают в список — просто с meta: undefined, и ?? loc.code подхватит код как запасной вариант.

useFormat — даты, числа, валюты

Бонусом идёт форматирование. Всё через нативные Intl-API, всё автоматически в активной локали.

ts Copy
import { useFormat } from 'vue-i18n-kit'

const { formatDate, formatNumber, formatCurrency } = useFormat()
ts Copy
formatDate(new Date(), { dateStyle: 'long' })
// → '28 марта 2026 г.'  (ru)
// → 'March 28, 2026'    (en)

formatNumber(1_234_567.89)
// → '1 234 567,89'   (ru)  — пробел как разделитель тысяч, запятая как десятичная
// → '1,234,567.89'   (en)  — и наоборот

formatCurrency(1999.99, 'EUR')
// → '1 999,99 €'   (ru)
// → '€1,999.99'    (en)

Переключил локаль — все числа и даты в шаблоне пересчитались. Никаких дополнительных действий.

Как это работает под капотом

Технически интересного тут несколько вещей.

Состояние — модульный синглтон. Вместо provide/inject и глобальных объектов на window — обычная модульная переменная. Плагин при install вызывает setState, composables читают через getState. Это сознательное решение: SSR в задачи пакета не входит, зато код получается проще и предсказуемее.

Lazy loading с кешированием. При вызове setLocale('ru') смотрим — есть ли 'ru' в Set загруженных локалей? Если нет — вызываем загрузчик () => import('./locales/ru.json'), регистрируем результат в vue-i18n через setLocaleMessage, добавляем ключ в Set. При повторном вызове setLocale('ru') сетевого запроса не будет — только смена активной локали. Это важно, если пользователь туда-обратно переключает язык.

Синхронная предзагрузка. defaultLocale и fallbackLocale грузятся синхронно при install, если их messages — уже объект. Это нужно, чтобы vue-i18n с первого рендера мог корректно отдавать переводы и fallback, не ожидая промиса.

ICU-плюрализация. tm берёт сырой шаблон через vue-i18n t(), передаёт во внутренний pluralizeIcu. Там регулярками разбираются plural-конструкции {varName, plural, ...}, для каждой вызывается Intl.PluralRules.select(count) — нативный браузерный API, который знает правила для всех языков по стандарту CLDR. Категория определяет форму, # заменяется числом. Инстанс Intl.PluralRules кешируется через computed — пересоздаётся только при смене локали, не на каждый tm.

Различение типов локалей. Это, пожалуй, самое нетривиальное место. LocaleEntry — это union из трёх типов: обычный объект с сообщениями, функция-загрузчик, или LocaleDefinition. Проблема: обычный объект и LocaleDefinition выглядят похоже, особенно если в вашем словаре есть ключ messages. Логика различения намеренно консервативная: объект считается LocaleDefinition только если у него есть поле meta или messages является функцией. Строковое значение messages: "Your messages" не сломает определение — это просто обычный ключ перевода.

Vite-плагин — переводчик ничего не пропустит

В команде обычно бывает так: разработчик добавил новые ключи в английский файл, а в русский забыл. Или наоборот — удалил ключ, который ещё жив в паре других локалей. Всё это тихо живёт до тех пор, пока кто-нибудь не переключит язык.

ts Copy
// vite.config.ts
import { vueI18nCheckPlugin } from 'vue-i18n-kit/vite'

export default defineConfig({
  plugins: [
    vue(),
    vueI18nCheckPlugin({
      localesDir: 'src/locales',
      defaultLocale: 'en',
      failOnMissing: true,  // прерывает сборку если что-то пропущено
    }),
  ],
})

Плагин сравнивает ключи всех JSON-файлов с эталонной локалью и репортит расхождения — при запуске сборки и при каждом изменении файла в dev-режиме. failOnMissing: true в CI гарантирует, что незаконченный перевод просто не уедет на прод.

Вывод выглядит так:

Copy
[vue-i18n-kit] Incomplete translations detected (reference: "en"):
  Locale "ru":
    Missing keys (2):
      - buttons.cancel
      - profile.title

CLI — для тех, кто любит терминал

Три команды на все случаи работы с файлами локалей.

bash Copy
# Создать директорию и файлы-заготовки
vue-i18n-kit init --dir src/locales --locales en,ru,de

# Добавить новую локаль — скопировать структуру из существующей
vue-i18n-kit add fr --from en --empty

# Проверить полноту переводов
vue-i18n-kit check --default en --fail

add с флагом --empty создаёт файл с теми же ключами, но пустыми значениями — удобно отдать переводчику. check --fail возвращает код 1 если есть расхождения, что позволяет встроить проверку в любой CI-пайплайн без лишних телодвижений.

TypeScript — всё типизировано

Пакет написан в strict-режиме, все публичные типы реэкспортируются:

ts Copy
import type {
  I18nPluginOptions,
  LocaleEntry,
  LocaleDefinition,
  LocaleInfo,
  PluralVars,
  UseTReturn,
  UseLocaleReturn,
} from 'vue-i18n-kit'

useLocale<TMeta> и useAvailableLocales<TMeta> принимают generic — тип meta подхватывается везде без приведения типов.

Итог

Начиналось как «просто вынесу в общую папку», закончилось полноценным пакетом с 102 тестами, CLI, Vite-плагином и нормальной документацией. Иногда так бывает.

Если устали каждый раз с нуля настраивать локализацию — попробуйте. Пакет открытый, issues и PR приветствуются.

Пакет на npm: vue-i18n-kit

Читать далее

30.03.2026

color-value-tools 1.1.1: от конвертера форматов до полноценного инструментария для работы с цветом

color-value-tools вырос из простого конвертера цветовых форматов в полноценный инструментарий: CSS Color Level 4, перцептивная интерполяция, цветовые гармонии, симуляция дальтонизма, WCAG-доступность, генераторы и CLI — всё в одном пакете без зависимостей.

Метки
colortypescriptnpmwcagcss
31.03.2026

css-magic-gradient 1.2.0 — гармонии, палитры, WCAG по всей длине и canvas-экспорт

Версия 1.2.0 библиотеки css-magic-gradient: расширенные цветовые гармонии, генераторы тинтов и шейдов, переработанная доступность с проверкой по всем точкам градиента, CSS-переменные, экспорт в canvas и 9 новых хуков для Vue и React.

Метки
css-градиентыtypescriptreactvuewcagcolor-harmony