vue-i18n-kit 0.4.0 — редактируй переводы прямо в приложении

12.04.2026
vue-i18n-kit 0.4.0 — редактируй переводы прямо в приложении

Расскажу про 0.4.0 подробно — там много всего, и каждая часть заслуживает объяснения.

Главная тема релиза — in-context editing: редактирование переводов прямо в работающем приложении. Это большая итерация, которую я долго откладывал, потому что хотел сделать нормально, а не просто «показать ключ и поле ввода». В итоге получилось то, чем хочется пользоваться каждый день.

Но начну с визарда — он теперь знает об in-context editing и настраивает всё сам.


Мастер настройки (vue-i18n-kit init) — что изменилось

bash Copy
npx vue-i18n-kit init

Писал про init в прошлый раз — там уже был авто-скан проекта, умный мерж импортов, создание файлов локалей. В 0.4.0 добавился новый шаг: визард теперь предлагает добавить vueI18nDevPlugin в vite.config.ts:

Copy
  Detected vite.config.ts. Add vueI18nMapPlugin automatically?
   Yes /  No

  Add vueI18nDevPlugin (in-context translation editor) to vite.config.ts?
   Yes /  No

Это два отдельных вопроса: vueI18nMapPlugin нужен всегда (он отдаёт карту локалей UI-серверу), а vueI18nDevPlugin — только если хочешь карандашики в dev-режиме.

Умный мерж импортов

Раньше уже мог быть import { vueI18nMapPlugin } from 'vue-i18n-kit/vite' в конфиге. Если просто добавить новый import { vueI18nDevPlugin } — получишь дубль импорта из того же модуля, что плохо выглядит и технически лишнее.

Визард это учитывает: он найдёт существующую строку импорта и добавит vueI18nDevPlugin прямо в неё:

ts Copy
// было
import { vueI18nMapPlugin } from 'vue-i18n-kit/vite'

// стало
import { vueI18nMapPlugin, vueI18nDevPlugin } from 'vue-i18n-kit/vite'

Если vueI18nNamespacePlugin уже был — точно так же, в тот же импорт. Остальной конфиг не трогается.

Финальный вывод — без рамок

Раньше визард в конце показывал сниппет для main.ts в рамке:

Copy
╭─ Add to main.ts ─────────────────────╮
│  import { createVueI18nPlugin } ...   │
│  app.use(createVueI18nPlugin({        │
│    ...                                │
│  }))                                  │
╰───────────────────────────────────────╯

Проблема одна: когда копируешь — символы рамки копируются вместе с кодом. Убрал рамку. Теперь — просто текст с отступом:

Copy
Add to main.ts:

  import { createVueI18nPlugin } from 'vue-i18n-kit'

  app.use(createVueI18nPlugin({
    defaultLocale: 'en',
    ...
  }))

  // In-context editor: register globals exposed by vueI18nDevPlugin (dev only)
  if (import.meta.env.DEV && window.__I18N_KIT_INSPECT_COMPONENT__) {
    app.component('I18nInspect', window.__I18N_KIT_INSPECT_COMPONENT__)
  }
  if (import.meta.env.DEV && window.__I18N_KIT_INSPECT_DIRECTIVE__) {
    app.directive('i18n-inspect', window.__I18N_KIT_INSPECT_DIRECTIVE__)
  }

Блоки идут один за другим: сначала подключение локалей, потом регистрация in-context globals. Всё в одном месте — скопировал, вставил в main.ts, готово.


In-context editing — редактируй там, где видишь

Типичный рабочий процесс с переводами выглядит так: запускаешь приложение, видишь что-то не то в тексте, идёшь в редактор, ищешь ключ (а их может быть несколько сотен), правишь, переключаешься обратно в браузер, проверяешь — часто выясняется, что исправил не ту реплику или неправильно понял контекст.

Это ручная петля, которую хочется разорвать.

In-context editing решает её в корне: ты видишь неправильный текст в браузере — кликаешь карандаш рядом с ним — меняешь значение прямо здесь — страница обновляется. Без поиска по ключам, без потери контекста.


Как подключить

Два места: vite.config.ts и main.ts.

ts Copy
// vite.config.ts
import { vueI18nDevPlugin, vueI18nMapPlugin } from 'vue-i18n-kit/vite'

