Skip to content

Vue Package (@i18n-micro/vue)

The @i18n-micro/vue package provides a lightweight, high-performance internationalization solution for Vue 3 applications. It shares the same core logic as Nuxt I18n Micro, offering reactive translations, route-specific support, and full TypeScript support.

Overview

@i18n-micro/vue is designed for Vue 3 applications that need internationalization without the Nuxt framework. It provides:

  • Lightweight - Uses shared core logic from @i18n-micro/core
  • Reactive - Automatic component updates when translations change
  • Route-specific translations - Support for page-level translations
  • Pluralization - Built-in plural form support
  • Formatting - Number, date, and relative time formatting
  • Type-safe - Full TypeScript support
  • Router-agnostic - Works with any router or without a router
  • DevTools Integration - Built-in Vue DevTools support for managing translations

Installation

Install the package using your preferred package manager:

bash
npm install @i18n-micro/vue
bash
yarn add @i18n-micro/vue
bash
pnpm add @i18n-micro/vue
bash
bun add @i18n-micro/vue

Peer Dependencies

The package requires Vue 3 and optionally Vue Router:

json
{
  "peerDependencies": {
    "vue": "^3.3.0",
    "vue-router": "^4.0.0"
  }
}

Quick Start

Basic Setup (Without Router)

typescript
import { createApp } from 'vue'
import { createI18n } from '@i18n-micro/vue'
import App from './App.vue'

const app = createApp(App)

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  // Automatically provided to the app - no need for manual provide calls
  locales: [
    { code: 'en', displayName: 'English', iso: 'en-US' },
    { code: 'fr', displayName: 'Français', iso: 'fr-FR' },
  ],
  defaultLocale: 'en',
  messages: {
    en: {
      greeting: 'Hello, {name}!',
      apples: 'no apples | one apple | {count} apples',
    },
    fr: {
      greeting: 'Bonjour, {name}!',
      apples: 'pas de pommes | une pomme | {count} pommes',
    },
  },
})

app.use(i18n)
app.mount('#app')

Usage in Components

vue
<template>
  <div>
    <p>{{ t('greeting', { name: 'World' }) }}</p>
    <p>{{ tc('apples', 5) }}</p>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from '@i18n-micro/vue'

const { t, tc, locale } = useI18n()

// Change locale reactively
locale.value = 'fr'
</script>

Core Concepts

Router Adapter Abstraction

The package uses a router adapter pattern to decouple i18n functionality from specific router implementations. This allows you to:

  • Use any router library (Vue Router, custom router, or no router)
  • Implement routing logic in your application, not in the i18n package
  • Keep the core package lightweight and router-agnostic

The I18nRoutingStrategy interface defines the contract between i18n and your router:

typescript
interface I18nRoutingStrategy {
  getCurrentPath: () => string
  linkComponent?: string | Component
  push: (target: { path: string }) => void
  replace: (target: { path: string }) => void
  resolvePath?: (to: string | { path?: string }, locale: string) => string | { path?: string }
  getRoute?: () => { fullPath: string; query: Record<string, unknown> }
}

Architecture

mermaid
graph TB
    App[Vue App] -->|uses| I18nPlugin[I18n Plugin]
    App -->|uses| Router[Router Adapter]
    I18nPlugin -->|provides| VueI18n[VueI18n Instance]
    Router -->|implements| I18nRoutingStrategy[I18nRoutingStrategy]
    VueI18n -->|uses| Router
    Components[Components] -->|inject| VueI18n
    Components -->|inject| I18nRoutingStrategy
    Components -->|use| useI18n[useI18n Composable]

Setup & Configuration

Basic Setup Without Router

For applications that don't need routing features:

typescript
import { createApp } from 'vue'
import { createI18n, I18nLocalesKey, I18nDefaultLocaleKey } from '@i18n-micro/vue'
import App from './App.vue'

const app = createApp(App)

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  // Automatically provided to the app
  locales: [
    { code: 'en', displayName: 'English', iso: 'en-US' },
    { code: 'fr', displayName: 'Français', iso: 'fr-FR' },
  ],
  defaultLocale: 'en',
  messages: {
    en: { welcome: 'Welcome' },
    fr: { welcome: 'Bienvenue' },
  },
})

app.use(i18n)
app.mount('#app')

Setup With Router Adapter

For applications with routing:

typescript
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createI18n } from '@i18n-micro/vue'
import { createVueRouterAdapter } from '@i18n-micro/vue'
import App from './App.vue'
import { routes, localesConfig, defaultLocale } from './app-config'

