@kevinmarrec/vue-i18n
| Install | |
|---|---|
npm install @kevinmarrec/vue-i18n |
|
| Latest Version: | 3.0.1 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 17, 2026 |
| Links: | GitHub · npm |
@kevinmarrec/vue-i18n
Description
Opinionated Internationalization (i18n) Vue plugin.
Opinions
-
Lazy loading of locales
- Supports JSON
- Supports YAML (with optional requirement: vite-plugin-yaml)
-
Locale is an explicit input, never sniffed behind your back
- You pass the locale to
install(), so server and client can agree on it detectLocale()is available when you want the browser's preferences, and is opt-in
- You pass the locale to
-
Locale tags, with or without a region subtag
en.yml,pt-BR.ymlandzh-Hans.jsonall keep their full tag as the localedetectLocale()matches the exact tag first, then its language subtag
-
URL-prefix helpers for localized routing
- The default locale serves unprefixed; every other locale gets a
/<locale>prefix/foo➡️/fr/foo/➡️/fr(/fr/withtrailingSlash: true)
- The default locale serves unprefixed; every other locale gets a
-
Fallbacking
- The default locale doubles as the fallback locale
- An empty translation is honoured, not treated as missing
- If the fallback locale also lacks the translation, the key is returned as is
-
Message Format features
-
Standard & nested keys
t('foo')resolvesfoot('foo.bar')resolvesfoo➡️bart('foo.bar.baz')resolvesfoo➡️bar➡️baz
-
Named interpolation
Hello {name}+t('key', { name: 'John' })=Hello John
-
List interpolation
Hello {0} {1}+t('key', ['John', 'Doe'])=Hello John Doe
-
Pluralization
car | cars+t('key', 0)=carscar | cars+t('key', 1)=carcar | cars+t('key', 2)=carsno apples | one apple | {count} apples+t('key', 0)=no applesno apples | one apple | {count} apples+t('key', 1)=one appleno apples | one apple | {count} apples+t('key', 2)=2 apples- Only exactly
1is singular, so a fractional count is plural:t('key', 1.5)=1.5 apples
-
-
Supports Server-Side Rendering (SSR) & Static Site Generation (SSG) without hydration mismatches
Usage
defineI18n() is called once, at module scope. It derives the available locales from the message
keys and returns everything both your router and your app need:
// src/i18n.ts
import { defineI18n } from '@kevinmarrec/vue-i18n'
export const i18n = defineI18n({
messages: import.meta.glob('./locales/*.{json,yaml,yml}'),
defaultLocale: 'en',
})
| Member | Description |
|---|---|
locales |
Locale codes derived from the message keys, e.g. ['en', 'fr'] |
defaultLocale |
Served unprefixed, and used as the fallback locale |
extractLocale(pathname) |
/fr/foo → { locale: 'fr', pathname: '/foo' } |
localizePath(pathname, locale) |
('/foo', 'fr') → /fr/foo; the default locale stays unprefixed |
detectLocale() |
Best match from navigator.languages, else defaultLocale. Browser-only |
install(locale) |
Resolves to the Vue plugin, with the locale's messages already loaded |
install() is asynchronous so that the base and fallback messages are loaded before the app
renders. That is what lets a server-rendered or prerendered document and its hydration agree.
<script setup lang="ts">
import { useI18n } from '@kevinmarrec/vue-i18n'
const { t } = useI18n()
</script>
<template>
<div>{{ t('welcome') }}</div>
</template>
Client-side rendering
With no server-rendered HTML to agree with, detectLocale() is safe as the install() input:
import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './i18n'
const app = createApp(App)
app.use(await i18n.install(i18n.detectLocale()))
app.mount('#app')
Server-side rendering & static site generation
Do not use detectLocale() here. The server cannot reach the same answer as the browser, so the
first client render would disagree with the served HTML — every translated node becomes a hydration
mismatch, and the shipped HTML is wrong for anyone who does not run JavaScript.
Derive the locale from the URL instead, on both sides, using extractLocale(). With
vite-ssg:
import { ViteSSG } from 'vite-ssg'
import App from './App.vue'
import { i18n } from './i18n'
import { routes } from './routes'
export const createApp = ViteSSG(
App,
{ routes },
async ({ app, routePath }) => {
const { locale } = i18n.extractLocale(routePath ?? location.pathname)
app.use(await i18n.install(locale))
},
)
Use localizePath() to build links and rel="alternate" tags for the other locales, and prerender
one document per locale.
Trailing slashes are canonical. A localized root path is /fr/, since prefixing / yields
/fr + /. That matches how each locale prerenders — to a directory (fr/index.html), which a
static host serves directly. Configure your framework to agree: with Vike, that is
trailingSlash: true, otherwise it normalizes /fr/ to /fr and every generated link costs a
redirect.
Switching locale
Prefer a full page navigation to a different localized URL — <a rel="external"> or
location.assign(). Each locale then gets its own document, its own <html lang> and its own
plugin instance, and there is no in-page state to keep in sync.
When a full navigation is not an option, setLocale() loads the messages then switches, so no
render observes a locale whose messages are still in flight:
const { setLocale } = useI18n()
await setLocale('fr')
Note that in a hydrated app this desynchronises the DOM from the HTML the server sent for that URL, which is why it is not the recommended path.
Related Packages
A pass-through (kudutsa in chewa) that fixes some of the oddities of vue-i18n
Checks for missing translations in your source code, and unused translations in...
Version History
| Version | Released | License |
|---|---|---|
| 3.0.1 | MIT | |
| 3.0.0 | MIT | |
| 2.0.1 | MIT | |
| 2.0.0 | MIT | |
| 1.6.1 | MIT | |
| 1.6.0 | MIT | |
| 1.5.0 | MIT | |
| 1.4.0 | MIT | |
| 1.3.0 | MIT | |
| 1.2.3 | MIT | |
| 1.2.2 | MIT | |
| 1.2.1 | MIT | |
| 1.2.0 | MIT | |
| 1.1.5 | MIT | |
| 1.1.4 | MIT | |
| 1.1.3 | MIT | |
| 1.1.2 | MIT | |
| 1.1.1 | MIT | |
| 1.1.0 | MIT | |
| 1.0.0 | MIT |
Pre-releases (2)
| Version | Released | License |
|---|---|---|
| 2.0.0-rc.2 | MIT | |
| 2.0.0-rc.1 | MIT |