@kevinmarrec/vue-i18n

Opinionated Internationalization (i18n) Vue plugin.
7,283 1
Install
npm install @kevinmarrec/vue-i18n
Latest Version:3.0.1
Vue:3
License:MIT
Last Updated:Oct 17, 2026
Links: GitHub  ·  npm
Maintainer: kevinmarrec

@kevinmarrec/vue-i18n

Description

Opinionated Internationalization (i18n) Vue plugin.

Opinions

  • Lazy loading of locales

  • 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
  • Locale tags, with or without a region subtag

    • en.yml, pt-BR.yml and zh-Hans.json all keep their full tag as the locale
    • detectLocale() 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/ with trailingSlash: true)
  • 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') resolves foo
      • t('foo.bar') resolves foo ➡️ bar
      • t('foo.bar.baz') resolves foo ➡️ 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) = cars
      • car | cars + t('key', 1) = car
      • car | cars + t('key', 2) = cars
      • no apples | one apple | {count} apples + t('key', 0) = no apples
      • no apples | one apple | {count} apples + t('key', 1) = one apple
      • no apples | one apple | {count} apples + t('key', 2) = 2 apples
      • Only exactly 1 is 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

@padcom/vue-i18n

Go to github for documentation.

15,057 4
@padcom/vue-i18n-kudutsa

A pass-through (kudutsa in chewa) that fixes some of the oddities of vue-i18n

268 0
i18n-cleaner

Checks for missing translations in your source code, and unused translations in...

1,564 1
vue-i18n-composable

Composition API for vue-i18n in Vue 2.x

151,938 47
@xiaonoodles/meetfun-i18n

基于 vue-i18n 的 uni-app 国际化解决方案,支持 Vue2/Vue3,支持多语言缓存、远程语...

2,217

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
Pre-releases (2)
Version Released License
2.0.0-rc.2 MIT
2.0.0-rc.1 MIT