Ask your question and get a summary of the document by referencing this page and the AI provider of your choice
Version History
- "Initial version"v9.5.109/26/2026
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
How to internationalize your TanStack Start application using use-intl in 2026
Table of Contents
What is use-intl?
use-intl is the framework-agnostic core of next-intl. It exposes the same useTranslations, useFormatter and IntlProvider APIs, ICU MessageFormat support and strong TypeScript integration, without any dependency on Next.js. That makes it one of the most common choices to translate a TanStack Start application, and it is the library AI assistants suggest most often for this stack.
TanStack Start does not ship an i18n layer. Routing, locale detection, SEO metadata and sitemap generation are left to you. This guide covers all of it, end to end:
- Locale-aware routing with an optional
{-$locale}segment (/about,/fr/about). - Per-route message loading so a page only downloads the namespaces and the locale it renders.
- Server rendering and hydration without text mismatches.
- Complete multilingual SEO: translated
<title>and description, canonical URL,hreflangalternates withx-default, Open Graph locales, JSON-LD, sitemap withxhtml:linkalternates,robots.txtand pre-rendering of every locale.
Looking for another stack? See the TanStack Start + Paraglide guide, the TanStack Start + Lingui guide, or the TanStack Start + Intlayer guide.
Using Next.js instead? See the next-intl guide.
What the benchmark says about use-intl on TanStack Start
The i18n benchmark runs the same 10-page, 10-locale TanStack Start app with every major library and measures what the browser actually downloads.
Dynamic JSON loading
Lazy-loads translations at runtime
Scoped JSON (namespacing)
Per-page translation namespaces
I18n Performance Benchmark
What is this metric?
The total gzip-compressed size of the internationalization library bundle. It only includes the provider and content retrieval logic after tree-shaking and minification.
Why is it important?
A smaller library size reduces the initial JavaScript payload, leading to faster download and execution times on the client.
View as
Key figures for use-intl@4.14.2, measured on 2026-09-26 (gzip):
Open the table in a modal to view all data content clearly
| Setup | Library size | JS per page | Other-locale leak | Other-page leak |
|---|---|---|---|---|
| No i18n (base app) | - | 111.0 KB | 0% | 0% |
use-intl (setup of this guide) | 75.9 KB | 128.7 KB | 0% | 0% |
@intlayer/use-intl (compat) | 6.7 KB | 129.4 KB | 0% | 0% |
react-intlayer (native Intlayer) | 4.5 KB | 126.8 KB | 0% | 0% |
What to take away:
- Split messages by page and load them per locale. It removes both leaks, and it is what the steps below implement.
- The runtime itself stays heavy (~76 KB gzip), because the ICU parser ships to the client. The
@intlayer/use-intlcompat adapter (step 17) keeps the exact same API with a ~7 KB runtime.
See the full data: TanStack Start benchmark report, and the benchmark repository.
Feature comparison on TanStack Start
How use-intl compares with the other libraries commonly used on TanStack Start:
Open the table in a modal to view all data content clearly
| Feature | react-intlayer (Intlayer) | use-intl | Paraglide JS | Lingui |
|---|---|---|---|---|
| Translations near components | ✅ Co-located | ❌ Centralized JSON | ❌ One JSON file per locale | ⚠️ Source text in components |
| TypeScript integration | ✅ Auto-generated types | ✅ Via AppConfig | ✅ Typed message functions | ⚠️ Macros only |
| Missing translation detection | ✅ Type errors and build warnings | ⚠️ Runtime fallback | ⚠️ Falls back to the base locale | ⚠️ Falls back to the source text |
| Rich content (JSX, Markdown) | ✅ Direct support | ⚠️ Tags via t.rich | ⚠️ Strings | ✅ JSX inside <Trans> |
| Localized routing | ✅ Built-in | ❌ Manual {-$locale} | ✅ urlPatterns + router rewrite | ❌ Manual {-$locale} |
| Locale switch without reload | ✅ Yes | ✅ Yes | ❌ Full page reload | ✅ Yes |
| Pluralization | ✅ Enumeration-based | ✅ ICU | ✅ Variants | ✅ ICU |
| ICU MessageFormat | ✅ Via format: "icu" | ✅ Native | ⚠️ Via an inlang plugin | ✅ Native |
| Content formats | ✅ .ts, .json, .md, .yaml... | ⚠️ .json | ⚠️ inlang JSON | ✅ PO, JSON, CSV |
| AI translation | ✅ Your own provider and key | ❌ No | ❌ No | ❌ No |
| Visual editor / CMS | ✅ Local editor + optional CMS | ❌ External platforms | ⚠️ inlang ecosystem apps | ❌ External platforms |
| SEO helpers (hreflang, sitemap) | ✅ Built-in | ❌ Manual | ⚠️ Localized URLs, rest manual | ❌ Manual |
| Runtime size (gzip, benchmark) | 4.5 KB | 75.9 KB | 1.8 KB | 56.7 KB |
| Leak, best setup (locale / page) | 0% / 0% | 0% / 0% | 49.7% / 0% | 8.6% / 0% |
| Missing translations in CI | ✅ npx intlayer test | ⚠️ Not built-in | ⚠️ Not built-in | ✅ lingui compile --strict |
Runtime size and leak figures come from the TanStack Start benchmark. Leak is measured on the best setup of each library.
Other TanStack Start guides: Lingui, Paraglide JS, and Intlayer.
Practices you should follow
- Set
langanddiron<html>for accessibility, screen readers and search engines. - Keep one URL per locale. Use a locale prefix (
/fr/about) rather than a cookie-only switch, so every translated page is crawlable and shareable. - Split messages by namespace (
common,home,about) and load them per route. - Load only the active locale. Never import every locale file in a module that ships to the client.
- Fix the time zone in
IntlProvider. Otherwise dates are formatted in the server time zone during SSR and in the visitor time zone on hydration, which causes hydration mismatches. - Translate your metadata, and declare
canonical,hreflangandx-defaulton every page. - Generate a multilingual sitemap and robots.txt, and pre-render every locale.
- Use real links for the locale switcher, not a
<select>, so crawlers can discover every language. - Type your messages so a missing key fails at compile time.
See our guide on internationalization and SEO and the hreflang guide.
Step-by-Step Guide to Set Up use-intl in a TanStack Start Application
Here's the project structure we'll be creating:
Copy the code to the clipboard
Install Dependencies
Start from a TanStack Start project, then add
use-intl:bashCopy codeCopy the code to the clipboard
- use-intl: provides
IntlProvider,useTranslations,useFormatterandcreateTranslator(usable outside React, for example inhead()).
- use-intl: provides
Centralize Your Locale Configuration
Create a single source of truth for your locales and URL helpers. Every other file (routes, SEO, sitemap, pre-rendering) imports from here, so adding a locale is a one-line change.
The default locale stays unprefixed (
/about), other locales are prefixed (/fr/about). This is the "as-needed" strategy: one URL per page per locale, and short URLs for your main audience.src/i18n/config.tsCopy codeCopy the code to the clipboard
Create Your Translation Files
Organize messages per locale and per namespace.
commonholds what every page needs (navigation, footer), and each page gets its own file, including its metadata.use-intl uses ICU MessageFormat, so plurals, selects and formatted arguments live in the message itself.
messages/en/common.jsonCopy codeCopy the code to the clipboard
messages/en/about.jsonCopy codeCopy the code to the clipboard
messages/fr/common.jsonCopy codeCopy the code to the clipboard
messages/fr/about.jsonCopy codeCopy the code to the clipboard
Create
home.jsonthe same way, with ametadataobject and the page content.Load Messages per Namespace and per Locale
This loader is the most important file for performance.
import.meta.globtells Vite to emit one chunk per JSON file. A route that asks for["about"]in French downloadsmessages/fr/about.jsonand nothing else, which is how the benchmark reaches 0% locale leak and 0% page leak.src/i18n/messages.tsCopy codeCopy the code to the clipboard
Type Your Messages
Module augmentation gives you autocompletion on
useTranslations("about")andt("counter.label"), and a compile error on any typo or removed key.src/i18n/use-intl.d.tsCopy codeCopy the code to the clipboard
Make sure
resolveJsonModuleis enabled in yourtsconfig.json.Create the Root Document
The root route renders
<html>. It reads the optional locale param to setlanganddir, so the attributes are correct in the server-rendered HTML, before any JavaScript runs.src/routes/__root.tsxCopy codeCopy the code to the clipboard
Create the Locale Layout Route
The
{-$locale}folder creates an optional path segment:/aboutand/fr/aboutboth match/{-$locale}/about. This layout:- Rejects unsupported prefixes (
/xx/about→ 404). - Loads the
commonnamespace for the current locale only. - Provides the messages through
IntlProvider.
The loader result is serialized into the HTML and reused on hydration, so the client does not download
common.jsona second time.staleTime: Infinitykeeps it cached across client navigations.src/routes/{-$locale}/route.tsxCopy codeCopy the code to the clipboard
IntlProviderdoes not merge messages from a parent provider. The next step adds a small component that does, so each page can add its own namespace on top ofcommon.- Rejects unsupported prefixes (
Scope Page Messages
Each page loads its own namespace in its loader, then wraps its content with
ScopedMessages, which merges the page namespace with the parent messages.src/components/ScopedMessages.tsxCopy codeCopy the code to the clipboard
Utilize Translations in Your Pages
The page loader fetches the
aboutnamespace for the current locale,head()builds translated, SEO-complete metadata from it (see step 13), and the component renders the content.src/routes/{-$locale}/about.tsxCopy codeCopy the code to the clipboard
Use Translations and Formatters in Components
Any component under the providers can call
useTranslationsanduseFormatter. Plurals are resolved by ICU, and numbers are formatted according to the active locale.src/components/Counter.tsxCopy codeCopy the code to the clipboard
Build a Localized Link Component
OptionalEvery route lives under
{-$locale}, so a link must carry the current locale param. This wrapper keeps the typedtoof TanStack Router and injects the locale for you.src/components/LocalizedLink.tsxCopy codeCopy the code to the clipboard
src/components/Header.tsxCopy codeCopy the code to the clipboard
Change the Language of Your Content
OptionalRender the switcher as links, not a
<select>. Links are crawlable, so search engines find every language version, and they work without JavaScript.to="."keeps the current page and only replaces the locale param. The cookie remembers the explicit choice for the redirect middleware of step 16.src/components/LocaleSwitcher.tsxCopy codeCopy the code to the clipboard
Internationalize Your Metadata
OptionalThis is where i18n pays off: each language version can rank on its own. Every page must expose:
- a translated
<title>anddescription; - a canonical URL pointing to itself (not to the default locale);
- one
hreflangalternate per locale, plusx-defaultfor unmatched languages; - Open Graph
og:locale,og:locale:alternateandog:url, used by social previews; - JSON-LD with
inLanguage, which helps search engines and AI assistants attribute the language of the page.
A single helper builds all of it, so pages stay short:
src/i18n/seo.tsCopy codeCopy the code to the clipboard
Use it in every page
head(), as shown in step 9. For the home page, passpath: "/".- a translated
Internationalize Your Sitemap
OptionalA multilingual sitemap lists every URL of every locale, and each entry declares all its alternates with
xhtml:link. Google uses these annotations exactly like thehreflangtags of the page, which makes them a reliable backup when a page is rarely crawled.TanStack Start server routes let you serve it from a file route:
src/routes/sitemap[.]xml.tsCopy codeCopy the code to the clipboard
Internationalize Your robots.txt
OptionalPrivate routes exist in every language, so the
Disallowrules must cover every prefix. Removepublic/robots.txtif the starter created one, then serve it from a route:src/routes/robots[.]txt.tsCopy codeCopy the code to the clipboard
Redirect First-Time Visitors to Their Language
OptionalA request middleware sends a visitor landing on
/to their preferred language, based on the locale cookie first, then theAccept-Languageheader. Only/is redirected: deep links are never touched, so shared URLs and crawlers always get the page they asked for.src/i18n/negotiateLocale.tsCopy codeCopy the code to the clipboard
src/start.tsCopy codeCopy the code to the clipboard
A visitor who explicitly picks English in the switcher gets
locale=enin the cookie, so they are never redirected again. On a fully static deployment (step 18),/is served as a file and this middleware does not run, which is fine: the page stays accessible and the switcher does the rest.Keep the use-intl API, Cut the Runtime with Intlayer
OptionalThe benchmark shows the heaviest part of a use-intl setup is the runtime itself (~76 KB gzip). The
@intlayer/use-intlcompat adapter exposes the same API (useTranslations,useFormatter,IntlProvider,createTranslator, ICU plurals,t.rich), but serves it from compiled Intlayer dictionaries: ~6.7 KB instead of ~75.9 KB, 0% locale leak and 0% page leak, with no change to your components.bashCopy codeCopy the code to the clipboard
The Vite plugin aliases
use-intlto the adapter, so existing imports keep working:vite.config.tsCopy codeCopy the code to the clipboard
Your JSON files stay the source of truth thanks to the sync JSON plugin:
intlayer.config.tsCopy codeCopy the code to the clipboard
The adapter is also a smooth migration path: once it runs, you can move components one by one to the native
useIntlayerAPI. See the Intlayer TanStack Start guide.Pre-render Every Locale
OptionalStatic HTML is the fastest page you can serve and the easiest one to index. List every localized path so TanStack Start pre-renders all language versions at build time, plus the sitemap and robots files:
vite.config.tsCopy codeCopy the code to the clipboard
Because the locale switcher renders real links,
crawlLinks: truealso discovers pages you forgot to list.Handle Localized 404 Pages
OptionalThe layout of step 7 already throws
notFound()for unknown locale prefixes. Add a catch-all route so unknown paths inside a locale also render the localized 404, and mark itnoindex: React 19 hoists the<meta>tag into<head>.src/components/NotFound.tsxCopy codeCopy the code to the clipboard
src/routes/{-$locale}/$.tsxCopy codeCopy the code to the clipboard
Access the Locale in Server Functions
OptionalServer functions do not receive route params. Read the locale cookie, and fall back on the
Accept-Languageheader, to send a localized email or store a language preference:src/server/getServerLocale.tsCopy codeCopy the code to the clipboard
To translate inside the server function, combine it with
loadMessagesandcreateTranslatorfromuse-intl.Automate Your Translations Using Intlayer
Optionaluse-intl renders translations, but it does not help you produce them. Intlayer is free and open source, and fills that gap even if you keep use-intl:
- Test missing translations in CI or unit tests. See testing your translations.
- Translate with AI using your own API key and provider:
npx intlayer filltranslates missing keys with the context of your app. See auto fill and the CLI. - Keep your JSON files as the source of truth with the sync JSON plugin.
- Edit content visually with the visual editor and the CMS, so non-developers can update translations.
- Give your AI agent context with the MCP server and agent skills.
- Scan your deployed site for missing
hreflang, wrong canonicals and locale leaks with the scan command.
To discover all features, see why Intlayer.
Frequently Asked Questions
Yes, if you want the next-intl API outside of Next.js. It gives you ICU messages, formatters and good TypeScript support, and it avoids Next.js-specific constraints such as setRequestLocale. The trade-off is weight: the benchmark measures ~76 KB gzip for the runtime, and a naive setup ships every locale and every page to the browser. Load namespaces per route and per locale, as in this guide, to avoid the leaks.
use-intl is the core of next-intl. next-intl adds Next.js integrations on top: a middleware, navigation helpers, getTranslations for Server Components and request configuration. On TanStack Start you use use-intl directly and implement routing with TanStack Router, as shown above.
Use a prefix in the URL. Each language version then has its own URL that search engines can index and users can share. A cookie is still useful to remember an explicit choice, which is what the redirect middleware of step 16 does.
The server and the browser format dates in different time zones. Pass an explicit timeZone to IntlProvider (or the visitor time zone stored in a cookie), so both sides produce the same text.
First, split messages by namespace and load them per route and per locale with import.meta.glob, which removes the locale and page leaks. Then, if the runtime size matters, switch to the @intlayer/use-intl adapter: same API, ~6.7 KB instead of ~75.9 KB in the benchmark.
Call createTranslator inside the route head() function with the messages returned by the route loader, then return title, description, canonical and hreflang links. Step 13 provides a reusable helper.
Comments
No comments yet. Be the first to share your thoughts.
