Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
If 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
How to pick the right Solid i18n library
Solid's reactivity model changes what an i18n library has to do. Components run once, so a translation stored in a const at setup is a frozen string, and a library that hands you strings instead of accessors will produce a page that switches language everywhere except in the three components where someone did that. Picking a library for Solid is partly about API, and partly about which one makes that mistake hard to write.
This guide lists the questions to answer first, then maps them to @solid-primitives/i18n, solid-i18next, Paraglide, @lingui/solid and Intlayer, for Vite + Solid and for SolidStart.

Table of Contents
Six questions to answer before comparing libraries
- Vite SPA or SolidStart? In an SPA the locale can live in a signal and nothing else. On SolidStart the locale has to be resolved on the server from the URL, and anything a crawler must see without JavaScript (
<html lang>,hreflang) belongs inentry-server.tsx. - How reactive does locale change need to be? A full page reload on switch is acceptable for some apps. If not, the library's values must be signals or accessors, and reading them must be tracked, not copied.
- Who writes the translations? Developers, a TMS, an agency delivering ICU strings, or an AI pipeline.
solid-i18nextspeaks i18next's format.@solid-primitives/i18nis whatever your dictionary object is. Match the vendor. - How many locales and pages? Two locales and five pages can ship everything. Ten locales and forty routes cannot, and lazy catalogs plus scoping become the main cost.
- Do you need types on keys?
@solid-primitives/i18ninfers them from the source dictionary.solid-i18nextneeds manual declaration. Compile-time libraries generate them. - How much feature surface do you need? Cookie management, locale-prefixed routing, redirects, formatters. The lightest option has none of it, and that is fine until it is not.
Write the answers down. Everything below refers back to them.
The landscape in one picture
Solid is the youngest ecosystem here and has the fewest options, spread across three waves.

i18next wrapped for Solid. Namespaces, backends, detectors, and a decade of plugins. Heaviest of the set, and the same t("a.b") costs as in React.
A flat dictionary you own, a translator() that returns accessors, types inferred from the source object. Very small, no scoping, no routing, no formatters. The community default.
Paraglide generates one function per message. Intlayer declares content per component in .content.ts files and returns signal-backed nodes. Lingui's Solid binding arrived in 2026 and brings its macro-based extraction.
The history of JavaScript i18n covers each wave in detail.
The decision that matters most: where content lives and when it loads
Two structural choices explain most of the bundle difference between setups:
- Centralized or scoped content. One dictionary for the app, or one declaration per component.
- Static or dynamic import. Everything at startup, or the active locale (and ideally the active route) fetched on demand.
The graph estimates the payload for a theoretical app of 1 to 10 pages, translated into 1 to 10 locales, with about 30 KB of text per page.

