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:
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:
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:
src/main.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:
vite --port 5199
curl -s http://localhost:5199/src/main.js | grep 'import en' → import en from "/@id/virtual:intlify-i18n-0";
curl -s 'http://localhost:5199/@id/virtual:intlify-i18n-0' | grep -o hello → hello
- Change
en.json to { "greeting": "goodbye" }
curl -s 'http://localhost:5199/@id/virtual:intlify-i18n-0' | grep -o 'hello\|goodbye' → still hello
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
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:
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
🤖 Commented by Claude
Description
On Vite 8, editing a locale file included via the
includeoption 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 in11.2.1.configResolveddecides which strategy to use based on whether Vite's internal JSON plugin is present:Vite 8 no longer registers a
vite:jsonplugin at all (Rolldown parses JSON natively), sohasViteJsonPluginisfalseandresolveIdrewrites every included locale file to avirtual:intlify-i18n-Nid.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?importis invalidated and serves fresh content/@id/virtual:intlify-i18n-0— the module the app imports — is never invalidated and keeps serving the previously compiled messagesthis.addWatchFile(realId)inloadregisters 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
handleHotUpdatehook in the plugin is also dead code on Vite 8 — the hook was removed in Vite 8, and it only invalidated theINTLIFY_BUNDLE_IMPORT_IDvirtual module anyway, not per-file locale virtual modules.Expected behavior
Editing a locale file included via
includeshould 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:
main.jsimports/src/locales/en.json?import(real module)/@id/virtual:intlify-i18n-0(virtual module)en.jsongoodbye(fresh)hello(stale)page reload src/locales/en.jsonReproduction
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:src/locales/en.json:{ "greeting": "hello" }src/main.js:Steps:
vite --port 5199curl -s http://localhost:5199/src/main.js | grep 'import en'→import en from "/@id/virtual:intlify-i18n-0";curl -s 'http://localhost:5199/@id/virtual:intlify-i18n-0' | grep -o hello→helloen.jsonto{ "greeting": "goodbye" }curl -s 'http://localhost:5199/@id/virtual:intlify-i18n-0' | grep -o 'hello\|goodbye'→ stillhellocurl -s 'http://localhost:5199/src/locales/en.json?import' | grep -o 'hello\|goodbye'→goodbyeThe browser shows
hellountil the dev server is restarted. Pinningvite@7.3.6and repeating makes step 5 returngoodbyeand logspage reload src/locales/en.json.Issue Package
unplugin-vue-i18n
System Info
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.15Screenshot
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:
A fix inside the plugin would presumably want a
hotUpdatehook that maps the changed file back throughrealPathToVirtualIdand invalidates the matching virtual module.Validations
🤖 Commented by Claude