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

Читать далее

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

30.03.2026

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

Метки
colortypescriptnpmwcagcss

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

31.03.2026

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

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