export default defineConfig({
  plugins: [
    vue(),
    vueI18nMapPlugin({ locales: { ... } }),
    vueI18nDevPlugin(),
  ],
})
ts Copy
// main.ts — добавить после app.use(createVueI18nPlugin(...))
if (import.meta.env.DEV && window.__I18N_KIT_INSPECT_COMPONENT__) {
  app.component('I18nInspect', window.__I18N_KIT_INSPECT_COMPONENT__)
}
if (import.meta.env.DEV && window.__I18N_KIT_INSPECT_DIRECTIVE__) {
  app.directive('i18n-inspect', window.__I18N_KIT_INSPECT_DIRECTIVE__)
}

Почему два шага, а не один? Потому что Vite-плагин работает на стороне сервера, а app.component и app.directive — на стороне клиента. Плагин не может напрямую вызвать app.component — у него нет доступа к экземпляру Vue-приложения.

Вместо этого плагин монтирует в document.body свой отдельный Vue-app (DevOverlay) и выставляет компонент и директиву через window-глобалы. Хост-приложение регистрирует их при инициализации — и они становятся частью его дерева компонентов.

В build-режиме весь этот код полностью выпадает: vueI18nDevPlugin — no-op, import.meta.env.DEV в runtime вычисляется в false и tree-shaking убирает обе ветки. В production-бандл ничего не попадает.


Как появляются карандашики

Плагин добавляет Vite transform-хук, который перехватывает каждый .vue файл в dev-режиме перед тем как @vitejs/plugin-vue его компилирует. Находит все интерполяции вида {{ t('key') }}, {{ $t('key') }}, {{ tm('key') }} с литеральными строковыми ключами — и оборачивает каждую:

html Copy
<!-- исходный шаблон -->
<p>{{ t('auth.login.title') }}</p>
<button>{{ t('buttons.submit') }}</button>

<!-- что видит @vitejs/plugin-vue -->
<p><I18nInspect i18n-key="auth.login.title">{{ t('auth.login.title') }}</I18nInspect></p>
<button><I18nInspect i18n-key="buttons.submit">{{ t('buttons.submit') }}</I18nInspect></button>

Компонент I18nInspect рендерит слот как есть, добавляя абсолютно позиционированный карандашик при hover. Никаких изменений в DOM-структуре — только обёртка с иконкой.

Важный момент про Vite: трансформация не происходит на старте сервера. .vue файлы преобразуются лениво — только когда браузер впервые их запрашивает. Открыл / — трансформировались компоненты главной страницы. Перешёл на /dashboard — трансформировались компоненты дашборда. Это стандартное поведение Vite, но оно объясняет, почему карандашики появляются после первой навигации на страницу, а не сразу при запуске.

Что трансформируется, а что нет:

  • {{ t('key') }} — да
  • {{ $t('key') }} — да
  • {{ tm('key') }} — да
  • {{ t(someVar) }} — нет, ключ динамический (для этого есть директива)
  • :placeholder="t('key')" — нет, атрибуты не трогаются (техническое ограничение)

Если автообёртка мешает — autoWrap: false в опциях плагина, и <I18nInspect> можно расставлять вручную.


Попап-редактор

Кликнул карандашик — открылся попап. Тёмная тема, всплывает по центру, фон за ним слегка затемняется.

Что внутри: ключ в заголовке, поля ввода для каждой локали с флагом и названием языка. Поля редактируемые, textarea с авторесайзом. Метка «changed» появляется на строке, которую изменил.

Сохранение: Ctrl+Enter (или Cmd+Enter на Mac) — отправляет PUT /api/locale/:code для каждой изменённой локали, остальные не трогает. Если сохранение прошло успешно — краткая зелёная отметка, попап закрывается. Vite HMR подхватывает изменение в JSON — фраза на странице за попапом обновляется сразу.

Если сервер редактора не запущен — попап покажет ошибку с сообщением, никаких молчаливых сбоев.

Клавиши: Escape закрывает попап, клик по тёмному фону тоже. Скролл страницы блокируется пока попап открыт.


Динамические ключи — директива v-i18n-inspect

Авто-обёртка через transform работает на этапе компиляции — она анализирует исходный текст .vue файла. Если ключ не литерал (t(someVar), t('prefix.' + name), t(items[i].key)) — статически его не извлечь.