@solid-primitives/i18n does nothing about either axis: you createResource a dictionary per locale, which gets you dynamic loading, and the rest is yours. solid-i18next has namespaces and lazy backends, but nothing enforces the mapping, so a shared component importing common makes it a dependency of every route. Paraglide gets the page axis through tree-shaking, although it did not take effect in the Solid benchmark implementation. Intlayer gets it through per-component declarations.
If your answer to question 4 was "many pages", weigh this section more than any API preference. The per-component vs centralized i18n post covers the maintenance side of the same trade-off.
The candidates
Library sizes are from the Solid benchmark: provider plus accessor in an empty component, after bundling, tree-shaking and minification, on a 10-page, 10-locale app. Content is measured separately.
Open the table in a modal to view all data content clearly
| Library | Content model | Reactivity on locale change | Types on keys | Scoping and lazy loading | Library size |
|---|---|---|---|---|---|
@solid-primitives/i18n | Flat dictionary you own | Signal, accessors returned by translator | Inferred from the source dictionary | None built in | Very small |
solid-i18next | i18next catalogs and namespaces | Store, re-render via provider | Manual declaration | Namespaces, lazy backends | ~14.9 kB |
| Paraglide | inlang project, generated functions | Read per call from cookie or storage | Generated | Tree-shaking (not in bench) | Near zero |
@lingui/solid | Source text in code, compiled catalogs | Signal-based | From the compiler | Per catalog | Small |
| Intlayer | One .content.ts per component | Signal-backed nodes, no component re-run | Generated, on by default | Yes, per component | Baseline |
Numbers are a snapshot at the benchmark's versions. @lingui/solid was not in the benchmark. Run it on your own app before deciding on size alone.
Paraglide's near-zero library size is by construction: the runtime is generated into your repository. Intlayer needs vite-intlayer, so it cannot run without a build step.
Match your answers to a library
@solid-primitives/i18n. A flat dictionary, a translator() that returns accessors, types inferred with no wiring. It is the right answer for a small app, and reading the source takes ten minutes. What you will write yourself: locale persistence, routing, formatters, and per-route splitting. If those lists grow, that is the signal to move.
solid-i18next lets you reuse catalogs, namespaces, backends and detectors as they are. It is the heaviest option and carries the same costs as react-i18next: manual type declaration, optimizations that are possible but time-consuming, and a t() that returns a string, so the frozen-translation bug is easy to write. Wrap reads in JSX or a memo and never store them at setup.
The locale has to come from the URL on the server so both sides agree; detecting it on the client is too late. @solid-primitives/i18n and solid-i18next leave the [[locale]] route, matchFilters, the redirect and the entry-server.tsx tags to you. Paraglide has a Vite plugin that handles routing. Intlayer ships middleware and the route helpers. Whichever you pick, put <html lang> and hreflang in entry-server.tsx; @solidjs/meta applies on the client after hydration in SolidStart v2. The Solid i18n post walks through that setup.
Pick a library whose values are signals or accessors and whose reads are tracked. @solid-primitives/i18n accessors and Intlayer nodes both update only the DOM nodes that read them, with no component re-run. solid-i18next re-renders through the provider. Paraglide reads the locale from cookie or storage on each message call rather than from a signal, which works but does more work per node than it should.
Scoped content compiled at build time. Intlayer ships only what a route renders. Paraglide should get there via tree-shaking; verify it in your setup, since it did not in the benchmark's. With solid-i18next, plan the namespace and lazy-loading strategy on day one and enforce it in review.
@solid-primitives/i18n gives you inferred types for free, which is more than most React libraries offer. For generated types that survive lazy loading and per-route splitting, Paraglide, @lingui/solid and Intlayer all produce them from the content. The detecting missing translations post compares what each catches at build time.
Then a centralized dictionary has no consumer left to justify it. Colocated content plus a CLI that fills the missing locales is the shorter path. Intlayer's fill command runs against your own API key (OpenAI, Anthropic, Mistral, Gemini) and only re-translates what changed.
Where each library falls short
@solid-primitives/i18n: no lazy loading or scoping beyond what you build, no routing, no cookie handling, no formatters. Excellent for small apps, quickly lacking for professional ones.solid-i18next: heaviest of the set, manual types, its own plural format, andt()returns a string so translations freeze if stored at setup.- Paraglide: generated files committed to the repo and regenerated before every push, tree-shaking did not take effect in the Solid benchmark, and the locale is read from storage per call instead of from a signal.
@lingui/solid: new in 2026, so little production feedback yet. Inherits Lingui'sextract/compilebuild step and its several overlapping syntaxes.- Intlayer: mandatory build plugin, smaller ecosystem, partial ICU support, and content spread across the codebase by design, so exporting one JSON for a translator needs tooling.
What each option looks like in code
The same component, a cart summary with a title and a plural, written with each candidate. Watch where the translation is read: in JSX it is tracked, in the setup body it is a frozen string.
Copy the code to the clipboard
Copy the code to the clipboard
Keys are typed from the English object with no codegen. There is no plural rule, no lazy loading and no routing; each is yours to add.
Copy the code to the clipboard
Copy the code to the clipboard
i18next catalogs, namespaces and plugins as they are. t returns a string, so const title = t("cart:title") at setup freezes it; keep the call inside JSX.
Copy the code to the clipboard
Copy the code to the clipboard
Every message is a generated, typed function. The locale is read from cookie or storage on each call rather than from a signal, so reactivity on switch is yours to wire.
Copy the code to the clipboard
Copy the code to the clipboard
All locales in one file beside the component. useIntlayer returns signal-backed nodes, so a locale change updates only the DOM nodes that read them. {content.title} in JSX is tracked; content.title.value in the setup body is not.
On an existing i18next codebase, the i18next compat adapter aliases the package at the bundler level so catalogs and t() keep working while Intlayer serves the content, and the migration guide covers the rest.
Before you commit
A feature table tells you what a library does today. These points tell you what living with it will be like.
Check repository activity.
Commits, issue response time, and whether the last minor release was this year. A sound design with no maintainer is a migration in waiting.
Do not pick by npm downloads.
The most installed library is the one that shipped first, not the one that fits a 2026 Solid codebase. Downloads measure history, not fit.

Ask who pays the maintainer, and what they sell.
i18next (behind solid-i18next) is backed by Locize. next-intl, vue-i18n, svelte-i18n and Lingui are backed by Crowdin. Tolgee, Paraglide (inlang) and Intlayer each run their own platform. A vendor whose revenue is hosted translation has little reason to make translation free inside your toolchain. Intlayer is the only one of the set that ships AI translation through the CLI with your own API key, and a CMS you can self-host.
Is it AI-agent ready?
Agents still struggle with i18n: they forget locales, invent keys, and mix message syntaxes. Does the library ship Agent Skills or an MCP server so the agent can list, fill and test content? And is content loading optimized by default, or does someone have to review namespaces and lazy imports every quarter?
Type safety out of the box.
Not "can be typed with extra wiring" but "a wrong key fails tsc on a fresh install". Check what happens with a key that does not exist, and with a locale that is missing one translation.
Detection of unused content.
Catalogs only grow. Intlayer's build purges unused fields and logs them (build.purge). Paraglide gets there by architecture, since an uncalled message function is tree-shaken. Everything else leaves the sweep to you.
Developer experience.
Setup time to first translated string, an LSP or VS Code extension that shows the translation on hover and jumps to the declaration, a CLI for fill, test and push, and a way for non-developers to edit content (visual editor or CMS) without a pull request.
Frequently Asked Questions
For a small one, yes, and it is the lightest option available. It stops being enough when you need lazy catalogs per route, locale routing on SolidStart, cookie persistence or formatters, because all of that is yours to build.
Because Solid components run once. A translation read into a const at setup is a plain string, not a subscription. Read it inside JSX, an effect or a memo, or pick a library whose values are accessors so the wrong version is harder to write.
Only if bundle size, generated types or build-time missing-key checks are actual requirements. The compiler vs declarative i18n post explains what compilers give you and where they can get it wrong.
Indirectly. Crawlers care about routing, hreflang, <html lang> and whether text is in the server-rendered HTML, which on SolidStart means entry-server.tsx. See the hreflang guide.
Going further
- Solid i18n benchmark: bundle size, leakage and locale-switch timings
- Solid i18n: why translations freeze on locale change
- Drop-in i18next compat adapter and the i18next migration guide
- The history of JavaScript i18n
- Compiler vs declarative i18n
- Per-component vs centralized i18n
- How bundle optimization works at build time
- Set up i18n in a Vite + Solid app and in a SolidStart app
- Same guide for React, Vue and Svelte
Comments
No comments yet. Be the first to share your thoughts.
