Plugin Options & Service
Plugin options
interface I18nPluginOptions {
defaultLocale: string
fallbackLocale?: string
locales: Record<string, LocaleEntry>
persistLocale?: boolean
storageKey?: string
vueI18nOptions?: Record<string, unknown>
}| Option | Type | Default | Description |
|---|---|---|---|
defaultLocale | string | — | Required. Locale loaded on startup. |
fallbackLocale | string | — | Locale used when a key is missing in the active locale. Also pre-loaded synchronously so it is available immediately. |
locales | Record<string, LocaleEntry> | — | Required. Map of locale codes to message objects, loader functions, or LocaleDefinition objects. See formats below. |
persistLocale | boolean | false | Save the selected locale to localStorage and restore it on next visit. |
storageKey | string | 'vue3-i18n-locale' | Key used for localStorage when persistLocale is true. |
vueI18nOptions | object | — | Extra 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)
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)
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
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().
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: 'Русский' } },
},
})// 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
| Property | Type | Description |
|---|---|---|
locale | Ref<string> | Currently active locale — the same ref instance as useLocale().locale. |
isLoading | Ref<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. |
availableLocales | ComputedRef<LocaleInfo[]> | All registered locales with their metadata. Same computed instance on every access. |
onLocaleChange | (cb: (lang: string) => void) => () => void | Subscribe 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.
// 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
| Context | Recommended API |
|---|---|
Vue component setup() | useLocale(), useT(), useAvailableLocales() — reactive, template-friendly |
| Router guards, Pinia stores, utility modules | plugin.service — no getCurrentInstance() needed |
| SSR entry points, server middleware | plugin.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:
// ✅ Correct — one plugin instance per Nuxt request
export default defineNuxtPlugin((nuxtApp) => {
const plugin = createVueI18nPlugin({ ... })
nuxtApp.vueApp.use(plugin)
})// ❌ 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:
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:
// types/i18n.ts
export interface AppLocaleMeta {
display: string // human-readable locale name
flag?: string // emoji flag, optional
author?: string // translator credit, optional
}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:
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: true | Manual localStorage | |
|---|---|---|
| Setup | One option in createVueI18nPlugin | Read on startup, write in onLocaleChange |
| Restore on page load | Automatic | You read the key and pass the value as defaultLocale |
| Good when | You just need locale to survive a page reload | You 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):
app.use(createVueI18nPlugin({
defaultLocale: 'en',
locales: { ... },
persistLocale: true,
}))Option B — manage storage yourself:
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: trueis set and you also calllocalStorage.setItemmanually, the plugin will overwrite your value on the nextsetLocale.