Для таких случаев есть директива v-i18n-inspect. Она работает в рантайме — берёт значение binding'а как ключ:

html Copy
<!-- динамический ключ через переменную -->
<span v-i18n-inspect="myKey">{{ t(myKey) }}</span>

<!-- ключ из v-for с конкатенацией -->
<li v-for="item in menuItems" :key="item.id"
    v-i18n-inspect="'menu.' + item.id">
  {{ t('menu.' + item.id) }}
</li>

<!-- ключ зависит от состояния компонента -->
<p v-i18n-inspect="isError ? 'status.error' : 'status.ok'">
  {{ t(isError ? 'status.error' : 'status.ok') }}
</p>

Директива работает так же как компонент: при hover на элемент добавляется outline и абсолютно позиционированная кнопка-карандаш. Клик — открывает попап с нужным ключом.

Технически: mounted навешивает mouseenter/mouseleave на el и хранит состояние в WeakMap. updated проверяет binding.oldValue !== binding.value и обновляет ключ если изменился. unmounted убирает все слушатели и восстанавливает стили.

Если у элемента position: static — директива временно устанавливает position: relative чтобы кнопка-карандаш позиционировалась относительно него. При unmounted — восстанавливает как было.

В production-сборке директива никогда не регистрируется (она за import.meta.env.DEV), v-i18n-inspect на элементах молча игнорируется Vue без предупреждений.


Полный редактор — встроенный iframe

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

Решение: кнопка в шапке попапа открывает полноценный UI редактора прямо в текущей вкладке как боковую панель.

Кликаешь — iframe выезжает справа, попап закрывается. За панелью остаётся видно приложение. Редактор открывается сразу на нужном ключе — URL строится автоматически: http://localhost:4173/?key=auth.login.title&edit=en. Внутри редактор сразу переходит к этому ключу и открывает его на редактирование.

Шапка панели рендерится снаружи iframe, в DevOverlay — чтобы не зависеть от того, что происходит внутри:

  • vue-i18n-kit — лого и название
  • — закрыть панель
  • ↗ Open in new tab — открыть тот же URL в новой вкладке, панель при этом закрывается

Важный момент: пока iframe открыт, можно продолжать ховерить по репликам в приложении за панелью. Появится попап поверх. Кликнешь «Открыть в редакторе» на другом ключе — src у iframe обновится, панель перейдёт на новый ключ без перезагрузки и закрытия-открытия.

Клавиши: Escape закрывает попап (если открыт). Второй Escape — закрывает iframe. Скролл страницы заблокирован пока открыто что-либо из двух.

Ширина панели настраивается через опцию плагина:

ts Copy
vueI18nDevPlugin({ iframeWidth: '480px' })

По умолчанию — 100vw (на весь экран). Это удобно: видишь редактор в полный размер, ничего не мешает. Для тех, кто хочет видеть и приложение, и редактор одновременно — поставить 480px или 50vw.


Команда vue-i18n-kit dev

Чтобы in-context editing работал, нужно запустить два процесса параллельно: dev-сервер приложения и vue-i18n-kit ui. Это две вкладки терминала, что немного неудобно.

Новая команда запускает оба сразу:

bash Copy
vue-i18n-kit dev

Она читает scripts.dev из package.json проекта и запускает его вместе с UI-сервером. Оба лога смешиваются в одном терминале с цветовым разделением.

bash Copy
vue-i18n-kit dev --ui-port 4200
# если 4173 уже занят

Оба процесса умирают вместе при Ctrl+C — не нужно вручную убивать второй.


Namespace splitting — разбиваем большой JSON на части

Стандартный подход — один en.json на всё приложение. Пока переводов 100–200 ключей это нормально. Но когда их становится 1000+ — файл грузится целиком при каждом запуске, даже если конкретной странице нужен только раздел dashboard.*.

Namespace splitting — это разбивка по первому сегменту ключа. auth.login.title, auth.register.emailauth.json. dashboard.widgets.revenue, dashboard.headerdashboard.json. Каждый namespace — отдельный динамический чанк.

Разбить существующий JSON

bash Copy
vue-i18n-kit split --dir src/locales --out src/locales/split

en.json размером 50kb превращается в:

Copy
src/locales/split/
  en/
    auth.json
    dashboard.json
    profile.json
    settings.json
    errors.json

