Ajukan pertanyaan Anda dan dapatkan ringkasan dokumen dengan merujuk halaman ini dan penyedia AI pilihan Anda
Konten halaman ini diterjemahkan menggunakan AI.
Lihat versi terakhir dari konten aslinya dalam bahasa InggrisJika Anda memiliki ide untuk meningkatkan dokumentasi ini, silakan berkontribusi dengan mengajukan pull request di GitHub.
Tautan GitHub ke dokumentasiSalin Markdown dokumentasi ke clipboard
TanStack Start i18n: locale routing, SSR and the type tax
TanStack Start ships no i18n layer, and unlike Next.js or Nuxt there is no first-party module to fill the gap. So the interesting question is not "which library", it is how a locale fits into a generated, type-safe route tree without breaking every Link in the app. This post covers the wiring: the route segment, the negotiation, SSR and prerender, and the honest state of the options.
Table of Contents
Put the locale in the URL, not in a cookie
The tempting shortcut is a cookie: read it on the server, set the locale, done. It works in development and fails everywhere else. One URL then serves several languages, so you cannot emit hreflang, you cannot prerender a page per locale, and a crawler sees whichever language your negotiation happens to pick.
A route segment fixes all three at once. In TanStack Router's file-based routing that means a directory:
Salin kode ke clipboard
The braces make the segment optional. {-$locale} matches /about and /fr/about, which is what you want when the default locale has no prefix. If every locale is prefixed, use plain $locale instead. Be careful stacking it with other dynamic segments in the same route, {-$locale}/$slug is ambiguous by construction.
Negotiate in beforeLoad, not in a component
beforeLoad runs on the server and on the client, before the route renders, and it can throw a redirect. That is the right hook for locale work: an unknown prefix must 404 rather than silently resolve.
Salin kode ke clipboard
Without that check, /xx/about matches the segment, renders in your default language, and gets indexed as a duplicate of /about. This is the single most common i18n bug I see on file-based routers.
Accept-Language negotiation for a first visit belongs one layer lower, in server middleware, so the redirect happens before the router boots. Intlayer's Vite plugin registers a proxy that does this and writes the locale cookie; if you wire it yourself, remember to exclude /api and asset paths.
The type tax nobody mentions
This is the part that surprises people. TanStack Router generates routeTree.gen.ts and types every to prop against it. Add a locale directory and every path in your app changes shape:
Salin kode ke clipboard
You do not want that literal in three hundred call sites. The usual fix is one wrapper component that strips the prefix from the public type and re-adds it at runtime:
Salin kode ke clipboard
Do the same for useNavigate. Budget an afternoon for this and a codemod pass over existing links: it is real work, and it is independent of which translation library you pick.
SSR, hydration and prerender
Resolve the locale from route params in the root shell, and server and client cannot disagree, because both read the same URL:
Salin kode ke clipboard
Read the locale from navigator.language or localStorage during render instead, and you get a hydration mismatch on every visitor whose browser disagrees with the server. The symptom is React error #418 in production and text that flickers from one language to another.
Prerendering is where the URL choice pays off. You expand your path list across locales and hand TanStack Start a page per localized route:
Salin kode ke clipboard
One gotcha specific to this stack: if you colocate content files inside src/routes, the router will try to turn them into routes. Exclude them with routeFileIgnorePattern in the tanstackStart() plugin options.
Loaders vs static strings
Two different things get conflated. UI strings ("Add to cart", "Loading") are static, known at build time, and belong in the component, not in a loader. Localized data (a product description from your CMS) is a fetch, and belongs in loader so it streams with the route.
Route metadata sits between the two, because head runs outside the React tree. You have three options and they trade off differently:
Buka tabel dalam modal untuk melihat semua isi data dengan jelas
Sync read in head | Async read in head | Read in loader, use in head | |
|---|---|---|---|
head signature | synchronous | async | synchronous, reads loaderData |
| Locales shipped | all declared locales | requested locale only | requested locale only |
| Cost | bigger route chunk | dictionary import on the head critical path, so slightly worse LCP | content threaded through loaderData |
The loader variant with staleTime: Infinity is the one I would default to on a real site: loaders of matched routes run in parallel, and the result is cached per locale.
The options today
The ecosystem here is thin. Most TanStack Start apps wire react-i18next by hand, because that is what the React answers on the internet describe, and it does work.
Buka tabel dalam modal untuk melihat semua isi data dengan jelas
| Option | How you wire it | Note |
|---|---|---|
react-i18next | Manual: provider in the root shell, catalogs per namespace, your own loader for SSR | Largest ecosystem. Around 17.3 kB in the TanStack benchmark, and namespace discipline is on you |
use-intl | Manual, same shape, ICU messages | Avoids the Next.js specific traps of next-intl; leakage problem is identical |
| Lingui | lingui extract / lingui compile in the build | Compile step, ICU. I found the workflow heavier than it needed to be with no clear win here |
| Paraglide | Codegen of message functions | The advertised tree-shaking did not hold up in the benchmark, and it re-reads the locale from storage per node instead of a store |
| Intlayer | Vite plugin, build-time compilation | Covers routing, prerender and sitemap; smaller ecosystem |
None of them solve the route-tree typing problem for you. That wrapper is yours to write either way.
Intlayer on TanStack Start
Intlayer's model is one content file per component, compiled at build time into per-component dictionaries. For this stack the practical difference is that locale routing, the prerender pass and the sitemap are covered by the same package rather than assembled from three.
Salin kode ke clipboard
Salin kode ke clipboard
useIntlayer inside components, getIntlayer or getIntlayerAsync outside the React tree (route head, loaders, server functions). In a server function, getLocale reads the cookie or header for you. Setup is npx intlayer init, plus the intlayer() plugin in vite.config.ts. As a data point, intlayer.org itself runs on TanStack Start.
What it costs you. A build plugin is mandatory: no plugin, no dictionaries, and that includes your test runner config. The project is younger and much smaller than i18next, so when something breaks on a TanStack Start canary you are reading source, not Stack Overflow. ICU message format is not fully supported, which matters if a translation vendor delivers ICU strings today. And the per-component model means you cannot hand a translator one big JSON file without going through the CLI or the CMS.
Coming from an existing codebase, the @intlayer/react-i18next compat adapter keeps useTranslation() and t() working while Intlayer serves the content, and the migration guide covers moving off it afterwards.
Common mistakes
- Locale in a cookie only. No
hreflang, no prerender, one URL per page for every language. Fix the URL scheme first, pick the library second. - No prefix validation.
/xx/aboutrendering your default language is a duplicate-content bug that only shows up in Search Console, months later. - Reading the locale on the client during render. Route params are the only source both sides agree on.
- A
<button>locale switcher. Crawlers do not click. Render locale links as anchors pointing at the localized URL. - Content files inside
src/routes. They become routes unless you setrouteFileIgnorePattern. - Awaiting dictionaries serially in
head. UsePromise.allif a head reads more than one.
Going further
- Full TanStack Start setup guide, step by step
- TanStack Start i18n benchmark: bundle size, leakage, locale-switch timings
- Drop-in
react-i18nextcompat adapter and the react-i18next migration guide - The wider React i18n landscape
- Per-component vs centralized i18n
- Getting hreflang right on a multilingual site
- Routing modes, domains and URL schemes in the configuration reference
Komentar
Belum ada komentar. Jadilah yang pertama membagikan pemikiran Anda.