const router = createRouter({
  history: createWebHistory(),
  routes,
})

// Create router adapter
const routingStrategy = createVueRouterAdapter(router, localesConfig, defaultLocale)

// Create i18n with routing strategy
const i18n = createI18n({
  locale: defaultLocale,
  fallbackLocale: defaultLocale,
  messages: { [defaultLocale]: {} },
  routingStrategy, // Pass adapter here
  // Automatically provided to the app - no need for manual provide calls
  locales: localesConfig,
  defaultLocale,
})

const app = createApp(App)
app.use(router)
app.use(i18n)

app.mount('#app')

Providing Locales Configuration

The useI18n composable requires locales configuration to be provided via Vue's dependency injection system.

Recommended approach: Pass locales and defaultLocale directly to createI18n - they will be automatically provided:

typescript
import { createI18n } from '@i18n-micro/vue'
import type { Locale } from '@i18n-micro/types'

const locales: Locale[] = [
  { code: 'en', displayName: 'English', iso: 'en-US' },
  { code: 'fr', displayName: 'Français', iso: 'fr-FR' },
  { code: 'de', displayName: 'Deutsch', iso: 'de-DE' },
]

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  locales, // Automatically provided
  defaultLocale: 'en', // Automatically provided
  messages: { /* ... */ },
})

app.use(i18n)

Alternative approach: If you need to provide locales manually (e.g., dynamically), you can use the injection keys:

typescript
import { I18nLocalesKey, I18nDefaultLocaleKey } from '@i18n-micro/vue'

app.provide(I18nLocalesKey, locales)
app.provide(I18nDefaultLocaleKey, 'en')

Router Integration

I18nRoutingStrategy Interface

The I18nRoutingStrategy interface defines how i18n interacts with your router:

typescript
interface I18nRoutingStrategy {
  /**
   * Returns current path (without locale prefix if needed, or full path)
   * Used for determining active classes in links
   */
  getCurrentPath: () => string

  /**
   * Component to use for rendering links (e.g., RouterLink)
   */
  linkComponent?: string | Component

  /**
   * Function to navigate to another route/locale
   */
  push: (target: { path: string }) => void

  /**
   * Function to replace current route
   */
  replace: (target: { path: string }) => void

  /**
   * Generate path for specific locale
   */
  resolvePath?: (to: string | { path?: string }, locale: string) => string | { path?: string }

  /**
   * (Optional) Get current route object for SEO/Meta tags
   */
  getRoute?: () => {
    fullPath: string
    query: Record<string, unknown>
  }
}

Creating a Router Adapter

Here's a complete example of creating a Vue Router adapter:

typescript
import { RouterLink, type Router } from 'vue-router'
import type { I18nRoutingStrategy } from '@i18n-micro/vue'
import type { Locale } from '@i18n-micro/types'

export function createVueRouterAdapter(
  router: Router,
  locales: Locale[],
  defaultLocale: string,
): I18nRoutingStrategy {
  const localeCodes = locales.map(loc => loc.code)

  /**
   * Path resolution logic (add prefix or not)
   */
  const resolvePath = (to: string | { path?: string }, locale: string): string => {
    const path = typeof to === 'string' ? to : (to.path || '/')
    const pathSegments = path.split('/').filter(Boolean)

    // If path already starts with a locale, remove it
    if (pathSegments.length > 0 && localeCodes.includes(pathSegments[0])) {
      pathSegments.shift()
    }

    const cleanPath = '/' + pathSegments.join('/')

    // If default locale - return clean path
    if (locale === defaultLocale) {
      return cleanPath
    }

    // Otherwise add prefix
    return `/${locale}${cleanPath === '/' ? '' : cleanPath}`
  }

  return {
    linkComponent: RouterLink,

    getCurrentPath: () => {
      return router.currentRoute.value.path
    },

    push: (target: { path: string }) => {
      router.push(target.path).catch(() => {})
    },

    replace: (target: { path: string }) => {
      router.replace(target.path).catch(() => {})
    },

    resolvePath: (to: string | { path?: string }, locale: string) => resolvePath(to, locale),

    getRoute: () => ({
      fullPath: router.currentRoute.value.fullPath,
      query: router.currentRoute.value.query,
    }),
  }
}

Setting Routing Strategy

You can set the routing strategy in two ways:

1. During plugin creation:

typescript
const routingStrategy = createVueRouterAdapter(router, locales, defaultLocale)

