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.1026/09/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 internationalise your TanStack Start application using Lingui in 2026
Table of Contents
What is Lingui?
Lingui is an i18n library built around macros and message extraction. You write the source text directly in your components ( t`Hello` , <Trans>Hello</Trans>), lingui extract collects every message into catalogues (PO files by default), translators fill them, and the Vite plugin compiles them to compact JavaScript. Messages use ICU MessageFormat, so plurals and selects are supported.
TanStack Start does not ship an i18n layer, so this guide wires Lingui into it from scratch:
- Macros compiled by Babel through
@rolldown/plugin-babel(required with@vitejs/plugin-reactv6 and Vite 8). - Locale routing with an optional
{-$locale}segment (/about,/fr/about). - One catalogue per locale, loaded on demand, and an
I18ninstance per render so concurrent SSR requests never share a locale. - Complete multilingual SEO: translated
<title>and description, canonical URL,hreflangwithx-default, Open Graph locales, JSON-LD, sitemap,robots.txt, pre-rendering and localised 404 pages.
Looking for another stack? See the TanStack Start + use-intl guide, the TanStack Start + Paraglide guide, or the TanStack Start + Intlayer guide.
Using Next.js? See the Next.js + Lingui guide. Comparing libraries? Read Lingui vs Intlayer.
What the benchmark says about Lingui 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 internationalisation 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 @lingui/core@6.6.0, 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% |
| Lingui (setup of this guide) | 56.7 KB | 115.2 KB | 9.3% | 0% |
@intlayer/lingui (compat) | 9.8 KB | 136.7 KB | 9.9% | 0% |
react-intlayer (native Intlayer) | 4.5 KB | 126.8 KB | 0% | 0% |
What to take away:
- Load one catalogue per locale, on demand. It keeps pages close to the base app size.
- The runtime stays heavy (~57 KB gzip). The
@intlayer/linguicompat adapter (step 16) keeps your macros and cuts it to ~10 KB.
See the full data: TanStack Start benchmark report, and the benchmark repository.
Feature comparison on TanStack Start
How Lingui 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 | ❌ Centralised 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> |
| Localised routing | ✅ Built-in | ❌ Manual {-$locale} | ✅ urlPatterns + router rewrite | ❌ Manual {-$locale} |
| Locale switch without reload | ✅ Yes | ✅ Yes | ❌ Full page reload | ✅ Yes |
| Pluralisation | ✅ 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 | ⚠️ Localised 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: use-intl, Paraglide JS, and Intlayer.
Practices you should follow
- Set
langanddiron<html>from the route locale, so they are correct in the server HTML. - Keep one URL per locale with a prefix, so every language version is indexable.
- Create one
I18ninstance per locale, never mutate a global one during SSR: two concurrent requests would overwrite each other's locale. - Load only the active catalogue, never import all of them in client code.
- Pick one macro style (
useLingui+tin components,msgfor lazy descriptors) and stick to it. Mixingt,i18n._,i18n.tand<Trans>makes the code harder to read for humans and AI assistants. - Run
lingui extractin CI so a new message never ships untranslated. - 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 internationalisation and SEO and the hreflang guide.
Step-by-Step Guide to Set Up Lingui in a TanStack Start Application
Here's the project structure we'll be creating:
Copy the code to the clipboard
Install Dependencies
bashCopy codeCopy the code to the clipboard
- @lingui/core / @lingui/react: runtime,
I18nProviderand the macros (@lingui/core/macro,@lingui/react/macro). - @lingui/cli:
lingui extractto collect messages into catalogues. - @lingui/vite-plugin: compiles
.pocatalogues on import, solingui compileis not needed. - @lingui/babel-plugin-lingui-macro + @rolldown/plugin-babel: transform the macros at build time.
- @lingui/core / @lingui/react: runtime,
Centralize Your Locale Configuration
The default locale stays unprefixed (
/about), other locales are prefixed (/fr/about).src/i18n/config.tsCopy codeCopy the code to the clipboard
Configure Lingui
The Lingui config reuses the same locale list, so the catalogues, the router and the sitemap never disagree.
lingui.config.tsCopy codeCopy the code to the clipboard
Add the extraction scripts:
package.jsonCopy codeCopy the code to the clipboard
i18n:checkfails in CI when a component contains a message that was not extracted and committed.Configure Vite
With
@vitejs/plugin-reactv6, Babel is no longer built in.@rolldown/plugin-babelruns the Lingui macro plugin, andlinguiTransformerBabelPresetonly processes files that import a macro, which keeps builds fast.vite.config.tsCopy codeCopy the code to the clipboard
Load Catalogs per Locale
The template literal in
import()lets Vite emit one chunk per catalogue, and the Lingui plugin compiles the.pofile into it. A French visitor downloads the French catalogue only.The compiled messages are plain data, so they can be returned by a route loader, serialised into the HTML, and reused on hydration.
src/i18n/lingui.tsCopy codeCopy the code to the clipboard
For TypeScript to accept the
.poimport, declare the module once:src/i18n/po.d.tsCopy codeCopy the code to the clipboard
Create the Root Document
The root route reads the optional locale param to set
langanddiron the server-rendered<html>.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. The layout rejects unknown prefixes, loads the catalogue of the current locale, and provides a dedicatedI18ninstance.src/routes/{-$locale}/route.tsxCopy codeCopy the code to the clipboard
Utilize Translations in Your Pages
Write the source text in the component. The macros turn it into message IDs at build time, and
lingui extractpicks it up.<Trans>for JSX content, including nested elements;useLingui().tfor strings (attributes, props);<Plural>for ICU plurals.
src/routes/{-$locale}/about.tsxCopy codeCopy the code to the clipboard
The dynamic
import()of a catalogue is cached by the module system, so callingloadI18nin several loaders does not download the catalogue twice.Extract and Translate Your Messages
Run the extraction. Lingui writes every message into each locale catalogue:
bashCopy codeCopy the code to the clipboard
Then translate the
msgstrof each entry:src/locales/fr/messages.poCopy codeCopy the code to the clipboard
src/locales/es/messages.poCopy codeCopy the code to the clipboard
By default, message IDs are hashes of the source text: changing the English text creates a new message. Use explicit IDs (
<Trans id="about.title">About us</Trans>) for texts that change often.Build a Localized Link Component
OptionalEvery route lives under
{-$locale}, so links must carry the current locale param.src/components/LocalizedLink.tsxCopy codeCopy the code to the clipboard
Change the Language of Your Content
OptionalRender the switcher as links, so crawlers find every language version.
to="."keeps the current page and replaces the locale param. The loader of the locale layout then fetches the new catalogue.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>and description, a self-referencing canonical, onehreflangper locale plusx-default, Open Graph locales, and JSON-LD withinLanguage. The metadata is translated in the loader (step 8), and this helper builds the rest:src/i18n/seo.tsCopy codeCopy the code to the clipboard
Internationalize Your Sitemap and robots.txt
OptionalThe sitemap lists every URL of every locale, each entry declaring all its alternates with
xhtml:link.robots.txtblocks private routes in every language and points to the sitemap. Removepublic/robots.txtif the starter created one.src/routes/sitemap[.]xml.tsCopy codeCopy the code to the clipboard
src/routes/robots[.]txt.tsCopy codeCopy the code to the clipboard
Pre-render Every Locale
OptionalList every localised path so TanStack Start pre-renders all language versions at build time:
vite.config.tsCopy codeCopy the code to the clipboard
Redirect First-Time Visitors and Handle 404 Pages
OptionalA request middleware sends a visitor landing on
/to their preferred language (cookie first, thenAccept-Language). Deep links are never redirected, so crawlers and shared URLs 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
For 404 pages, a catch-all route renders the localised
notFoundComponentof the layout. Mark itnoindex: React 19 hoists the<meta>into<head>.src/components/NotFound.tsxCopy codeCopy the code to the clipboard
src/routes/{-$locale}/$.tsxCopy codeCopy the code to the clipboard
Keep Your Macros, Cut the Runtime with Intlayer
OptionalThe
@intlayer/linguicompat adapter keeps your source untouched: the macros compile exactly as before, and the resultingi18n._(),useLingui()and<Trans>calls are served by compiled Intlayer dictionaries. In the benchmark, the runtime drops from ~56.7 KB to ~9.8 KB gzip.bashCopy codeCopy the code to the clipboard
Add the plugin after the macro transform, so it aliases
@lingui/coreand@lingui/reactto the adapter:vite.config.tsCopy codeCopy the code to the clipboard
Catalogues are synchronised with the sync JSON plugin (JSON catalogues) or the sync PO plugin (PO catalogues). See the full setup in the Lingui compat guide, and a side-by-side comparison in Lingui vs @intlayer/lingui.
Automate Your Translations Using Intlayer
OptionalLingui extracts messages, but filling dozens of catalogues by hand is where most of the time goes. Intlayer is free and open source, and its tooling works alongside Lingui:
- Translate with AI using your own API key and provider. See auto fill and the CLI.
- Keep your PO files as the source of truth with the sync PO plugin.
- Test missing translations in CI. See testing your translations.
- Audit your deployed site for missing
hreflang, wrong canonicals and locale leaks with the scan command.
Frequently Asked Questions
Yes. Lingui has no dedicated TanStack Start integration, but its Vite plugin and Babel macro plugin work as is. The two points to get right are running the macros through @rolldown/plugin-babel (Vite 8 and @vitejs/plugin-react v6 no longer include Babel), and creating an I18n instance per locale instead of activating a global one during SSR.
On the server, one process renders many requests at the same time. Calling i18n.activate("fr") on a shared object would switch the language of a request rendering in English in parallel. setupI18n creates an isolated instance per locale, which is safe.
No. @lingui/vite-plugin compiles .po catalogues when they are imported. You only run lingui extract to collect new messages.
Declare them with the msg macro, and translate them in the route loader with i18n._(msg`...`). The loader returns plain strings, so head() stays synchronous and the values are serialised for hydration. Step 8 and step 12 show the full setup.
The benchmark measures ~56.7 KB gzip for the runtime. With one catalogue per locale loaded on demand, pages weigh ~115 KB against 111 KB without i18n. Importing every catalogue statically raises it to ~152 KB.
Yes. The @intlayer/lingui adapter keeps the macros and swaps the runtime. You can then move components to useIntlayer one at a time. See the compat adapters.
Comments
No comments yet. Be the first to share your thoughts.
