Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
The content of this page was translated using an AI.
See the last version of the original content in EnglishIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Vite i18n: the parts that are about Vite, not your framework
Most "Vite i18n" tutorials are really React or Vue tutorials that happen to use Vite. This one is about the layer underneath: how catalogues get imported, what Rollup does with them, and why the lazy loading you wrote is probably not lazy.
Table of Contents
Static import is the default, and it is eager
The simplest setup imports every catalogue at the top of a module.
Copy the code to the clipboard
That is three catalogues in the entry chunk, on every page, for every user. It is fine for two locales and a hundred strings. At ten locales it is the single biggest avoidable cost in the bundle.
import.meta.glob and the flag everyone gets wrong
Vite's glob import is the usual fix.
Copy the code to the clipboard
Lazy is the default: each entry is a function returning a dynamic import, and Rollup emits one chunk per file. Adding { eager: true } inlines all of them into the importing module, which is exactly what you were trying to avoid.
Copy the code to the clipboard
The trap is that both versions work in dev, because Vite serves modules unbundled. The difference only shows up in dist. Check with npx vite build && npx vite preview, then look at what the entry chunk actually contains.
Per-route splitting rarely splits
This is the part that surprises people. You split catalogues by page:
Copy the code to the clipboard
Then two routes both import checkout.json, and Rollup hoists it into a shared chunk that loads on both. Rollup's chunking is driven by the module graph, not by your folder names: a module reachable from more than one entry becomes common. Adding a third route that imports it changes nothing, and adding a fourth might split it differently.
So per-route locale splitting only holds if the import graph really is disjoint. If it matters, verify it rather than assume it:
Copy the code to the clipboard
If you need to force the boundary, build.rollupOptions.output.manualChunks is the escape hatch, at the cost of maintaining it by hand.
Catalogues do not hot-reload
Edit a component, Vite swaps it in. Edit locales/fr.json, and depending on how it was imported, nothing happens. JSON imported dynamically has no HMR boundary, so the module graph does not know to invalidate the consumers.
People work around this by restarting the dev server every time they touch a string, usually without realising it is avoidable. The fix belongs to the i18n plugin: it has to accept the HMR update and push new messages into the running app. When you evaluate a library, check whether its Vite plugin does this, because it is a daily-friction thing rather than a production thing.
define bakes the locale in
It is tempting to resolve the default locale at build time:
Copy the code to the clipboard
define is a textual replacement performed at build time. Whatever value is present when you build is the value that ships, so this forces one build per locale. That is a legitimate strategy, and it is exactly what Angular's official i18n does. It is not what you want if a single deployment has to serve every language.
For values that must vary per request, keep them out of define and resolve them at runtime.
Moving message parsing to build time
Every mature option in this ecosystem eventually does the same thing: stop parsing messages in the browser.
Open the table in a modal to view all data content clearly
| Plugin | What it moves to build time |
|---|---|
@intlify/unplugin-vue-i18n | Compiles vue-i18n messages to render functions, lets you ship runtime |
| Lingui (macro plus plugin) | Extracts and compiles catalogues, replaces macros with message IDs |
| Paraglide (inlang) | Compiles each message into its own tree-shakable function |
vite-intlayer | Builds per-component dictionaries, then purges and minifies unused fields |
The shared payoff is twofold: the runtime message compiler stops shipping, and unused entries become statically removable. The shared cost is that your dev server and your CI both need the plugin, and a bare tsc or a non-Vite test runner will not see the generated output without extra configuration.
vue-i18n is the clearest example of the first payoff. Without @intlify/unplugin-vue-i18n you ship a compiler that calls new Function, which is both bytes you did not need and a Content Security Policy problem.
SSR: never hold the locale in a module
If you add SSR, whether through a framework or vite-plugin-ssr, the rule that matters is this: a module-level variable holding the current locale is shared across every concurrent request on that server process.
Copy the code to the clipboard
Two users hitting the server at the same time will race, and one of them gets the other's language. It does not reproduce in development because you are the only request. Resolve the locale per request and pass it down explicitly, through context or through your framework's request-local storage.
Intlayer's Vite plugin
Intlayer registers a single plugin that handles the dictionary build, dev-mode watching and the optimisation pipeline.
Copy the code to the clipboard
Import rewriting, purge and minify are on by default. The two flags worth knowing live in intlayer.config.ts:
Copy the code to the clipboard
Because content is declared per component rather than per locale file, the purge pass has a module graph to work against, which is what makes the removal safe. The trade is the one named above: the plugin is mandatory everywhere the code is compiled, including CI and test runners. Details in bundle optimisation.
Common mistakes
{ eager: true }on a glob you meant to lazy-load. Works in dev, ships everything in prod.- Trusting folder structure to produce chunks. Rollup follows imports, not directories. Measure the build.
- Restarting the dev server to see a translation change. That is a missing HMR handler, not normal.
- Putting the locale in
define. You have just committed to one build per language. - Module-level locale state with SSR. Cross-request leak that never appears locally.
- Benchmarking the dev server. Unbundled modules tell you nothing about the bundle.
Going further
- Bundle optimisation: purge, minify and what reaches the browser
- Benchmark reports across frameworks
- Configuration reference
- Set up Intlayer with Vite and React
- Drop-in i18next compat adapter
- React i18n: how the provider model works
- Vue i18n: how it works and where it hurts
- Per-component vs centralised i18n
Comments
No comments yet. Be the first to share your thoughts.
