TanStack Start
Setup
Basic TanStack Start setup with Better i18n
This guide covers the basic setup for Better i18n with TanStack Start.
Step 1: Create i18n Configuration
Create a minimal configuration file:
export const i18nConfig = {
project: "your-org/your-project",
defaultLocale: "en",
} as constAvailable languages are automatically fetched from the Better i18n CDN manifest. No need to maintain a static list!
Step 2: Setup Root Layout
Load messages on the server and provide them to the client:
import {
createRootRouteWithContext,
Outlet,
HeadContent,
Scripts,
} from "@tanstack/react-router"
import { BetterI18nProvider } from "@better-i18n/use-intl"
import { getMessages } from "@better-i18n/use-intl/server"
import { i18nConfig } from "../i18n.config"
interface RouterContext {
locale: string
}
export const Route = createRootRouteWithContext<RouterContext>()({
loader: async ({ context }) => {
const locale = context.locale || i18nConfig.defaultLocale
const messages = await getMessages({
project: i18nConfig.project,
locale,
})
return { messages, locale }
},
component: RootComponent,
})
function RootComponent() {
const { messages, locale } = Route.useLoaderData()
return (
<html lang={locale}>
<head>
<HeadContent />
</head>
<body>
<BetterI18nProvider
project={i18nConfig.project}
locale={locale}
messages={messages}
timeZone="UTC"
> // [!code ++]
<Outlet />
</BetterI18nProvider> // [!code ++]
<Scripts />
</body>
</html>
)
}Timezone Requirement: use-intl v4+ requires a timeZone during SSR to prevent markup mismatches. Use "UTC" as a safe default.
BetterI18nProvider Props
| Prop | Type | Default | Description |
|---|---|---|---|
project | string | Required | Project identifier (org/project) |
locale | string | Required | Current locale (from router context) |
messages | Messages | — | Pre-loaded SSR messages from getMessages() |
timeZone | string | System tz | IANA timezone — set explicitly to avoid SSR hydration mismatches |
onLocaleChange | (locale: string) => void | — | Locale switch callback |
storage | TranslationStorage | — | Persistent translation cache adapter |
staticData | Record<string, Messages> | — | Offline fallback translations |
fetchTimeout | number | 10000 | CDN fetch timeout in ms |
retryCount | number | 1 | Retry attempts on CDN failure |
Step 3: Use Translations
Use the useTranslations hook in any component:
import { createFileRoute } from "@tanstack/react-router"
import { useTranslations } from "@better-i18n/use-intl"
export const Route = createFileRoute("/")({
component: HomePage,
})
function HomePage() {
const t = useTranslations("home")
return (
<div>
<h1>{t("title")}</h1>
<p>{t("description")}</p>
</div>
)
}Step 4: Add Language Switcher
Use the built-in component or build your own:
import { LanguageSwitcher } from "@better-i18n/use-intl"
function Header() {
return (
<header>
<nav>
<LanguageSwitcher className="locale-select" />
</nav>
</header>
)
}Interpolation
Pass dynamic values to translations:
function Greeting({ user }) {
const t = useTranslations("greeting")
return <p>{t("welcome", { name: user.name })}</p>
}{
"greeting": {
"welcome": "Welcome back, {name}!"
}
}Date & Number Formatting
Use the useFormatter hook:
import { useFormatter } from "@better-i18n/use-intl"
function ProductPrice({ price, date }) {
const format = useFormatter()
return (
<div>
<span>{format.number(price, { style: "currency", currency: "USD" })}</span>
<time>{format.dateTime(date, { dateStyle: "medium" })}</time>
</div>
)
}