const i18n = createI18n({
  locale: 'en',
  routingStrategy, // Set here
})

2. After plugin creation:

typescript
const i18n = createI18n({
  locale: 'en',
})

// Later, when router is ready
i18n.setRoutingStrategy(createVueRouterAdapter(router, locales, defaultLocale))

Custom Router Adapter

You can create adapters for any router library. Here's an example for a custom router:

typescript
import type { I18nRoutingStrategy } from '@i18n-micro/vue'

function createCustomRouterAdapter(customRouter: CustomRouter): I18nRoutingStrategy {
  return {
    getCurrentPath: () => customRouter.getCurrentPath(),
    push: (target) => customRouter.navigate(target.path),
    replace: (target) => customRouter.replace(target.path),
    resolvePath: (to, locale) => {
      const path = typeof to === 'string' ? to : (to.path || '/')
      return locale === 'en' ? path : `/${locale}${path}`
    },
  }
}

Core API

createI18n(options: CreateI18nOptions)

Creates and installs the i18n plugin for your Vue application.

Parameters:

PropertyTypeRequiredDefaultDescription
localestring-Current locale code (e.g., 'en')
fallbackLocalestringSame as localeFallback locale when translation is missing
messagesRecord<string, Translations>{}Initial translation messages
pluralPluralFuncdefaultPluralCustom pluralization function
missingWarnbooleanfalseShow console warnings for missing translations
missingHandler(locale: string, key: string, routeName: string) => void-Custom handler for missing translations
routingStrategyI18nRoutingStrategy-Router adapter for routing features

Returns: I18nPlugin

The returned object contains:

  • global: The VueI18n instance for direct access
  • install: Vue plugin install function
  • setRoutingStrategy: Method to set routing strategy after creation

Example:

typescript
import { createApp } from 'vue'
import { createI18n } from '@i18n-micro/vue'

const i18n = createI18n({
  locale: 'en',
  fallbackLocale: 'en',
  messages: {
    en: {
      welcome: 'Welcome',
      greeting: 'Hello, {name}!',
    },
    fr: {
      welcome: 'Bienvenue',
      greeting: 'Bonjour, {name}!',
    },
  },
  missingWarn: true,
  missingHandler: (locale, key, routeName) => {
    console.warn(`Missing translation: ${key} in ${locale} for route ${routeName}`)
  },
})

const app = createApp(App)
app.use(i18n)

VueI18n Class

The core i18n instance class that handles all translation logic.

Properties

  • locale: Ref<string> - Current locale (reactive)
  • fallbackLocale: Ref<string> - Fallback locale (reactive)
  • currentRoute: Ref<string> - Current route name (reactive)
  • cache: TranslationCache - Translation cache (readonly)

Methods

t(key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string): CleanTranslation

Translates a key with optional parameters and fallback value.

typescript
const i18n = createI18n({ /* ... */ })

// Basic translation
i18n.global.t('welcome') // "Welcome"

// With parameters
i18n.global.t('greeting', { name: 'John' }) // "Hello, John!"

// With default value
i18n.global.t('missing', {}, 'Default text') // "Default text"

// Route-specific translation
i18n.global.t('title', {}, null, 'home') // Uses 'home' route translations
ts(key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string): string

Same as t() but always returns a string.

tc(key: TranslationKey, count: number | Params, defaultValue?: string): string

Pluralization-aware translation.

typescript
// With count number
i18n.global.tc('apples', 0) // "no apples"
i18n.global.tc('apples', 1) // "one apple"
i18n.global.tc('apples', 5) // "5 apples"

// With params object
i18n.global.tc('items', { count: 3, type: 'books' })
tn(value: number, options?: Intl.NumberFormatOptions): string

Formats a number according to the current locale.

typescript
i18n.global.tn(1234.56) // "1,234.56" (en) or "1 234,56" (fr)
i18n.global.tn(1234.56, { style: 'currency', currency: 'USD' }) // "$1,234.56"
td(value: Date | number | string, options?: Intl.DateTimeFormatOptions): string

Formats a date according to the current locale.

