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
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 internationalize your TanStack Start application using Paraglide JS in 2026
Table of Contents
What is Paraglide JS?
Paraglide JS (by inlang) is a compiler-based i18n library. Instead of shipping a runtime that looks up keys in a JSON object, it compiles each message into a typed JavaScript function (m.about_title()). Unused messages can be dropped by the bundler, and a typo in a key is a compile error.
Paraglide is the i18n approach used in the official TanStack Router examples, and it integrates with TanStack Start through three pieces:
- a Vite plugin that compiles messages and the runtime into
src/paraglide; - a server middleware that resolves the locale of each request;
- a router rewrite that maps localized URLs (
/fr/about) to your route tree (/about), so you don't need a$localesegment.
This guide sets up all three, then covers everything Paraglide leaves to you: lang and dir, locale switcher, translated metadata, canonical, hreflang with x-default, Open Graph, JSON-LD, sitemap, robots.txt, pre-rendering and localized 404 pages.
Looking for another stack? See the TanStack Start + use-intl guide, the TanStack Start + Lingui guide, or the TanStack Start + Intlayer guide.
Comparing the two compiler-based approaches? Read is Intlayer lighter than Paraglide?.
What the benchmark says about Paraglide 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 @inlang/paraglide-js@2.15.1, 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 | Page load |
|---|---|---|---|---|---|
| No i18n (base app) | - | 111.0 KB | 0% | 0% | 15.7 ms |
| Paraglide JS | 1.8 KB | 125.1 KB | 49.7% | 0% | 22.1 ms |
react-intlayer | 4.5 KB | 126.8 KB | 0% | 0% | 14.8 ms |
use-intl | 75.9 KB | 128.7 KB | 0% | 0% | 17.4 ms |
| Lingui | 56.7 KB | 120.2 KB | 8.6% | 0% | 21.9 ms |
What to take away:
- The runtime is tiny, and pages do not leak. The runtime is generated for your configuration, and messages are imported where they are used.
- Locales leak. Each message function contains every locale, so about half of the translated strings shipped to a page are in languages the visitor does not use. The more locales you add, the bigger this share gets.
- Page load is the slowest of the group, partly because the locale is resolved through strategies on each call rather than read from a React context.
See the full data: TanStack Start benchmark report, and the benchmark repository.
Feature comparison on TanStack Start
How Paraglide JS 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, use-intl, and Intlayer.
Practices you should follow
- Set
langanddiron<html>from the resolved locale, on the server. - Keep one URL per locale with a prefix strategy (
/fr/about), so every language version is indexable. - Put
urlfirst in your locale strategy, so the URL is the source of truth, and crawlers get the page they asked for. - Use flat, descriptive message keys (
about_title) that map cleanly to function names. - Commit your
messages/*.json, not the generatedsrc/paraglidefolder, to avoid merge conflicts on generated files. - 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, so crawlers discover every language.
See our guide on internationalization and SEO and the hreflang guide.
Step-by-Step Guide to Set Up Paraglide JS in a TanStack Start Application
Here's the project structure we'll be creating:
Copy the code to the clipboard
Notice there is no $locale folder: the router rewrite removes the prefix before route matching.
Install Dependencies
Start from a TanStack Start project, then initialize Paraglide. The init command creates
project.inlang/settings.json, a firstmessages/en.jsonand installs the package.bashCopy codeCopy the code to the clipboard
- @inlang/paraglide-js: the compiler and its Vite plugin. There is no runtime package to install: the runtime is generated into your project.
Configure Your Locales
project.inlang/settings.jsonis the single source of truth for locales. The message format plugin reads one JSON file per locale.project.inlang/settings.jsonCopy codeCopy the code to the clipboard
Configure the Vite Plugin and the URL Strategy
The plugin compiles messages on every change. Three options matter for TanStack Start:
strategy: the ordered list of places to read the locale from.urlfirst makes the URL the source of truth.cookieandpreferredLanguageare used by the middleware when the URL does not decide.urlPatterns: how a locale maps to a URL. Non-default locales are listed first, because the first matching pattern wins. Here the default locale stays unprefixed (/about), and other locales are prefixed (/fr/about).outputStructure: "message-modules": one module per message, which lets the bundler drop messages a page does not import.
vite.config.tsCopy codeCopy the code to the clipboard
Add the generated folder to
.gitignore. It is rebuilt ondevandbuild:.gitignoreCopy codeCopy the code to the clipboard
Create Your Translation Files
Each key becomes a function exported from
src/paraglide/messages. Flat, snake_case keys give the cleanest function names. Variables use{name}placeholders.messages/en.jsonCopy codeCopy the code to the clipboard
messages/fr.jsonCopy codeCopy the code to the clipboard
Plurals use the variants syntax of the inlang message format:
messages/en.jsonCopy codeCopy the code to the clipboard
Add the Server Middleware
The middleware resolves the locale of each request with your strategy, and makes it available to
getLocale()for the whole server render, through anAsyncLocalStoragescope. That is what makes concurrent requests in different languages safe.In TanStack Start, wrap the default server entry:
src/server.tsCopy codeCopy the code to the clipboard
Rewrite Localized URLs in the Router
TanStack Router's
rewriteoption translates URLs at the boundary of the router:- input:
/fr/aboutis de-localized to/aboutbefore matching, so a singleabout.tsxroute serves every language; - output: every generated
href(links, redirects, navigation) is localized for the active locale, so<Link to="/about">renders/fr/abouton a French page.
src/router.tsxCopy codeCopy the code to the clipboard
Because links are localized by the rewrite, you don't need a custom
LocalizedLinkcomponent: use TanStack Router'sLinkas usual.- input:
Create the Root Document
getLocale()returns the locale resolved by the middleware on the server, and the locale from the URL in the browser, solanganddirare identical in the server HTML and after hydration.src/i18n/config.tsCopy codeCopy the code to the clipboard
src/routes/__root.tsxCopy codeCopy the code to the clipboard
Utilize Translations in Your Pages
Messages are plain functions: import
m, call the function, pass variables as an object. Everything is typed, including the variables.src/routes/index.tsxCopy codeCopy the code to the clipboard
src/routes/about.tsxCopy codeCopy the code to the clipboard
A message function also accepts an explicit locale:
m.about_title({}, { locale: "fr" }). It is useful in server code that renders a language other than the one of the request, such as emails.Change the Language of Your Content
OptionalRender the switcher as links with
localizeHref, so crawlers discover every language.setLocalestores the choice in the cookie and reloads the page in the new language: a full reload is the expected Paraglide behavior, because message functions read the locale on each call instead of subscribing to a React state.src/components/LocaleSwitcher.tsxCopy codeCopy the code to the clipboard
Internationalize Your Metadata
OptionalEach language version can rank on its own, provided every page exposes:
- a translated
<title>anddescription; - a canonical URL pointing to itself;
- one
hreflangalternate per locale, plusx-default; - Open Graph
og:locale,og:locale:alternateandog:url; - JSON-LD with
inLanguage.
Paraglide's
localizeUrlbuilds the alternate URLs from yoururlPatterns, so they can never drift from the real routing:src/i18n/seo.tsCopy codeCopy the code to the clipboard
- a translated
Internationalize Your Sitemap
OptionalA multilingual sitemap lists every URL of every locale, and each entry declares all its alternates with
xhtml:link:src/routes/sitemap[.]xml.tsCopy codeCopy the code to the clipboard
Internationalize Your robots.txt
OptionalPrivate routes exist in every language, so
Disallowrules must cover every localized path. Removepublic/robots.txtif the starter created one, then serve it from a route:src/routes/robots[.]txt.tsCopy codeCopy the code to the clipboard
Pre-render Every Locale
OptionalList the localized path of every page so TanStack Start pre-renders all language versions.
localizeHrefis generated code with no browser dependency, so it can run invite.config.ts, but the file only exists after a first compilation. Listing the paths by hand, as below, avoids that ordering issue:vite.config.tsCopy codeCopy the code to the clipboard
Because the switcher renders real links,
crawlLinks: truealso discovers pages you forgot to list.Handle Localized 404 Pages
OptionalWith the rewrite,
/fr/does-not-existis matched as/does-not-exist, andgetLocale()still returnsfr, so the rootnotFoundComponentof step 7 renders in French. A catch-all route makes sure deep paths also reach it. Mark the pagenoindex: React 19 hoists the<meta>into<head>.src/components/NotFound.tsxCopy codeCopy the code to the clipboard
src/routes/$.tsxCopy codeCopy the code to the clipboard
Access the Locale in Server Functions
OptionalServer functions run inside the Paraglide middleware scope, so
getLocale()works there too:src/server/sendWelcomeEmail.tsCopy codeCopy the code to the clipboard
Compare with Intlayer
OptionalThere is no drop-in adapter from Paraglide to Intlayer, because both follow the same idea: compile content at build time and ship as little runtime as possible. The differences are in what reaches the browser and how content is organized:
- Locales: Intlayer loads dynamic dictionaries per locale (0% locale leak in the benchmark), while each Paraglide message function carries every locale (49.7%).
- Content organization: content can live in
.content.tsfiles next to each component, or in centralized files. See per-component vs centralized i18n. - Locale switch: content is read from a React context, so switching locale re-renders without a reload.
- Generated code: nothing is generated inside
src, so there is nothing to regenerate before a commit.
If you come from another library rather than Paraglide, the compat adapters keep the
use-intl,next-intl,react-i18next,react-intlor Lingui API and swap the runtime.See is Intlayer lighter than Paraglide? and the Intlayer TanStack Start guide.
Automate Your Translations Using Intlayer
OptionalParaglide renders translations, but it does not help you produce them. Intlayer is free and open source, and its tooling helps even on a Paraglide project:
- Translate with AI using your own API key and provider. See auto fill and the CLI.
- Keep your JSON files as the source of truth with the sync JSON plugin.
- Test missing translations in CI. See testing your translations.
- Scan your deployed site for missing
hreflang, wrong canonicals and locale leaks with the scan command.
Frequently Asked Questions
It is a solid one: it is used in the official TanStack Router examples, it has the smallest runtime of the benchmark (~1.8 KB gzip), and messages are fully typed. The trade-offs are that every message function contains all locales, which leaks roughly half of the translated strings to visitors of other languages, and that switching locale reloads the page.
No. The router rewrite removes the locale prefix before route matching and adds it back to generated links, so a single about.tsx serves /about, /fr/about and /es/about.
Message functions read the locale when they are called, they are not subscribed to a React state. setLocale therefore reloads the page by default, so every message re-renders in the new language. You can pass { reload: false }, but then you must re-render the tree yourself.
It is better not to. The folder is regenerated on every dev and build, and committing it causes merge conflicts on generated files. Commit messages/*.json and project.inlang/settings.json instead.
Use localizeUrl to build one absolute URL per locale in the route head(), and add an x-default pointing to the base locale. Step 10 provides a reusable helper, and step 11 adds the same alternates to the sitemap.
Unused messages are dropped when you use outputStructure: "message-modules", so other pages' content does not leak. Unused locales are not: each message function contains every translation, which is why the benchmark measures a 49.7% locale leak.
Comments
No comments yet. Be the first to share your thoughts.
