# vue-i18n-kit — AI Reference Vue 3 i18n toolkit built on top of `vue-i18n`: a lazy-loading plugin (monolithic or per-namespace locale files), 6 composables (`t`/`tm` translation, locale switching, `Intl`-based formatting, ICU pluralization, namespace loading), 5 Vite plugins (key-completeness check, locale-map dump, bundle-inline, namespace virtual module, an in-context dev-overlay editor), a types-only config subpath, and a 15-subcommand CLI (`vue-i18n-kit`) plus a standalone browser locale editor (`vue-i18n-kit ui`). **Unscoped package name: `vue-i18n-kit`**, not `@macrulez/vue-i18n-kit` — confirmed in `package.json`, README, and every generated artifact (`declare module 'vue-i18n-kit'`, virtual module specifiers). Version 0.4.11. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs). For human-readable narrative docs, see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/vue-i18n-kit/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/vue-i18n-kit/guide/overview - GitHub: https://github.com/macrulezru/vue-i18n-kit - npm: https://www.npmjs.com/package/vue-i18n-kit Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map | Import path | Contains | |---|---| | `vue-i18n-kit` | `createVueI18nPlugin`, 6 composables, all runtime types. Requires `vue` + `vue-i18n` (real peers, not optional). | | `vue-i18n-kit/vite` | 5 Vite plugins + 2 `@internal`-but-exported helpers, Node-side build tooling. Only type-only `vite` import (`ResolvedConfig`) — safe without `vite` installed at runtime, though nothing but a Vite build ever imports this subpath in practice. | | `vue-i18n-kit/config` | **Types only, zero runtime** — re-exports exactly `I18nKitRules` and `I18nKitIgnore` from `src/config/schema.ts`. See section 9 for what's conspicuously *not* here. | | `vue-i18n-kit` (bin) | CLI, 15 subcommands + `ui` (browser locale editor) + `--version`. Section 10. | `peerDependencies`: `vue ^3.3.0`, `vue-i18n ^11.0.0` (both required, neither optional), `vite >=5.0.0` (optional, only for `/vite`). Single runtime dependency: `@clack/prompts` (CLI prompt UI, Node-side only, never reaches a browser bundle). --- ## 2. Core plugin (`plugin.ts`, `types/index.ts`) ```ts function createVueI18nPlugin = Record>( options: I18nPluginOptions ): I18nPlugin // I18nPlugin = Vue Plugin & { readonly service: I18nService } interface I18nPluginOptions { defaultLocale: string fallbackLocale?: string locales: Record persistLocale?: boolean // save/restore active locale via localStorage storageKey?: string // default 'vue3-i18n-locale' vueI18nOptions?: Record // spread into vue-i18n's own createI18n() options, can override anything above except legacy/locale/messages } type LocaleEntry = LocaleMessages | LocaleLoader | LocaleDefinition type LocaleMessages = Record type LocaleLoader = () => Promise interface LocaleDefinition> { messages?: LocaleMessages | LocaleLoader // optional when namespaces is provided namespaces?: Record eagerNamespaces?: string[] // undefined → load all namespaces eagerly (default); [] → fully lazy; string[] → only these load on setLocale meta?: TMeta } ``` `createI18nInstance()` (`createI18n.ts`) wraps vue-i18n's own `createI18n` with `legacy: false` (Composition API mode only — no Options-API `$t` global injection from vue-i18n itself; this package's own composables are the intended API surface). `vueI18nOptions` is spread last, so a consumer CAN override `fallbackLocale`/anything vue-i18n accepts by passing it there — but `legacy`, `locale`, and `messages` are always set by this package first and are NOT in the spread, so those three specifically cannot be overridden via `vueI18nOptions`. ### `I18nService` — the `.service` property, usable outside `setup()` ```ts interface I18nService { readonly locale: Ref // same Ref instance as useLocale().locale readonly isLoading: Ref setLocale(lang: string): Promise readonly availableLocales: ComputedRef[]> onLocaleChange(callback: (lang: string) => void): () => void // returns unsubscribe loadNamespace(ns: string, locale?: string): Promise // locale defaults to current active isNamespaceLoaded(ns: string, locale?: string): boolean } ``` **Every property/method on `.service` throws `'[vue-i18n-kit] Plugin is not installed yet. Call app.use(plugin) before using service.'` if accessed before `app.use(pluginInstance)`** — `createVueI18nPlugin()` itself returns immediately with a lazily-bound service object; nothing is wired until `install()` runs. `isNamespaceLoaded()` is the one exception — returns `false` instead of throwing when not yet installed. ### `install(app)` bootstrap sequence — exact order 1. Resolve `storageKey` (default `'vue3-i18n-locale'`). 2. If `persistLocale`, read `localStorage[storageKey]` via `loadPersistedLocale()` — **wrapped in try/catch, returns `null` on any failure** (private browsing, SSR, storage disabled) — never throws. 3. `requestedLocale` = the persisted locale IF it's both present AND still a registered key in `options.locales` — otherwise `options.defaultLocale`. A persisted locale for a since-removed locale silently falls back to default, no warning. 4. If the requested locale's `messages` resolves to a **plain object** (not a loader function), it's registered into vue-i18n's `initialMessages` synchronously, before `createI18n()` runs — avoids an initial-render flash of untranslated keys. If it's a loader function or namespace-only, this step is skipped and step 7 handles it asynchronously. 5. Same synchronous pre-load attempt for `fallbackLocale`, if set and different from the requested locale — lets vue-i18n's own fallback mechanism work immediately without an extra async round-trip once step 7 finishes loading the real locale. 6. `createI18nInstance()` builds the actual vue-i18n instance with whatever synchronous messages steps 4-5 collected. 7. Per-app state (`I18nKitState`) is built and `app.provide()`'d under `I18N_KIT_KEY` (a `Symbol`) — **not a module-level singleton**, so multiple `app` instances (SSR request-per-render, multiple apps on one page) never share state. 8. `app.use(i18n)` registers vue-i18n itself. 9. **Async bootstrap**: if step 4 was skipped (loader function OR namespace-only locale with no plain-object messages), `setLocale()` runs asynchronously post-install. Failures are swallowed: `.catch(() => {})` — `isLoading` is cleared either way, but nothing surfaces a load failure to whoever called `app.use()`. Check `service.isLoading`/wrap your own error boundary around the locale loader itself if you need to detect a failed initial load. ### `setLocale(state, lang)` (internal engine — call via `service.setLocale()`/`useLocale().setLocale()`, not directly) - **Throws** (not silent) if `lang` isn't a key in `options.locales`: `'[vue-i18n-kit] Locale "X" is not registered. Available locales: a, b, c'`. - Loads monolithic `messages` only if present AND not already in `loadedLocales` (a `Set` on the per-app state) — repeated `setLocale()` calls for an already-loaded locale skip the network/ loader call entirely and just flip `i18n.global.locale.value`. - Namespace loading: `eager = extractEagerNamespaces(entry)`; `toLoad = eager === undefined ? Object.keys(namespaces) : eager` — all namespace loads run via `Promise.all` (parallel, not sequential). - Sets `i18n.global.locale.value = lang` only **after** all loads (messages + eager namespaces) resolve — no flash of the old locale's content mixed with the new locale's partially-loaded state. - Persists to `localStorage` (if `persistLocale`) and fires every `localeChangeCallbacks` subscriber **after** the locale value is set, in that order. - `isLoading` is set `true` at the very start and cleared in a `finally` block — always cleared even if a namespace loader throws (the `Promise.all` rejection propagates up through `setLocale()`'s own promise, so a caller doing `await setLocale(...)` DOES see the rejection — only the async-bootstrap path in `install()`, step 9 above, swallows it). ### `loadOneNamespace(state, lang, ns)` — namespace merge mechanics - Idempotent via `state.loadedNamespaces: Map>` — a second call for an already-loaded `(lang, ns)` pair is a no-op. - Silently no-ops (returns without loading anything) if the locale isn't registered, or the namespace isn't defined for that locale — **no error, no warning** in either case. - Merge is `{ ...existing, ...nsMessages }` — a **shallow** merge against whatever `i18n.global.getLocaleMessage(lang)` currently holds. Two namespaces both defining the same top-level key (e.g. both ship a `common: {...}` object) will have the later-loaded one's `common` **fully overwrite** the earlier one's, not deep-merge. Nothing detects or warns about this collision — namespace files are expected to use disjoint top-level keys by convention, not enforcement. --- ## 3. Composables (`src/composables/*.ts`) ```ts useLocale>(): { locale: Ref; setLocale(lang): Promise; isLoading: Ref; localeMeta: ComputedRef } useT(): { t: ComposerTranslation /* vue-i18n's own t() */; tm(keyOrTemplate: string, vars: PluralVars): string } useAvailableLocales(): { availableLocales: ComputedRef<{code: string; meta: TMeta|undefined}[]> } useFormat(): { formatDate(value, options?: Intl.DateTimeFormatOptions): string; formatNumber(value, options?): string; formatCurrency(value, currency, options?): string } usePluralize(): { pluralizeIcu(vars: PluralVars, template: string): string; pluralCategory(count: number): Intl.LDMLPluralRule } useNamespace(ns: string | string[]): { isLoading: Ref; isLoaded: Ref } ``` - All composables except `useFormat`/`usePluralize` call `useI18nKitState()` internally, which `inject()`s the per-app state and **throws** `'[vue-i18n-kit] Plugin not installed...'` if called outside a component tree where `createVueI18nPlugin` was `app.use()`'d. `useFormat`/`usePluralize` only need vue-i18n's own `useI18n()` (for the reactive `locale` ref), not this package's state — they work as long as vue-i18n itself is installed, even (in principle) without `createVueI18nPlugin`. - `useT().t` is vue-i18n's own `ComposerTranslation` passed through unchanged — full `t()` feature set (named/list interpolation, plural via vue-i18n's own legacy pipe syntax, linked messages) still works; `tm()` is this package's own addition, a completely separate ICU engine (section 4) that bypasses vue-i18n's message compiler entirely. - `useT().tm()`'s key lookup reads directly from `i18n.global.getLocaleMessage(locale)` via a manual dot-path walker (`getRawMessage`, `useT.ts:51-59`) — **not** vue-i18n's own resolver — specifically because vue-i18n v11's compiler rejects ICU `{var, plural, ...}` syntax as invalid message syntax. Falls back to `fallbackLocale`'s messages if the key isn't found in the active locale; if not found there either, **the `keyOrTemplate` argument itself is used as a literal ICU template** — a typo'd key with no matching fallback silently renders as if you'd passed a raw template string (no console warning, unlike vue-i18n's own `t()`, which does warn on missing keys by default). - `useNamespace(ns)` re-loads on every locale change via a `watch(..., { immediate: true })` on `state.i18n.global.locale.value`, AND registers `onServerPrefetch()` for SSR. `isLoaded` is reset to `false` at the start of every reload (not just the first), so a component relying on `isLoaded` to gate rendering will briefly flip back to the loading state on every locale switch, even for a namespace it's requesting again for a *different* locale that may load near-instantly (e.g. from a warm cache) — worth debouncing/ transitioning around in the UI if that flicker matters. - `useAvailableLocales()`/`useLocale().localeMeta` both read `entry.meta` via `extractMeta()` — `undefined` for any locale registered as a plain messages object or bare loader function (no `LocaleDefinition` wrapper), not an error. --- ## 4. ICU pluralization engine (`usePluralize.ts`) — regex-based, not a real parser ``` ICU_PLURAL_RE = /\{(\w+),\s*plural,\s*((?:\w+\s*\{[^{}]*\}\s*)+)\}/g ICU_FORM_RE = /(\w+)\s*\{([^{}]*)\}/g ``` Two-pass, string-level, NOT a recursive-descent ICU MessageFormat parser: 1. **Plural pass**: matches `{varName, plural, category {text} ...}`. `count = Number(vars[varName] ?? 0)` — **if `varName` isn't a key in the `vars` object passed to `tm()`/`pluralizeIcu()` (typo, refactor drift), this silently defaults to `0`** and picks whichever plural category `Intl.PluralRules` maps `0` to for the active locale — no warning, no error, just a plausible-looking but wrong rendered string. Category selection: `Intl.PluralRules(locale.value).select(count)`, cached via `computed()` (new instance only when `locale` itself changes, not per call). Form-body fallback: `formMap[category] ?? formMap['other'] ?? ''` — a template missing BOTH the resolved category and `other` silently renders as an **empty string** for that whole plural construct (not the raw template, not an error). `#` inside a matched form is replaced with `String(count)` via `replaceAll`. 2. **Plain-interpolation pass** (runs on the *entire* result of pass 1, including text outside any plural construct): `{varName}` → `vars[varName]` if present, else **left untouched as literal `{varName}` text** in the output — the one part of this whole engine that fails visibly rather than silently. 3. **Nesting limitation**: `ICU_FORM_RE`'s `[^{}]*` means a plural form's body cannot itself contain `{...}` — no nested plurals, and critically **no plain `{varName}` interpolation INSIDE a plural form's text** either, since any `{` inside the form breaks `ICU_PLURAL_RE`'s own outer match first. Put `{varName}` interpolations outside the `{count, plural, ...}` block (as every example in the README and source JSDoc actually does — `'{points} {points, plural, one {рубль} ...}'`, interpolation variable outside, count variable duplicated as the plural selector) rather than inside a category's `{text}`. 4. **Malformed/unmatched ICU is not an error** — if the outer `ICU_PLURAL_RE` simply doesn't match (mismatched braces, a nested `{}`, wrong keyword), that portion of the string passes through pass 1 completely unprocessed, and pass 2 will then treat any `{word}`-shaped remnant as a plain interpolation attempt. CLDR plural categories actually exercised by `Intl.PluralRules` per-locale (documented in the JSDoc, not hardcoded here — this package defers entirely to the platform's `Intl` implementation): English (one, other), Russian/Polish (one, few, many, other), Arabic (zero, one, two, few, many, other), Japanese (other only), Turkish (one, other). --- ## 5. `useFormat()` — thin `Intl` wrapper, no caching `formatDate`/`formatNumber`/`formatCurrency` each construct a fresh `Intl.DateTimeFormat`/`Intl.NumberFormat` **on every call** (not memoized per options+locale combination) — fine for occasional calls, but if used inside a hot render path over a large list, consider hoisting your own cached formatter instead of calling these in a tight loop. `formatDate` accepts `Date | number | string`, wraps non-`Date` values in `new Date(value)` before formatting — no validation that a string input actually parses to a valid date (an invalid string silently produces an `Invalid Date` formatted output, same as native `Date` behavior, not a thrown error). --- ## 6. SSR - No module-level mutable state anywhere in `plugin.ts`/`state.ts` — everything lives on the per-`app` state object via `provide`/`inject` (`I18N_KIT_KEY`, a `Symbol`), confirmed safe for concurrent request-scoped SSR rendering. - `useNamespace()` is the only composable with an explicit SSR hook (`onServerPrefetch`) — a component using `useNamespace()` during SSR will have its namespace(s) loaded before the server render completes. - **`persistLocale` + SSR hydration caveat (undocumented in source comments)**: `loadPersistedLocale()` always returns `null` on the server (no `localStorage` global), so SSR always renders using `defaultLocale`. On client hydration, if `persistLocale: true` and a different locale was actually stored, `install()`'s async bootstrap (section 2, step 9) re-runs `setLocale()` to the *real* persisted locale — but this happens asynchronously, **after** mount, not before hydration. A returning visitor with a non-default persisted locale will see a visible flash from `defaultLocale` to their real locale, and depending on what text differs, potentially a Vue hydration mismatch warning in the console. Nothing in this package detects or warns about this scenario — if you use `persistLocale` with SSR, either read the persisted locale server-side yourself (e.g. from a cookie mirrored alongside localStorage) and pass it as `defaultLocale`, or accept the flash. - The suggested SSR-safe pattern per the `vueI18nInlinePlugin` doc comment (section 7) is to bake locale JSON statically into the bundle via that plugin rather than relying on lazy loaders for an SSR app. --- ## 7. Vite plugins (`vue-i18n-kit/vite`) — Node-side build tooling, `enforce: 'pre'` on all 5 ```ts vueI18nCheckPlugin(options?: { localesDir?: string // default 'src/locales' defaultLocale?: string // default: first locale file found alphabetically failOnMissing?: boolean // default false — true fails the BUILD (this.error), false only warns rules?: I18nKitRules // currently unused by this plugin's own runCheck() — accepted in the type but not read anywhere in vueI18nCheckPlugin's implementation (see gotcha) }): VitePlugin vueI18nMapPlugin(options: { locales: Record }> output?: string // default 'i18n-tools/locales.config.json' }): VitePlugin // Active ONLY when Vite runs with `--mode i18n-dump`. Writes locales.config.json // + locales.entries.json (via scanner.ts's buildEntriesMap), then process.exit(0) // — this plugin's configResolved() terminates the Vite process entirely in that mode. vueI18nInlinePlugin(options: { locales: Record }> }): VitePlugin // Virtual module 'virtual:vue-i18n-kit/locales' — reads each locale JSON file at // BUILD time (configResolved's root + load()), embeds as a static `export default {...}`. // A missing/malformed file logs a console.warn and substitutes {} for that locale // rather than failing the build. vueI18nNamespacePlugin(options?: { dir?: string // default 'src/locales/split' locales?: Record }): VitePlugin // Virtual module 'virtual:vue-i18n-namespaces' exporting `locales: Record` // built by scanning `dir`'s subdirectories (one per locale code) for *.json files // (one per namespace) — generates `{ code: { namespaces: { ns: () => import('...') } } }` // with real dynamic imports so Vite code-splits each namespace file individually. // HMR: any .json change under `dir` triggers a full virtual-module reload. vueI18nDevPlugin(options?: { uiUrl?: string // default process.env.I18N_KIT_UI_URL ?? 'http://localhost:4173' autoWrap?: boolean // default true — wrap {{ t(...) }}/{{ tm(...) }}/{{ $t(...) }} template interpolations with wrapFunctions?: string[] // default ['t', 'tm', '$t'] iframeWidth?: string // default '100vw' (NOTE: JSDoc comment says '480px', actual default in code is '100vw' — see gotcha) }): VitePlugin // Active ONLY in `vite serve` (dev). Complete no-op during `vite build` — verified // via `isServe` guard checked in every hook (resolveId/load/transform/transformIndexHtml). ``` **`wrapTranslationCalls(source, fnAliases)` and `injectInspectRegistration(code)`** are exported from `/vite` alongside the 5 plugins, explicitly marked `@internal exported for unit-testing` in their JSDoc — usable, but not part of the intended public surface; don't build application logic around them. `wrapTranslationCalls()` only rewrites `{{ }}` template interpolations (never element attributes like `:title="t('x')"`), only literal string-key calls (a dynamic key expression, or any key containing `${`, is left unwrapped), and won't double-wrap a call immediately followed by ``. --- ## 8. In-context dev editor (`vue-i18n-kit dev` / `vueI18nDevPlugin` + `ui-server/server.ts`) `vue-i18n-kit dev` (`cli/commands/dev.ts`) spawns two processes in parallel: your app's own dev server (`--app-cmd`, default likely `npm run dev` — read that file directly if you need the exact default) and `vue-i18n-kit ui` (the locale editor, `ui-server/server.ts`, a plain `node:http` server — no Express/Fastify dependency), wiring `I18N_KIT_UI_URL` into the app process's env so `vueI18nDevPlugin` picks it up automatically without manual config. `ui-server/server.ts` exposes a REST + Server-Sent-Events API for the browser editor SPA (`ui-app/`, a separate Vue 3 build, output served as static assets from `dist/ui-server/public`): locale CRUD, a DeepL/ LibreTranslate machine-translation proxy, a translation-memory store, XLIFF/PO/CSV export, and 403 enforcement of `config.locked` key patterns (server-side, not just a UI-level warning — a request trying to write a locked key is rejected regardless of what the client sent). --- ## 9. `vue-i18n-kit/config` subpath — types only, narrower than the config schema itself ```ts // entire subpath content: export type { I18nKitRules, I18nKitIgnore } from './schema.js' ``` `I18nKitRules` (validation toggles: `interpolationPatterns`, `lengthWarningFactor`, `warnOnHtmlTags`, `warnOnIcuErrors`, `warnOnDuplicateValues`, `minValueLength`) and `I18nKitIgnore` (`prune`/`duplicates`/`unused`/`scanExclude` glob-pattern exclusion lists) ARE exported — these match `vueI18nCheckPlugin`'s own `I18nCheckPluginOptions.rules` type exactly (duplicated locally inside `vite-plugin/index.ts` rather than imported from `config/schema.ts` — two structurally-identical-but-separately-declared interfaces, not a shared reference; they'll only diverge if one file is edited without the other). **Not exported from this subpath**: `I18nKitConfig` (the full `i18n-kit.config.json` shape — `version`, `extends`, `localesDir`, `toolkitDir`, `locales: LocaleConfig[]`, `rules`, `ignore`, `locked`, `integrations`, `staleTracking`, `translation`, `namespaces`, `memory`, `scanner`), `LocaleConfig` (one registered locale's config-file entry: `code`/`path`/`meta`/`createdAt`/`updatedAt`), or `LocaleMeta` (the config-file's own richer per-locale metadata shape — `display`, `flag`, `direction`, `author`, `version`, plus an open index signature — a different, config-file-specific type from this package's runtime `LocaleDefinition.meta: TMeta`, which is fully consumer-defined). All three live in `src/config/schema.ts` and are used pervasively by every CLI command that reads/writes `i18n-kit.config.json`, but that file is `src/config/index.ts` (internal I/O helpers), which is not a build entry — so nothing outside this package's own source can currently `import type { I18nKitConfig } from 'vue-i18n-kit/config'` to type-check a hand-written or programmatically-generated `i18n-kit.config.json`. Not advertised anywhere as available (README/ CHANGELOG never mention these three type names), so this reads as a gap rather than a broken promise — see the gotcha list. --- ## 10. CLI (`vue-i18n-kit`, bin → `dist/cli/index.js`) Dispatch: plain `switch` on `process.argv[2]`; custom `--flag value` / `--flag` (boolean) parser, no external arg-parsing library. | Command | Key flags | Purpose | |---|---|---| | `init` | (interactive) | `@clack/prompts` wizard — the only real init path | | `add ` | `--dir --from --empty` | scaffold a new locale JSON from an existing one | | `check` | `--dir --default --fail` | key-completeness diff across locale files, exit code reflects result | | `merge ` | `--dir --locale --overwrite --dry --no-sort` | deep-merge a shared JSON into locale files, respects `locked` | | `prune` | `--dir --entries --dry --yes --ignore` | remove unused keys (via source-scanning), respects `ignore`/`locked` | | `types` | `--out --locale --dir --watch` | generate a `TranslationKey` union `.d.ts` from one locale file | | `stats` | `--format console\|json\|html --out --dir` | coverage report, phantom/unused key detection | | `split` | `--dir --out --dry` | flat locale JSON → per-namespace files | | `merge-ns` | `--dir --out --dry --no-sort` | inverse of `split` | | `stale` | `--dir --locale` | sha1-hash "reference value changed" detector (`i18n-kit.notes.json`) | | `export` | `--format xliff\|po --locale(required) --out --dir --ref` | XLIFF 1.2 / Gettext PO exporter | | `import ` | `--dir --dry` | XLIFF/PO parser → locale JSON writer | | `auto-config` | (none) | scans source for `createVueI18nPlugin(...)`, patches target `vite.config.ts`/`nuxt.config.ts` to insert the check/dev plugins (regex/bracket-matching text surgery on the file, not an AST transform) | | `dev` | `--ui-port --app-cmd` | app dev server + `ui` server together, wires `I18N_KIT_UI_URL` | | `ui` | `--port` | standalone browser locale editor (section 8) | | `--version`/`-v` | | reads `VERSION` from `package.json` at runtime via `readFileSync` — NOT hardcoded (was, historically — see section 11) | | (no args / unknown) | | full help text | **No Nuxt module** — no `defineNuxtModule`/`addPlugin`/`addImports`/ `addComponent` anywhere in this package. "Nuxt support" is entirely `auto-config`/`init`'s text-patching of a target `nuxt.config.ts`'s `vite.plugins` array — same mechanism as for a plain `vite.config.ts`, no Nuxt-specific auto-registration magic. If you're checking this package against the "stale Nuxt addImports list" bug class seen in sibling packages: not applicable, there is no such list here to go stale. --- ## 11. Fixed-bug history (verify against your installed version) Per `CHANGELOG.md`, both fixed in `0.4.8` (2026-09-03): - **CLI `--version`/`-v` was hardcoded** (`'0.3.0'`, regardless of the actually-installed package version) — now reads the real version from `package.json` at runtime (section 10). Confirmed live in current source. - **`vue-i18n-kit/config` subpath was documented since `0.3.0` but never actually built or added to `package.json`'s `exports` map** — now a real, types-only subpath (section 9), confirmed present in both `exports` and `dist/config/index.d.ts`. Note this is a different, narrower gap than the still-open one described in section 9 (`I18nKitConfig`/`LocaleConfig`/`LocaleMeta` were never promised in the first place, so their absence isn't a regression of this fix). `package.json` is currently at `0.4.11`, but `CHANGELOG.md`'s newest entry is `0.4.8` — the three version bumps between them (`0.4.9`-`0.4.11`) were README-only changes (confirmed via `git log`: a doc slim-down, a "when you'd reach for this" section, live-demo examples), no source changes, so nothing functional is undocumented — but the changelog's own version-history record is stale if you're relying on it to enumerate every release. --- ## 12. Consolidated gotcha list 1. **`tm()` silently renders the key itself as a literal ICU template when the key isn't found** in either the active or fallback locale — no console warning, unlike vue-i18n's own `t()` (section 3). 2. **`pluralizeIcu()`/`tm()` silently default a missing plural variable to `0`** rather than warning — a typo'd or renamed variable name picks the locale's `0`-category plural form silently (section 4). 3. **ICU plural forms cannot contain nested `{...}`** — no plain `{varName}` interpolation inside a plural category's text, no nested plurals; put interpolation variables outside the plural construct (section 4). 4. **A plural template missing both the resolved CLDR category and `other`** renders that construct as an empty string, not an error or the raw template (section 4). 5. **`loadOneNamespace()` merges namespace messages shallowly** — two namespaces both defining the same top-level key will have the later one fully overwrite the earlier one's, with zero collision detection or warning (section 2). 6. **`persistLocale: true` + SSR**: a returning visitor's real locale is only restored client-side, asynchronously, after mount — expect a visible flash from `defaultLocale` and a possible hydration mismatch warning; nothing in the package detects or documents this (section 6). 7. **`I18nKitConfig`/`LocaleConfig`/`LocaleMeta`** (the actual `i18n-kit.config.json` shape) are not exported from `vue-i18n-kit/config` — only `I18nKitRules`/`I18nKitIgnore` are (section 9). 8. **`vueI18nCheckPlugin`'s `rules` option is accepted in its type but never read** by the plugin's own `runCheck()` implementation — the option exists on `I18nCheckPluginOptions` and is documented as "the same rules configurable in `i18n-kit.config.json`," but tracing `runCheck()`'s body shows it never references `options.rules` anywhere; only key-presence (missing/extra) is actually checked by this plugin, none of the richer validations (`warnOnHtmlTags`, `lengthWarningFactor`, etc.) that `I18nKitRules` describes. Those richer checks live in the browser locale editor (`ui-server`), not in this Vite plugin, despite the shared option type suggesting otherwise. 9. **`vueI18nDevPlugin`'s `iframeWidth` default is `'100vw'` in the actual code**, while its own JSDoc comment states the default as `'480px'` — a stale doc comment; trust the code (`options.ts` destructuring default) over the comment above it. 10. **`src/cli/commands/init.ts` (`runInit`) is entirely dead code** — never imported by `cli/index.ts` (which uses `init-wizard.ts`'s `runInitWizard` exclusively for the real `init` command) or any test. Its `EXAMPLE_MESSAGES` scaffold also uses vue-i18n's legacy pipe-separated plural syntax (`'{count} item | {count} items'`), incompatible with this package's own ICU-only `tm()`/`pluralizeIcu()` (section 4) — stale and misleading if ever resurrected, harmless as long as it stays unreferenced. 11. **A plain locale-messages object with real top-level keys literally named `meta`, `namespaces`, or a `messages` key whose value happens to be a function** would be misidentified as a `LocaleDefinition` by `isLocaleDefinition()`'s duck-typing (checks for the presence of those exact keys) — narrow, but a real edge case if your actual translated content ever needs a top-level key with one of those three names (section 2). 12. **`useNamespace()` resets `isLoaded` to `false` at the start of every reload**, including on every locale switch for a namespace it's already loaded once — a component gating render on `isLoaded` will flicker back to "loading" on every locale change even if the new load resolves near-instantly (section 3). 13. **`useFormat()`'s three formatters construct a fresh `Intl.DateTimeFormat`/`Intl.NumberFormat` on every call** — no memoization by locale+options; hoist your own cached formatter if calling these in a hot loop over many items (section 5). 14. **`vueI18nMapPlugin` calls `process.exit(0)`** inside `configResolved()` when `--mode i18n-dump` is active — this deliberately terminates the whole Vite process after writing the map files; don't combine `--mode i18n-dump` with any other Vite task you expect to keep running in the same invocation (section 7).