typescript
i18n.global.td(new Date()) // "12/31/2023" (en) or "31/12/2023" (fr)
i18n.global.td(new Date(), { dateStyle: 'full' }) // "Sunday, December 31, 2023"
tdr(value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string

Formats a relative time (e.g., "2 hours ago").

typescript
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
i18n.global.tdr(yesterday) // "yesterday"
i18n.global.tdr(Date.now() - 3600000) // "1 hour ago"
has(key: TranslationKey, routeName?: string): boolean

Checks if a translation key exists.

typescript
i18n.global.has('welcome') // true
i18n.global.has('missing') // false
addTranslations(locale: string, translations: Translations, merge?: boolean): void

Adds or merges translations for a locale.

typescript
// Add new translations
i18n.global.addTranslations('en', {
  newKey: 'New translation',
})

// Replace existing (merge = false)
i18n.global.addTranslations('en', {
  welcome: 'New Welcome',
}, false)
addRouteTranslations(locale: string, routeName: string, translations: Translations, merge?: boolean): void

Adds route-specific translations.

typescript
i18n.global.addRouteTranslations('en', 'home', {
  title: 'Home Page',
  description: 'Welcome to our home page',
})
mergeTranslations(locale: string, routeName: string, translations: Translations): void

Merges translations into existing route translations.

mergeGlobalTranslations(locale: string, translations: Translations): void

Merges translations into global translations.

clearCache(): void

Clears the translation cache.

setRoute(routeName: string): void

Sets the current route name for route-specific translations.

getRoute(): string

Gets the current route name.

Composables

useI18n(options?: UseI18nOptions)

The useI18n composable provides access to i18n functionality in Vue components.

Options:

typescript
interface UseI18nOptions {
  locales?: Locale[]
  defaultLocale?: string
}

Returns:

typescript
{
  // Direct access to instance
  instance: VueI18n

  // Locale (reactive)
  locale: ComputedRef<string>

  // Locale helpers
  getLocales: () => Locale[]
  defaultLocale: () => string
  getLocaleName: () => string | null

  // Routing helpers
  localeRoute: (to: string | { path?: string }, localeCode?: string) => string | { path?: string }
  localePath: (to: string | { path?: string }, locale?: string) => string
  switchLocale: (newLocale: string) => void

  // Translation methods
  t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string) => CleanTranslation
  ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string) => string
  tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string
  tn: (value: number, options?: Intl.NumberFormatOptions) => string
  td: (value: Date | number | string, options?: Intl.DateTimeFormatOptions) => string
  tdr: (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions) => string
  has: (key: TranslationKey, routeName?: string) => boolean

  // Route management
  setRoute: (routeName: string) => void
  getRoute: () => string
  getLocale: () => string

  // Translation management
  addTranslations: (locale: string, translations: Translations, merge?: boolean) => void
  addRouteTranslations: (locale: string, routeName: string, translations: Translations, merge?: boolean) => void
  mergeTranslations: (locale: string, routeName: string, translations: Translations) => void
  mergeGlobalTranslations: (locale: string, translations: Translations) => void
  clearCache: () => void
}

Example:

vue
<template>
  <div>
    <p>{{ t('greeting', { name: 'World' }) }}</p>
    <p>{{ tc('apples', count) }}</p>
    <button @click="switchLocale('fr')">Switch to French</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from '@i18n-micro/vue'

const { t, tc, locale, switchLocale } = useI18n()
const count = ref(5)

// Change locale
locale.value = 'fr'

// Or use switchLocale (handles routing if adapter is provided)
switchLocale('fr')
</script>

useLocaleHead(options?: UseLocaleHeadOptions)

Generates SEO meta tags and links for locale switching.

Options:

typescript
interface UseLocaleHeadOptions {
  addDirAttribute?: boolean
  identifierAttribute?: string
  addSeoAttributes?: boolean
  baseUrl?: string | (() => string)
}

Returns:

typescript
{
  metaObject: Ref<MetaObject>
  updateMeta: (canonicalQueryWhitelist?: string[]) => void
}

Example:

vue
<template>
  <div>
    <Head>
      <Html :lang="metaObject.htmlAttrs.lang" :dir="metaObject.htmlAttrs.dir" />
      <Link
        v-for="link in metaObject.link"
        :key="link.rel"
        :rel="link.rel"
        :href="link.href"
        :hreflang="link.hreflang"
      />
      <Meta
        v-for="meta in metaObject.meta"
        :key="meta.property"
        :property="meta.property"
        :content="meta.content"
      />
    </Head>
  </div>
</template>

<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { useLocaleHead } from '@i18n-micro/vue'
import { useRoute } from 'vue-router'

const { metaObject, updateMeta } = useLocaleHead({
  baseUrl: 'https://example.com',
})

const route = useRoute()

// Update meta when route changes
watch(() => route.path, () => {
  updateMeta()
})

