intlify / intlify/bundle-tools
Locale files are never invalidated on Vite 8 — dev server serves stale translations until restart
- Dominant language
- TypeScript
- Stars
- 270
- Forks
- 46
- PR merge metrics
- No merged PRs in 30d
Description
## Description
On Vite 8, editing a locale file included via the `include` option never updates the dev server — the app keeps rendering the previous translations until the dev server is fully restarted. No HMR update and no page reload is emitted.
The cause is the virtual-id path added in #565 (`fix(unplugin-vue-i18n): bypass builtin:vite-json on Vite 8 via virtual ids`), first shipped in `11.2.1`.
`configResolved` decides which strategy to use based on whether Vite's internal JSON plugin is present:
```js
const jsonPlugin = getVitePlugin(config, 'vite:json')
hasViteJsonPlugin = !!jsonPlugin
```
Vite 8 no longer registers a `vite:json` plugin at all (Rolldown parses JSON natively), so `hasViteJsonPlugin` is `false` and `resolveId` rewrites every included locale file to a `virtual:intlify-i18n-N` id.
That virtual module is what the app actually imports, and Vite has no way to map the virtual id back to `src/locales/en.json`. So when the file changes:
- `/src/locales/en.json?import` is invalidated and serves **fresh** content
- `/@id/virtual:intlify-i18n-0` — the module the app imports — is **never** invalidated and keeps serving the previously compiled messages
`this.addWatchFile(realId)` in `load` registers the file with the watcher (which is why the file change is detected at all), but it does not associate the virtual module with the file in Vite's dev module graph, so nothing invalidates it.
The `handleHotUpdate` hook in the plugin is also dead code on Vite 8 — the hook was removed in Vite 8, and it only invalidated the `INTLIFY_BUNDLE_IMPORT_ID` virtual module anyway, not per-file locale virtual modules.
### Expected behavior
Editing a locale file included via `include` should invalidate the compiled messages and trigger a page reload, as it does on Vite 7.
Direct A/B on the same project, only the Vite version changed:
| | vite 7.3.6 | vite 8.1.5 |
| --- | --- | --- |
| `main.js` imports | `/src/locales/en.json?import` (real module) | `/@id/virtual:intlify-i18n-0` (virtual module) |
| served after editing `en.json` | `goodbye` (fresh) | `hello` (stale) |
| dev server log | `page reload src/locales/en.json` | *(nothing)* |
### Reproduction
Minimal repro — `vue@3.5.38`, `vue-i18n@11.4.2`, `@intlify/unplugin-vue-i18n@11.2.4`, `@vitejs/plugin-vue@6.0.8`, `vite@8.1.5`.
`vite.config.js`:
```js
import path from 'path';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite';
export default defineConfig({
plugins: [
vue(),
VueI18nPlugin({ include: path.resolve(__dirname, './src/locales/**') }),
],
});
```
`src/locales/en.json`:
```json
{ "greeting": "hello" }
```
`src/main.js`:
```js
import { createApp, h } from 'vue';
import { createI18n } from 'vue-i18n';
import en from './locales/en.json';
const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } });
createApp({ render: () => h('h1', i18n.global.t('greeting')) }).use(i18n).mount('#app');
```
Steps:
1. `vite --port 5199`
2. `curl -s http://localhost:5199/src/main.js | grep 'import en'` → `import en from "/@id/virtual:intlify-i18n-0";`
3. `curl -s 'http://localhost:5199/@id/virtual:intlify-i18n-0' | grep -o hello` → `hello`
4. Change `en.json` to `{ "greeting": "goodbye" }`
5. `curl -s 'http://localhost:5199/@id/virtual:intlify-i18n-0' | grep -o 'hello\|goodbye'` → still **`hello`**
6. `curl -s 'http://localhost:5199/src/locales/en.json?import' | grep -o 'hello\|goodbye'` → `goodbye`
The browser shows `hello` until the dev server is restarted. Pinning `vite@7.3.6` and repeating makes step 5 return `goodbye` and logs `page reload src/locales/en.json`.
### Issue Package
unplugin-vue-i18n
### System Info
```shell
OS: macOS 14.3 (darwin 23.3.0)
@intlify/unplugin-vue-i18n: 11.2.4 (also reproduces on 11.2.1 – 11.2.3; 11.1.2 and 11.2.0 are unaffected, they predate the virtual-id path)
vite: 8.1.5
rolldown: 1.1.5
@vitejs/plugin-vue: 6.0.8
vue: 3.5.38
vue-i18n: 11.4.2
node: 24.15
```
### Screenshot
_No response_
### Additional context
As a workaround, a small dev-only plugin that invalidates the intlify virtual modules and forces a reload restores the previous behaviour:
```js
const localesDir = path.resolve(__dirname, './src/locales');
const i18nLocaleHmr = () => ({
name: 'i18n-locale-hmr',
apply: 'serve',
hotUpdate({ file }) {
if (!file.startsWith(localesDir)) { return; }
const { moduleGraph, hot } = this.environment;
[...moduleGraph.idToModuleMap.values()]
.filter((mod) => mod.id?.includes('virtual:intlify-i18n-'))
.forEach((mod) => moduleGraph.invalidateModule(mod));
hot.send({ type: 'full-reload' });
return [];
},
});
```
A fix inside the plugin would presumably want a `hotUpdate` hook that maps the changed file back through `realPathToVirtualId` and invalidates the matching virtual module.
### Validations
- [x] Read the [Contributing Guidelines](https://github.com/intlify/bundle-tools/blob/main/CONTRIBUTING.md).
- [x] Read the README
- [x] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [x] Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/intlify/bundle-tools/discussions).
🤖 Commented by Claude
Contributor guide
Research direction
Start by reproducing the Vite 8 behavior with the provided vite.config.js, locale file, and curl steps, then read the plugin's configResolved, resolveId, load, and hotUpdate hooks. Trace realPathToVirtualId and the Vite 8 module graph to identify how the changed locale maps to its virtual module. Done means editing an included locale triggers invalidation and a page reload without restarting the dev server.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, vite
- Domain
- build-system, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100