Команда не угадывает структуру — она берёт верхний уровень ключей как имена неймспейсов. auth.login.title → неймспейс auth.

Подключить в Vite

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

export default defineConfig({
  plugins: [
    vue(),
    vueI18nNamespacePlugin({
      locales: {
        en: 'src/locales/split/en',
        ru: 'src/locales/split/ru',
      },
    }),
  ],
})

Плагин регистрирует каждый namespace как отдельный динамический импорт. При навигации на /dashboard — подгружается только dashboard.json. auth.json загрузится когда пользователь дойдёт до страницы авторизации. Бандл на старте становится легче.

Режим в редакторе

В i18n-kit.config.json:

json Copy
{
  "namespaces": true
}

С этим флагом редактор переключается в namespace-режим: в тулбаре над таблицей появляются вкладки-таблетки по каждому неймспейсу. Кликаешь — таблица фильтруется, показывает только ключи этого раздела. Удобно работать над конкретным блоком, не листая весь список.

Собрать обратно

Если нужно временно вернуться к плоскому JSON:

bash Copy
vue-i18n-kit merge-ns

Берёт namespace-файлы из директории и объединяет обратно в один en.json.


Translation memory — переводи один раз

Когда работаешь над локализацией достаточно долго, замечаешь: одни и те же фразы встречаются снова и снова. «Сохранить» в кнопке формы. «Отмена» рядом. «Загрузка…» в спиннерах. «Что-то пошло не так» в обработчиках ошибок.

Каждый раз переводить их вручную или вспоминать как ты это переводил в прошлый раз — лишняя работа.

Translation memory решает это: редактор автоматически запоминает каждый перевод, который ты сохранил. При следующем открытии любой ячейки на редактирование — если в памяти есть похожие строки, они появятся как предложения. Нажал на чип — значение вставилось в поле.

Похожесть ищется по исходному тексту (тексту в эталонной локали), а не по ключу. Поэтому если в buttons.save и form.saveButton одинаковый исходный текст «Save» — память предложит уже готовый перевод для второго.

Memory хранится в i18n-kit.memory.json в корне проекта. Можно добавить в .gitignore если не хочется шарить между разработчиками, или наоборот — закоммитить чтобы команда пользовалась общей памятью.

Управление в Settings-панели редактора:

  • Clear memory — удалить все записи
  • Export memory — скачать JSON

Если память мешает или не нужна совсем:

json Copy
{
  "memory": { "enabled": false }
}

i18n Ally — настройка одним вопросом

i18n Ally — VS Code extension, которое показывает переводы прямо в коде рядом с вызовами t(). Удобная вещь, но требует настройки: нужно указать пути к файлам локалей, коды языков, эталонный язык.

vue-i18n-kit init теперь предлагает сгенерировать эту настройку автоматически:

Copy
  Generate .vscode/settings.json for i18n Ally VS Code extension?
   Yes /  No

При Yes записывает .vscode/settings.json:

json Copy
{
  "i18n-ally.locales": ["en", "ru"],
  "i18n-ally.pathMatcher": "src/locales/{locale}.json",
  "i18n-ally.sourceLanguage": "en",
  "i18n-ally.enabledParsers": ["json"],
  "i18n-ally.keystyle": "nested"
}

Данные берутся из того, что визард уже знает: коды локалей из Шага 1, директория из Шага 2, первая локаль как sourceLanguage.

Если .vscode/settings.json уже существует — новые настройки мержатся в существующий объект, ничего не затирается.

После этого Ally сразу видит ваши ключи и показывает переводы в редакторе без дополнительной настройки.


Итого

Всё это в версии 0.4.0:

bash Copy
npm install vue-i18n-kit@latest

NPM: https://www.npmjs.com/package/vue-i18n-kit
GitHub: https://github.com/macrulezru/vue-i18n-kit

Читать далее

11.04.2026

vue-i18n-kit 0.3.0 — TypeScript типы, устаревшие переводы, XLIFF/PO, DeepL и отчёт по покрытию

Крупное обновление инструментов локализации для Vue 3: генерация TypeScript-типов из ключей, детектор устаревших переводов, экспорт и импорт XLIFF/PO для переводчиков, поддержка DeepL, CLI-отчёт по покрытию и улучшенный дашборд.

Метки
vue3i18nlocalizationvue-i18nclideveloper-tools