Skip to content

Plugin Options & Service

Plugin options

ts
interface I18nPluginOptions {
  defaultLocale: string
  fallbackLocale?: string
  locales: Record<string, LocaleEntry>
  persistLocale?: boolean
  storageKey?: string
  vueI18nOptions?: Record<string, unknown>
}
OptionTypeDefaultDescription
defaultLocalestringRequired. Locale loaded on startup.
fallbackLocalestringLocale used when a key is missing in the active locale. Also pre-loaded synchronously so it is available immediately.
localesRecord<string, LocaleEntry>Required. Map of locale codes to message objects, loader functions, or LocaleDefinition objects. See formats below.
persistLocalebooleanfalseSave the selected locale to localStorage and restore it on next visit.
storageKeystring'vue3-i18n-locale'Key used for localStorage when persistLocale is true.
vueI18nOptionsobjectExtra options forwarded directly to vue-i18n's createI18n.

Locale entry formats

Each locale in the locales map accepts one of three forms. They can be freely mixed within the same config.

1. Plain message object (synchronous)

ts
locales: {
  en: { buttons: { submit: 'Submit' }, greeting: 'Hello, {name}!' },
}

Messages are bundled into the app at build time and available immediately.

2. Async loader function (lazy)

ts
locales: {
  ru: () => import('./locales/ru.json'),
}

The JSON file is fetched only when setLocale('ru') is called. Until then it has zero impact on the initial bundle size.

3. LocaleDefinition — messages + custom metadata

ts
locales: {
  en: {
    messages: () => import('./locales/en.json'),
    meta: { display: 'English', flag: '🇬🇧' },
  },
  ru: {
    messages: () => import('./locales/ru.json'),
    meta: { display: 'Русский', flag: '🇷🇺', author: 'Danil Lisin' },
  },
}

meta is an arbitrary object — the shape is entirely up to the project. It is accessible through useLocale().localeMeta and useAvailableLocales().availableLocales[n].meta. All three forms can be mixed freely in the same locales map.

Plugin service

createVueI18nPlugin returns an I18nPlugin object — it satisfies Vue's Plugin interface (so app.use(plugin) works unchanged) and exposes a .service property that is usable anywhere in the application, including outside Vue component setup().

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

export const i18nPlugin = createVueI18nPlugin({
  defaultLocale: 'en',
  locales: {
    en: { messages: () => import('./locales/en.json'), meta: { display: 'English' } },
    ru: { messages: () => import('./locales/ru.json'), meta: { display: 'Русский' } },
  },
})
ts
// router/index.ts — outside setup()
import { i18nPlugin } from '@/i18n'

router.beforeEach(async (to) => {
  const lang = to.params.lang as string
  if (lang) await i18nPlugin.service.setLocale(lang)
})

service API

PropertyTypeDescription
localeRef<string>Currently active locale — the same ref instance as useLocale().locale.
isLoadingRef<boolean>true while a locale file is being fetched.
setLocale(lang: string) => Promise<void>Switch locale. Lazy-loads if needed. Throws if lang is not registered.
availableLocalesComputedRef<LocaleInfo[]>All registered locales with their metadata. Same computed instance on every access.
onLocaleChange(cb: (lang: string) => void) => () => voidSubscribe to locale switches. Returns an unsubscribe function.

onLocaleChange

Subscribe to locale switches from anywhere — useful for syncing external state that cannot be driven by Vue reactivity.

ts
// Update <html lang> on every switch
i18nPlugin.service.onLocaleChange((lang) => {
  document.documentElement.lang = lang
})

// Unsubscribe when no longer needed
const unsubscribe = i18nPlugin.service.onLocaleChange((lang) => {
  analytics.track('locale_changed', { lang })
})
unsubscribe()

service vs composables — when to use which

ContextRecommended API
Vue component setup()useLocale(), useT(), useAvailableLocales() — reactive, template-friendly
Router guards, Pinia stores, utility modulesplugin.service — no getCurrentInstance() needed
SSR entry points, server middlewareplugin.service — inject the plugin instance from your plugin file

SSR note