onMounted(() => {
  updateMeta()
})
</script>

Components

I18nT

Translation component for rendering translated text.

Props:

PropTypeRequiredDefaultDescription
keypathTranslationKey-Translation key
pluralnumber | string-Count for pluralization
tagstring'span'HTML tag to render
paramsRecord<string, string | number | boolean>{}Parameters for interpolation
defaultValuestring''Default value if translation is missing
htmlbooleanfalseRender as HTML
hideIfEmptybooleanfalseHide component if translation is empty
customPluralRulePluralFunc-Custom pluralization function
numbernumber | string-Number to format and interpolate
dateDate | string | number-Date to format and interpolate
relativeDateDate | string | number-Date for relative time formatting

Slots:

  • default - Slot with translation text: { translation: string }

Examples:

vue
<template>
  <!-- Basic translation -->
  <I18nT keypath="welcome" />

  <!-- With parameters -->
  <I18nT
    keypath="greeting"
    :params="{ name: 'Vue' }"
  />

  <!-- Pluralization -->
  <I18nT keypath="apples" :plural="0" />
  <I18nT keypath="apples" :plural="1" />
  <I18nT keypath="apples" :plural="5" />

  <!-- Number formatting -->
  <I18nT
    keypath="number"
    :number="1234.56"
  />

  <!-- Date formatting -->
  <I18nT
    keypath="date"
    :date="new Date()"
  />

  <!-- Relative date formatting -->
  <I18nT
    keypath="relativeDate"
    :relative-date="oneHourAgo"
  />

  <!-- HTML rendering -->
  <I18nT
    keypath="htmlContent"
    html
    tag="div"
  />

  <!-- Custom tag -->
  <I18nT
    keypath="title"
    tag="h1"
  />

  <!-- With slot -->
  <I18nT keypath="message">
    <template #default="{ translation }">
      <strong>{{ translation }}</strong>
    </template>
  </I18nT>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { I18nT } from '@i18n-micro/vue'

const oneHourAgo = computed(() => new Date(Date.now() - 3600000))
</script>

Localized link component that works with or without a router adapter.

Props:

PropTypeRequiredDefaultDescription
tostring | { path?: string }-Target path or route object
activeStyleCSSProperties{}Styles to apply when link is active
localeRoute(to: string | { path?: string }, locale?: string) => string | { path?: string }-Custom locale route resolver

Slots:

  • default - Link content

Examples:

vue
<template>
  <!-- Basic link -->
  <I18nLink to="/">
    Home
  </I18nLink>

  <!-- With active style -->
  <I18nLink
    to="/about"
    :active-style="{ color: 'red', fontWeight: 'bold' }"
  >
    About
  </I18nLink>

  <!-- External link (automatically detected) -->
  <I18nLink to="https://example.com">
    External Link
  </I18nLink>

  <!-- With custom locale route -->
  <I18nLink
    to="/products"
    :locale-route="(to) => localeRoute(to, locale)"
  >
    Products
  </I18nLink>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { I18nLink, useI18n } from '@i18n-micro/vue'

const { locale, localeRoute } = useI18n()
</script>

I18nSwitcher

Locale switcher component with dropdown interface.

Props:

PropTypeRequiredDefaultDescription
localesLocale[]-Locales list (uses injected if not provided)
currentLocalestring | (() => string)-Current locale (uses i18n instance if not provided)
getLocaleName() => string | null-Function to get current locale name
switchLocale(locale: string) => void-Function to switch locale
localeRoute(to: string | { path?: string }, locale?: string) => string | { path?: string }-Function to resolve locale route
customLabelsRecord<string, string>{}Custom labels for locales
customWrapperStyleCSSProperties{}Custom wrapper styles
customButtonStyleCSSProperties{}Custom button styles
customDropdownStyleCSSProperties{}Custom dropdown styles
customItemStyleCSSProperties{}Custom item styles
customLinkStyleCSSProperties{}Custom link styles
customActiveLinkStyleCSSProperties{}Custom active link styles
customDisabledLinkStyleCSSProperties{}Custom disabled link styles
customIconStyleCSSProperties{}Custom icon styles

Slots:

  • before-button - Content before the button
  • before-selected-locale - Content before selected locale in button
  • after-selected-locale - Content after selected locale in button
  • before-dropdown - Content before dropdown
  • before-dropdown-items - Content before dropdown items
  • before-item - Content before each locale item: { locale: Locale }
  • before-link-content - Content before link content: { locale: Locale }
  • after-link-content - Content after link content: { locale: Locale }
  • after-item - Content after each locale item: { locale: Locale }
  • after-dropdown-items - Content after dropdown items
  • after-dropdown - Content after dropdown

