VitePress Package (@i18n-micro/vitepress)
Runtime dictionaries, $t / <I18nT> inside markdown, and an optional <I18nSwitcher> — built on @i18n-micro/vue, synced with VitePress path locales.
Positioning
| Need | Solution |
|---|---|
Duplicate .md, URL prefixes | VitePress locales |
Runtime $t / components / path sync / SEO head | @i18n-micro/vitepress |
Default theme chrome labels (docFooter, search UI) | Your own themeConfig per locale |
Playground
packages/vitepress/playground — pnpm -C packages/vitepress/playground dev
Install
pnpm add @i18n-micro/vitepressnpm install @i18n-micro/vitepressyarn add @i18n-micro/vitepressPeers: vitepress, vue. Node scripts also use the workspace dependency @i18n-micro/node (re-exported through /node).
Setup
locales/en.json
locales/fr.json
locales/pages/guide/demo/en.json # optional page-scoped
locales/pages/guide/demo/fr.jsonPage files map to route names the same way as Nuxt/Astro (guide/demo → guide-demo).
1. Config — @i18n-micro/vitepress/config
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { withI18n, buildVitePressLocales } from '@i18n-micro/vitepress/config'
const locales = [
{ code: 'en', iso: 'en-US', displayName: 'English', og: 'en_US' },
{ code: 'fr', iso: 'fr-FR', displayName: 'Français', og: 'fr_FR' },
]
const defaultLocale = 'en'
export default defineConfig(
withI18n(
{
locales: buildVitePressLocales(locales, defaultLocale),
// themeConfig.i18nRouting is injected automatically
},
{
locale: defaultLocale,
defaultLocale,
locales,
translationDir: 'locales',
metaBaseUrl: 'https://example.com', // optional SEO head
},
),
)2. Theme — @i18n-micro/vitepress/theme
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import { defineI18nTheme } from '@i18n-micro/vitepress/theme'
export default defineI18nTheme(DefaultTheme)Initial locale is taken from the current path before install (no default-locale flash on /fr/…).
Advanced: manual createI18n
One factory for the client: Vue plugin + path methods + enhanceApp (no separate router helper exports).
import { createI18n, messagesFromGlob } from '@i18n-micro/vitepress'
const i18n = createI18n({
locale: 'en',
defaultLocale: 'en',
locales: [{ code: 'en' }, { code: 'fr' }],
messages: messagesFromGlob(import.meta.glob('../../locales/*.json', { eager: true })),
})
i18n.localizePath('/guide', 'fr')
// in theme enhanceApp:
i18n.enhanceApp({ app, router })Node scripts — @i18n-micro/vitepress/node
Uses @i18n-micro/node createI18n / loadTranslations (same as Astro & CLI), then attaches VitePress path methods with BaseI18n.extend:
import { createI18n } from '@i18n-micro/vitepress/node'
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
translationDir: './locales',
locales: ['en', 'fr'],
defaultLocale: 'en',
})
await i18n.loadTranslations()
i18n.t('cta.readMore')
i18n.localizePath('/guide', 'fr')Plain Node without paths: import { createI18n } from '@i18n-micro/node'.
In-page translations
Each VitePress markdown file is a Vue SFC.
Global components + $t (recommended in MD)
createI18n / defineI18nTheme install @i18n-micro/vue, which registers I18nT / I18nLink / I18nGroup / I18nSwitcher and $t / $tc / $ts.
{{ $t('cta.readMore') }}
<I18nT keypath="greeting" :params="{ name: 'VitePress' }" />
<I18nLink to="/guide/demo">Demo</I18nLink>useI18n in <script setup>
<script setup>
import { useI18n } from '@i18n-micro/vitepress'
const { t, tc, locale, localePath, switchLocale } = useI18n()
</script>
# {{ t('section.title') }}Language dropdown
When locales has more than one entry, VitePress shows VPNavBarTranslations. withI18n wires themeConfig.i18nRouting so the globe matches <I18nSwitcher> / path helpers.
Use <I18nSwitcher> only in custom layouts — not next to the default globe.
SSR notes
- Locale on prerender comes from the URL path, not cookies.
- Theme code must not touch
windowat import time. - Import
defineI18nThemefrom/themeso Node config evaluation never loadsvirtual:i18n-micro/*.
API surface
| Export | Role |
|---|---|
createI18n (.) | Client: Vue plugin + path methods + enhanceApp |
getLocaleFromPath / stripSiteBase / routeNameFromPath (.) | Manual locale sync when syncWithVitePress: false |
createI18n (/node) | Node: @i18n-micro/node + path methods |
defineI18nTheme (/theme) | Zero-boilerplate theme |
withI18n (/config) | Config + virtual modules + optional SEO |
buildVitePressLocales (/config) | Build VitePress locales from Locale[] |
buildVitePressLocaleHead (/config) | Manual SEO head (also used by metaBaseUrl) |
messagesFromGlob | Optional glob → messages map for createI18n |
useI18n / I18nT / … | Re-exported from @i18n-micro/vue |
BaseI18n.extend (@i18n-micro/core) | Attach custom methods on any i18n instance |
OpenAPI-style sites (integration locks)
Regression suite: packages/vitepress/tests/integration-openapi.test.ts (deep paths, base=/openapi_docs/, sync off + manual locale, Node generators, SEO).
| Pattern | Do this |
|---|---|
Deep /…/{spec}/{op} pages, only root UI JSON | Prefer default syncWithVitePress: true (keeps setRoute('index') when no page dict) or syncWithVitePress: false + getLocaleFromPath |
Never call setRoute(deepName) without page dictionaries | Root UI keys disappear until setRoute('index') |
Generators / getTranslation | createI18n from /node + await loadTranslations() + i18n.t(key) (set i18n.locale) |
Custom canonical in transformPageData | Pass meta: false to withI18n (avoid double tags) |
| Package SEO with site base | metaBaseUrl + VitePress base → absolute URLs include base |
| Rename from older package builds | withI18nMicro → withI18n, createVitePressI18n → createI18n, loadMessages → node createI18n |
Resources
- Playground:
packages/vitepress/playground - Repository: https://github.com/s00d/nuxt-i18n-micro
License
MIT