service stores state in a closure created when createVueI18nPlugin is called. In SSR the plugin must be created per request, not at module level:

ts
// ✅ Correct — one plugin instance per Nuxt request
export default defineNuxtPlugin((nuxtApp) => {
  const plugin = createVueI18nPlugin({ ... })
  nuxtApp.vueApp.use(plugin)
})
ts
// ❌ Wrong — shared across all SSR requests
const plugin = createVueI18nPlugin({ ... })   // module level
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(plugin)   // plugin.service.locale is shared — requests contaminate each other
})

TypeScript

All public types are re-exported for use in consumer projects:

ts
import type {
  // Plugin
  I18nPluginOptions,
  I18nPlugin, // return type of createVueI18nPlugin — Plugin & { service }
  I18nService, // { locale, isLoading, setLocale, availableLocales, onLocaleChange }

  // Locale entry types
  LocaleMessages, // Record<string, unknown>
  LocaleEntry, // LocaleMessages | LocaleLoader | LocaleDefinition
  LocaleDefinition, // { messages, meta? }
  LocaleInfo, // { code, meta } — returned by useAvailableLocales

  // Composable return shapes
  UseLocaleReturn,
  UseTReturn,
  UseAvailableLocalesReturn,
  UseFormatReturn,
  UsePluralizeReturn,

  // Pluralization
  PluralVars, // Record<string, string | number>
} from 'vue-i18n-kit'

Typing locale metadata

Define a project-wide interface for your meta shape and pass it as a generic to both composables:

ts
// types/i18n.ts
export interface AppLocaleMeta {
  display: string // human-readable locale name
  flag?: string // emoji flag, optional
  author?: string // translator credit, optional
}
ts
import type { AppLocaleMeta } from '@/types/i18n'
import { useLocale, useAvailableLocales } from 'vue-i18n-kit'

const { localeMeta } = useLocale<AppLocaleMeta>()
localeMeta.value?.display // string | undefined  ✓

const { availableLocales } = useAvailableLocales<AppLocaleMeta>()
availableLocales.value[0].meta?.flag // string | undefined  ✓

Error handling

Unknown locale

setLocale throws a descriptive error if the requested locale is not registered:

ts
try {
  await setLocale('de')
} catch (err) {
  // [vue-i18n-kit] Locale "de" is not registered. Available locales: en, ru
  console.error(err.message)
}

Failed network request

If the async loader function rejects, setLocale resets isLoading to false and re-throws the original error. isLoading.value is guaranteed to be false after the catch block.

Plugin not installed

Calling any composable before app.use(createVueI18nPlugin(...)) throws immediately:

[vue-i18n-kit] Plugin not installed. Call app.use(createVueI18nPlugin(...)) before using composables.

Locale persistence

When persistLocale: true is set, the selected locale is saved to localStorage under the configured storageKey. On the next page load the plugin reads this value and uses it as the initial locale, falling back to defaultLocale if the saved value is not a registered locale code.

localStorage calls are wrapped in try/catch so the plugin works without issues in environments where storage is restricted (private browsing, certain iframe contexts).

persistLocale vs manual localStorage

persistLocale: trueManual localStorage
SetupOne option in createVueI18nPluginRead on startup, write in onLocaleChange
Restore on page loadAutomaticYou read the key and pass the value as defaultLocale
Good whenYou just need locale to survive a page reloadYou store extra data alongside the locale, use sessionStorage, or share the key with other parts of the app

Option A — let the plugin handle it (recommended for most projects):

ts
app.use(createVueI18nPlugin({
  defaultLocale: 'en',
  locales: { ... },
  persistLocale: true,
}))

Option B — manage storage yourself:

ts
const saved = localStorage.getItem('my-locale')
const initial = ['en', 'ru'].includes(saved ?? '') ? saved! : 'en'

app.use(i18nPlugin) // persistLocale is NOT set

i18nPlugin.service.onLocaleChange((lang) => {
  localStorage.setItem('my-locale', lang)
})

Do not combine both options for the same storage key. If persistLocale: true is set and you also call localStorage.setItem manually, the plugin will overwrite your value on the next setLocale.