Examples:

vue
<template>
  <!-- Basic switcher -->
  <I18nSwitcher />

  <!-- With custom props -->
  <I18nSwitcher
    :locales="locales"
    :current-locale="locale"
    :get-locale-name="() => getLocaleName()"
    :switch-locale="switchLocale"
    :locale-route="localeRoute"
  />

  <!-- With custom styling -->
  <I18nSwitcher
    :custom-button-style="{ backgroundColor: '#007bff', color: 'white' }"
    :custom-dropdown-style="{ borderRadius: '8px' }"
  />

  <!-- With custom labels -->
  <I18nSwitcher
    :custom-labels="{ en: 'English', fr: 'Français', de: 'Deutsch' }"
  />

  <!-- With slots -->
  <I18nSwitcher>
    <template #before-button>
      <span>Language: </span>
    </template>
    <template #after-item="{ locale }">
      <span class="flag">{{ getFlag(locale.code) }}</span>
    </template>
  </I18nSwitcher>
</template>

<script setup lang="ts">
import { I18nSwitcher, useI18n } from '@i18n-micro/vue'

const { locale, getLocales, getLocaleName, switchLocale, localeRoute } = useI18n()
const locales = getLocales()
</script>

I18nGroup

Component for grouping translations with a common prefix.

Props:

PropTypeRequiredDefaultDescription
prefixstring-Translation key prefix
groupClassstring''CSS class for the wrapper div

Slots:

  • default - Scoped slot with: { prefix: string, t: (key: string, params?: Params) => string }

Examples:

vue
<template>
  <I18nGroup prefix="home">
    <template #default="{ t: groupT }">
      <h1>{{ groupT('title') }}</h1>
      <p>{{ groupT('description') }}</p>
      <p>{{ groupT('greeting', { name: 'User' }) }}</p>
    </template>
  </I18nGroup>

  <!-- With custom class -->
  <I18nGroup prefix="about" group-class="about-section">
    <template #default="{ t }">
      <div>{{ t('content') }}</div>
    </template>
  </I18nGroup>
</template>

<script setup lang="ts">
import { I18nGroup } from '@i18n-micro/vue'
</script>

Advanced Usage

Route-Specific Translations

You can define translations specific to routes:

typescript
// Add route-specific translations
i18n.global.addRouteTranslations('en', 'home', {
  title: 'Home Page',
  description: 'Welcome to our home page',
})

i18n.global.addRouteTranslations('en', 'about', {
  title: 'About Us',
  description: 'Learn more about us',
})

// Set current route
i18n.global.setRoute('home')

// Use route-specific translation
i18n.global.t('title') // "Home Page"
i18n.global.t('title', {}, null, 'about') // "About Us"

Dynamic Translation Loading

Load translations dynamically:

typescript
async function loadLocaleTranslations(locale: string) {
  const messages = await import(`./locales/${locale}.json`)
  i18n.global.addTranslations(locale, messages.default, false)
}

// Load on demand
await loadLocaleTranslations('fr')

// Preload in background
Promise.all([
  loadLocaleTranslations('de'),
  loadLocaleTranslations('es'),
]).catch(() => {
  // Handle errors
})

Pluralization

Pluralization is handled automatically based on the count:

typescript
// Translation key with plural forms
const messages = {
  en: {
    apples: 'no apples | one apple | {count} apples',
    items: 'no items | one item | {count} items',
  },
}

// Usage
i18n.global.tc('apples', 0) // "no apples"
i18n.global.tc('apples', 1) // "one apple"
i18n.global.tc('apples', 5) // "5 apples"

// With parameters
i18n.global.tc('items', { count: 3, type: 'books' })

Custom Plural Function

You can provide a custom pluralization function:

typescript
import { createI18n } from '@i18n-micro/vue'

const i18n = createI18n({
  locale: 'ru',
  plural: (key, count, params, locale, t) => {
    // Russian plural rules
    const forms = key.split(' | ')
    if (count % 10 === 1 && count % 100 !== 11) {
      return forms[0] || key
    }
    if (count % 10 >= 2 && count % 10 <= 4 && (count % 100 < 10 || count % 100 >= 20)) {
      return forms[1] || key
    }
    return forms[2] || key
  },
})

