|
| 1 | +--- |
| 2 | +title: 'VitePress Package (`@i18n-micro/vitepress`)' |
| 3 | +description: 'Runtime i18n for VitePress themes and markdown pages.' |
| 4 | +outline: 'deep' |
| 5 | +--- |
| 6 | + |
| 7 | +# VitePress Package (`@i18n-micro/vitepress`) |
| 8 | + |
| 9 | +Runtime dictionaries, `$t` / `<I18nT>` inside markdown, and an optional `<I18nSwitcher>` — built on `@i18n-micro/vue`, synced with VitePress path locales. |
| 10 | + |
| 11 | +## Positioning |
| 12 | + |
| 13 | +VitePress i18n is three separate problems. This package only solves **runtime UI strings**. |
| 14 | + |
| 15 | +| Layer | Need | Solution | |
| 16 | +|-------|------|----------| |
| 17 | +| A. Site structure | Duplicate `.md`, URL prefixes, `locales` | **VitePress built-in** | |
| 18 | +| B. Default theme chrome | `docFooter`, search UI, outline labels | **`vitepress-i18n`**, hand-written `themeConfig`, or tools like **ai-i18n-tools** | |
| 19 | +| C. Runtime UI in MD / custom theme | `t` / plural / components / custom dropdown | **`@i18n-micro/vitepress`** | |
| 20 | + |
| 21 | +``` |
| 22 | +@i18n-micro/vitepress ≠ theme chrome translator |
| 23 | +@i18n-micro/vitepress ≠ markdown copier |
| 24 | +``` |
| 25 | + |
| 26 | +### Compose with other helpers |
| 27 | + |
| 28 | +`withI18n` is already used by [`vitepress-i18n`](https://www.npmjs.com/package/vitepress-i18n). Our helper is intentionally named **`withI18nMicro`**: |
| 29 | + |
| 30 | +```ts |
| 31 | +import { defineConfig } from 'vitepress' |
| 32 | +import { withI18n } from 'vitepress-i18n' // optional: chrome labels |
| 33 | +import { withI18nMicro } from '@i18n-micro/vitepress/config' |
| 34 | + |
| 35 | +export default defineConfig( |
| 36 | + withI18n( |
| 37 | + withI18nMicro(vitePressConfig, i18nMicroOptions), |
| 38 | + vitePressI18nOptions, |
| 39 | + ), |
| 40 | +) |
| 41 | +``` |
| 42 | + |
| 43 | +## Install |
| 44 | + |
| 45 | +::: code-group |
| 46 | + |
| 47 | +```bash [pnpm] |
| 48 | +pnpm add @i18n-micro/vitepress |
| 49 | +``` |
| 50 | + |
| 51 | +```bash [npm] |
| 52 | +npm install @i18n-micro/vitepress |
| 53 | +``` |
| 54 | + |
| 55 | +```bash [yarn] |
| 56 | +yarn add @i18n-micro/vitepress |
| 57 | +``` |
| 58 | + |
| 59 | +::: |
| 60 | + |
| 61 | +Peers: `vitepress`, `vue`. |
| 62 | + |
| 63 | +## Setup |
| 64 | + |
| 65 | +Put dictionaries under the site root, e.g.: |
| 66 | + |
| 67 | +``` |
| 68 | +locales/en.json |
| 69 | +locales/fr.json |
| 70 | +locales/pages/guide/demo/en.json # optional page-scoped |
| 71 | +locales/pages/guide/demo/fr.json |
| 72 | +``` |
| 73 | + |
| 74 | +Page files map to route names the same way as Nuxt/Astro (`guide/demo` → `guide-demo`). On `/guide/demo` / `/fr/guide/demo`, `setRoute('guide-demo')` loads them. |
| 75 | + |
| 76 | +### 1. Config — `@i18n-micro/vitepress/config` |
| 77 | + |
| 78 | +```ts |
| 79 | +// .vitepress/config.mts |
| 80 | +import { defineConfig } from 'vitepress' |
| 81 | +import { |
| 82 | + withI18nMicro, |
| 83 | + createI18nRoutingFromAdapter, |
| 84 | +} from '@i18n-micro/vitepress/config' |
| 85 | + |
| 86 | +const locales = [ |
| 87 | + { code: 'en', iso: 'en-US', displayName: 'English' }, |
| 88 | + { code: 'fr', iso: 'fr-FR', displayName: 'Français' }, |
| 89 | +] |
| 90 | + |
| 91 | +export default defineConfig( |
| 92 | + withI18nMicro( |
| 93 | + { |
| 94 | + locales: { |
| 95 | + root: { label: 'English', lang: 'en-US' }, |
| 96 | + fr: { label: 'Français', lang: 'fr-FR', link: '/fr/' }, |
| 97 | + }, |
| 98 | + themeConfig: { |
| 99 | + // Self-contained function (safe for VitePress site-data serialization) |
| 100 | + i18nRouting: createI18nRoutingFromAdapter({ |
| 101 | + defaultLocale: 'en', |
| 102 | + localeCodes: locales.map((l) => l.code), |
| 103 | + }), |
| 104 | + }, |
| 105 | + }, |
| 106 | + { |
| 107 | + locale: 'en', |
| 108 | + defaultLocale: 'en', |
| 109 | + locales, |
| 110 | + translationDir: 'locales', |
| 111 | + }, |
| 112 | + ), |
| 113 | +) |
| 114 | +``` |
| 115 | + |
| 116 | +`withI18nMicro` injects Vite virtual modules: |
| 117 | + |
| 118 | +- `virtual:i18n-micro/config` — locales / defaults / `localeKeyToCode` |
| 119 | +- `virtual:i18n-micro/messages` — per-file JSON imports from `translationDir` (+ `routeMessages`) |
| 120 | + |
| 121 | +Import the helper from **`/config`** (Node + `fs`). Do not import it from the client theme. |
| 122 | + |
| 123 | +### 2. Theme — `defineI18nTheme` (recommended) |
| 124 | + |
| 125 | +```ts |
| 126 | +// .vitepress/theme/index.ts |
| 127 | +import DefaultTheme from 'vitepress/theme' |
| 128 | +import { defineI18nTheme } from '@i18n-micro/vitepress' |
| 129 | + |
| 130 | +export default defineI18nTheme(DefaultTheme) |
| 131 | +``` |
| 132 | + |
| 133 | +Initial locale is taken from the current path before install (no default-locale flash on `/fr/…`). |
| 134 | + |
| 135 | +#### Advanced: own messages / `createVitePressI18n` |
| 136 | + |
| 137 | +```ts |
| 138 | +import { createVitePressI18n, messagesFromGlob } from '@i18n-micro/vitepress' |
| 139 | + |
| 140 | +const messages = messagesFromGlob( |
| 141 | + import.meta.glob('../../locales/*.json', { eager: true }), |
| 142 | +) |
| 143 | +const { enhanceApp } = createVitePressI18n({ /* … */, messages }) |
| 144 | +``` |
| 145 | + |
| 146 | +Node FS helpers (scripts / config tooling): |
| 147 | + |
| 148 | +```ts |
| 149 | +import { loadMessages, loadTranslationBuckets } from '@i18n-micro/vitepress/node' |
| 150 | +// or from '@i18n-micro/vitepress/config' |
| 151 | +``` |
| 152 | + |
| 153 | +## In-page translations |
| 154 | + |
| 155 | +Each VitePress markdown file is a Vue SFC. Three patterns: |
| 156 | + |
| 157 | +### Global components + `$t` (recommended in MD) |
| 158 | + |
| 159 | +`createVitePressI18n` installs `@i18n-micro/vue`, which registers `I18nT` / `I18nLink` / `I18nGroup` / `I18nSwitcher` and `$t` / `$tc` / `$ts`. |
| 160 | + |
| 161 | +```md |
| 162 | +{{ $t('cta.readMore') }} |
| 163 | + |
| 164 | +<I18nT keypath="greeting" :params="{ name: 'VitePress' }" /> |
| 165 | + |
| 166 | +<I18nLink to="/guide/demo">Demo</I18nLink> |
| 167 | +``` |
| 168 | + |
| 169 | +Component names must be PascalCase or contain a hyphen (VitePress hydration rule). |
| 170 | + |
| 171 | +### `useI18n` in `<script setup>` |
| 172 | + |
| 173 | +```md |
| 174 | +<script setup> |
| 175 | +import { useI18n } from '@i18n-micro/vitepress' |
| 176 | +const { t, tc, locale } = useI18n() |
| 177 | +</script> |
| 178 | + |
| 179 | +# {{ t('section.title') }} |
| 180 | +``` |
| 181 | + |
| 182 | +### Prose vs JSON keys |
| 183 | + |
| 184 | +| Content | How to localize | |
| 185 | +|---------|-----------------| |
| 186 | +| Long guides / articles | Duplicate `.md` per locale (VitePress) | |
| 187 | +| Shared UI phrases, CTA, plurals | JSON + `$t` / `<I18nT>` | |
| 188 | +| Page-only UI keys | `locales/pages/<path>/<locale>.json` | |
| 189 | +| Tip/warning container titles | VitePress `locales.*.markdown.container` | |
| 190 | + |
| 191 | +## Language dropdown |
| 192 | + |
| 193 | +### Built-in (default theme) |
| 194 | + |
| 195 | +When `locales` has more than one entry, VitePress shows **`VPNavBarTranslations`** (globe flyout). Wire `themeConfig.i18nRouting` (boolean or function) so corresponding pages resolve correctly. Use `createI18nRoutingFromAdapter` so the built-in menu matches `<I18nSwitcher>` paths. |
| 196 | + |
| 197 | +### `<I18nSwitcher>` (custom themes only) |
| 198 | + |
| 199 | +Use in custom layouts / page content — **not** in the default navbar next to the globe. |
| 200 | + |
| 201 | +```ts |
| 202 | +import { h } from 'vue' |
| 203 | +import DefaultTheme from 'vitepress/theme' |
| 204 | +import { defineI18nTheme, I18nSwitcher } from '@i18n-micro/vitepress' |
| 205 | + |
| 206 | +export default defineI18nTheme({ |
| 207 | + ...DefaultTheme, |
| 208 | + Layout: () => |
| 209 | + h(DefaultTheme.Layout, null, { |
| 210 | + // custom theme example — avoid with default VPNavBarTranslations |
| 211 | + 'doc-footer-before': () => h(I18nSwitcher), |
| 212 | + }), |
| 213 | +}) |
| 214 | +``` |
| 215 | + |
| 216 | +Navigation always changes the URL via the VitePress router (`router.go`). Client-only locale without a path change is not supported (against VitePress’ model). |
| 217 | + |
| 218 | +::: warning Do not mount both in the navbar |
| 219 | +With the default theme, the globe menu (`VPNavBarTranslations`) is enough. |
| 220 | +Putting `<I18nSwitcher>` in `nav-bar-content-*` duplicates the control. Prefer page/footer slots or a fully custom theme. |
| 221 | +::: |
| 222 | + |
| 223 | +| Scenario | Use | |
| 224 | +|----------|-----| |
| 225 | +| Default theme docs | Built-in language menu | |
| 226 | +| Custom theme / hero / footer | `<I18nSwitcher>` | |
| 227 | +| Both in navbar | Avoid — pick one | |
| 228 | + |
| 229 | +## SSR notes |
| 230 | + |
| 231 | +- Locale on prerender comes from the URL path (adapter), not cookies. |
| 232 | +- Theme code must not touch `window` at import time. |
| 233 | +- `$t` / components used in MD become dynamic Vue nodes (static prose stays static). |
| 234 | + |
| 235 | +## API surface |
| 236 | + |
| 237 | +| Export | Role | |
| 238 | +|--------|------| |
| 239 | +| `@i18n-micro/vitepress/config` → `withI18nMicro` | Config helper + virtual modules (Node) | |
| 240 | +| `defineI18nTheme` | Zero-boilerplate theme (`enhanceApp`) | |
| 241 | +| `createVitePressI18n` | Manual `enhanceApp` installer + path sync | |
| 242 | +| `createVitePressRouterAdapter` | Path ↔ locale (prefix except default) | |
| 243 | +| `createI18nRoutingFromAdapter` | Serializable `themeConfig.i18nRouting` | |
| 244 | +| `messagesFromGlob` | Optional glob → messages map | |
| 245 | +| `I18nT` / `I18nLink` / `I18nGroup` / `I18nSwitcher` / `useI18n` | Re-exported from `@i18n-micro/vue` | |
| 246 | +| `/node` → `loadMessages` / `loadTranslationBuckets` | Node FS loaders (scripts; prefer `/config` for site config) | |
| 247 | + |
| 248 | +## License |
| 249 | + |
| 250 | +MIT |
0 commit comments