Better I18NBetter I18N
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:

app/i18n.config.ts
export const i18nConfig = { 
  project: "your-org/your-project", 
  defaultLocale: "en", 
} as const

Available 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:

app/routes/__root.tsx
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

PropTypeDefaultDescription
projectstringRequiredProject identifier (org/project)
localestringRequiredCurrent locale (from router context)
messagesMessagesPre-loaded SSR messages from getMessages()
timeZonestringSystem tzIANA timezone — set explicitly to avoid SSR hydration mismatches
onLocaleChange(locale: string) => voidLocale switch callback
storageTranslationStoragePersistent translation cache adapter
staticDataRecord<string, Messages>Offline fallback translations
fetchTimeoutnumber10000CDN fetch timeout in ms
retryCountnumber1Retry attempts on CDN failure

Step 3: Use Translations

Use the useTranslations hook in any component:

app/routes/index.tsx
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>
}
Translation file
{
  "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>
  )
}

Next Steps

On this page