Number Formatting

Format numbers according to locale:

typescript
// Basic formatting
i18n.global.tn(1234.56) // "1,234.56" (en) or "1 234,56" (fr)

// Currency
i18n.global.tn(1234.56, { style: 'currency', currency: 'USD' }) // "$1,234.56"

// Percentage
i18n.global.tn(0.15, { style: 'percent' }) // "15%"

// With locale override
i18n.global.tn(1234.56, { style: 'currency', currency: 'EUR' }) // "€1,234.56"

Date Formatting

Format dates according to locale:

typescript
const date = new Date('2023-12-31')

// Basic formatting
i18n.global.td(date) // "12/31/2023" (en) or "31/12/2023" (fr)

// Full date style
i18n.global.td(date, { dateStyle: 'full' }) // "Sunday, December 31, 2023"

// Custom format
i18n.global.td(date, { year: 'numeric', month: 'long', day: 'numeric' })

Relative Time Formatting

Format relative time:

typescript
const oneHourAgo = new Date(Date.now() - 3600000)

i18n.global.tdr(oneHourAgo) // "1 hour ago"

// With options
i18n.global.tdr(oneHourAgo, { numeric: 'auto' }) // "an hour ago"

Missing Translation Handling

Handle missing translations:

typescript
const i18n = createI18n({
  locale: 'en',
  missingWarn: true, // Show console warnings
  missingHandler: (locale, key, routeName) => {
    // Custom handler
    console.error(`Missing: ${key} in ${locale} for route ${routeName}`)
    // Send to error tracking service
  },
})

SSR Support

Server-Side Setup

typescript
// entry-server.ts
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from 'vue'
import { createMemoryHistory, createRouter } from 'vue-router'
import { createI18n } from '@i18n-micro/vue'
import { createVueRouterAdapter } from '@i18n-micro/vue'
import App from './App.vue'
import { routes, localesConfig, defaultLocale } from './app-config'

export async function render(url: string) {
  // Create router with memory history for SSR
  const router = createRouter({
    history: createMemoryHistory(),
    routes,
  })

  router.push(url)
  await router.isReady()

  // Determine current locale from URL
  const route = router.currentRoute.value
  const localeParam = route.params.locale as string | undefined
  const localeCodes = localesConfig.map(locale => locale.code)
  const currentLocale = localeParam && localeCodes.includes(localeParam) 
    ? localeParam 
    : defaultLocale

  // Create router adapter
  const routingStrategy = createVueRouterAdapter(
    router,
    localesConfig,
    defaultLocale,
  )

  // Create i18n instance
  const i18n = createI18n({
    locale: currentLocale,
    fallbackLocale: defaultLocale,
    messages: { [currentLocale]: {} },
    routingStrategy,
    // Automatically provided to the app
    locales: localesConfig,
    defaultLocale,
  })

  // Load translations
  await loadTranslations(i18n.global, currentLocale)

  // Create app
  const app = createSSRApp(App)
  app.use(router)
  app.use(i18n)
  // locales and defaultLocale are automatically provided by the plugin

  // Render to string
  const html = await renderToString(app)

  // Return HTML and state
  return {
    html,
    state: {
      locale: i18n.global.locale.value,
      route: route.path,
    },
  }
}

Client-Side Hydration

typescript
// entry-client.ts
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { createI18n, I18nLocalesKey, I18nDefaultLocaleKey } from '@i18n-micro/vue'
import { createVueRouterAdapter } from '@i18n-micro/vue'
import App from './App.vue'
import { routes, localesConfig, defaultLocale } from './app-config'

const router = createRouter({
  history: createWebHistory(),
  routes,
})

async function initApp() {
  // Get initial state from SSR
  const initialState = (window as { __INITIAL_STATE__?: { locale?: string } }).__INITIAL_STATE__
  const initialLocale = typeof initialState?.locale === 'string'
    ? initialState.locale
    : defaultLocale

  // Create router adapter
  const routingStrategy = createVueRouterAdapter(
    router,
    localesConfig,
    defaultLocale,
  )

  // Create i18n instance
  const i18n = createI18n({
    locale: initialLocale,
    fallbackLocale: defaultLocale,
    messages: { [initialLocale]: {} },
    routingStrategy,
    // Automatically provided to the app
    locales: localesConfig,
    defaultLocale,
  })

  // Load translations
  await loadTranslations(i18n.global, initialLocale)

  // Preload other locales
  await preloadTranslations(i18n.global, initialLocale)

  // Create app
  const app = createApp(App)
  app.use(router)
  app.use(i18n)
  // locales and defaultLocale are automatically provided by the plugin

  await router.isReady()
  app.mount('#app')
}

initApp().catch(console.error)

State Serialization

In your server template:

html
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="app"><!--ssr-outlet--></div>
  <script>
    window.__INITIAL_STATE__ = <!--ssr-state-->;
  </script>
  <script type="module" src="/src/entry-client.ts"></script>
</body>
</html>

DevTools Integration

This package supports DevTools integration via the @i18n-micro/devtools-ui Vite plugin. See the DevTools UI Package documentation for details.

TypeScript Support

Type Exports

The package exports all necessary types:

typescript
import type {
  VueI18n,
  VueI18nOptions,
  I18nPlugin,
  I18nRoutingStrategy,
  UseI18nOptions,
  UseLocaleHeadOptions,
  Locale,
  Translations,
  TranslationKey,
  Params,
  CleanTranslation,
} from '@i18n-micro/vue'

Type Safety

All translation methods are type-safe:

typescript
// Translation keys are typed
const key: TranslationKey = 'welcome' // ✅
const invalid: TranslationKey = 'missing' // ⚠️ Type error if key doesn't exist

// Parameters are typed
i18n.t('greeting', { name: 'John' }) // ✅
i18n.t('greeting', { invalid: 'value' }) // ⚠️ Type error

Examples & Recipes

Complete SPA Example

See the playground implementation in packages/vue/playground/src/ for a complete working example.

Router Integration Example

typescript
// router-adapter.ts
import { RouterLink, type Router } from 'vue-router'
import type { I18nRoutingStrategy } from '@i18n-micro/vue'
import type { Locale } from '@i18n-micro/types'

export function createVueRouterAdapter(
  router: Router,
  locales: Locale[],
  defaultLocale: string,
): I18nRoutingStrategy {
  const localeCodes = locales.map(loc => loc.code)

  const resolvePath = (to: string | { path?: string }, locale: string): string => {
    const path = typeof to === 'string' ? to : (to.path || '/')
    const pathSegments = path.split('/').filter(Boolean)

    if (pathSegments.length > 0 && localeCodes.includes(pathSegments[0])) {
      pathSegments.shift()
    }

    const cleanPath = '/' + pathSegments.join('/')
    return locale === defaultLocale ? cleanPath : `/${locale}${cleanPath === '/' ? '' : cleanPath}`
  }

  return {
    linkComponent: RouterLink,
    getCurrentPath: () => router.currentRoute.value.path,
    push: (target) => router.push(target.path).catch(() => {}),
    replace: (target) => router.replace(target.path).catch(() => {}),
    resolvePath: (to, locale) => resolvePath(to, locale),
    getRoute: () => ({
      fullPath: router.currentRoute.value.fullPath,
      query: router.currentRoute.value.query,
    }),
  }
}

Component Usage Example

vue
<template>
  <div id="app">
    <nav>
      <I18nLink to="/">
        {{ t('nav.home') }}
      </I18nLink>
      <I18nLink to="/about">
        {{ t('nav.about') }}
      </I18nLink>
    </nav>

    <div class="locale-switcher">
      <I18nSwitcher
        :locales="getLocales()"
        :current-locale="locale"
        :get-locale-name="() => getLocaleName()"
        :switch-locale="switchLocale"
        :locale-route="localeRoute"
      />
    </div>

    <main>
      <router-view />
    </main>
  </div>
</template>

<script setup lang="ts">
import { watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import { I18nLink, I18nSwitcher, useI18n } from '@i18n-micro/vue'
import { defaultLocale } from './app-config'

const route = useRoute()
const { t, getLocales, locale, getLocaleName, localeRoute: baseLocaleRoute, switchLocale: baseSwitchLocale } = useI18n()

const localeRoute = computed(() => {
  return (to: string | { path?: string }) => {
    return baseLocaleRoute(to, locale.value)
  }
})

const switchLocale = (newLocale: string) => {
  baseSwitchLocale(newLocale)
}

// Sync locale from URL
watch(
  () => route.params.locale,
  (newLocale) => {
    const targetLocale = (typeof newLocale === 'string' ? newLocale : defaultLocale) || defaultLocale
    if (locale.value !== targetLocale) {
      locale.value = targetLocale
    }
  },
  { immediate: true },
)
</script>

Resources

License

MIT

Released under the MIT License.