Using Translations
How to use translations in components, in route metadata, and with the Trans component.
This guide covers how to access and display translations in your components using use-intl.
In Components
Components — whether they render on the server during SSR or on the client — read translations through the useTranslations hook from use-intl. The hook reads from the context provided by I18nProvider in the root providers.
import { useTranslations } from 'use-intl';function SettingsPage() { const t = useTranslations('settings'); return ( <div> <h1>{t('pageTitle')}</h1> <p>{t('pageDescription')}</p> </div> );}Multiple Namespaces
Load multiple namespaces by calling useTranslations multiple times:
function DashboardPage() { const tCommon = useTranslations('common'); const tBilling = useTranslations('billing'); return ( <div> <h1>{tCommon('dashboardTabLabel')}</h1> <p>{tBilling('currentPlan')}</p> </div> );}Accessing Nested Keys
For deeply nested translations, you can either pass the full path to useTranslations or use dot notation in the key:
// Option 1: Namespace includes the pathconst t = useTranslations('settings.personalSettings.name');const title = t('title');// Option 2: Access from a higher namespace with dot notationconst t = useTranslations('settings');const title = t('personalSettings.name.title');Outside React Render (Route Metadata)
There are no React Server Components in TanStack Start. When you need a translation outside the React render tree — most commonly in a route's head() for the document title — use the synchronous getTranslator helper from #/lib/i18n/translator.ts. It returns a function that takes a dotted message key (no namespace argument) and returns a string.
import { createFileRoute } from '@tanstack/react-router';import { getTranslator } from '#/lib/i18n/translator.ts';export const Route = createFileRoute('/_authenticated/settings/preferences')({ head: () => { const t = getTranslator(); return { meta: [{ title: t('settings.preferences.pageTitle') }], }; }, component: PreferencesPage,});You can also pass loader data into the meta and add a description:
import { getTranslator } from '#/lib/i18n/translator.ts';export const Route = createFileRoute('/_public/changelog/')({ head: ({ loaderData }) => { const t = getTranslator(); return { meta: [ { title: t('marketing.changelog') }, { name: 'description', content: t('marketing.changelogSubtitle') }, ], }; },});getTranslator(locale?) builds the translator from the bundled messages for the locale (defaulting to the default locale), so it is synchronous and safe to call inside head().
The Trans Component
For translations that include JSX elements or rich formatting, use the Trans component from @kit/ui/trans:
import { Trans } from '@kit/ui/trans';// Basic usage<Trans i18nKey="auth.welcomeMessage" />// With interpolated values<Trans i18nKey="common.greeting" values={{ name: user.name }}/>// With components for rich text<Trans i18nKey="auth.termsAgreement" components={{ TermsLink: <a href="/terms" className="underline" />, PrivacyLink: <a href="/privacy" className="underline" />, }}/>Trans Component Props
| Prop | Type | Description |
|---|---|---|
i18nKey | string | Translation key with namespace prefix (e.g., 'auth.login.title') |
values | object | Values to interpolate into the translation |
components | object | React elements or functions for rich text |
defaults | ReactNode | Fallback content if translation not found |
ns | string | Override namespace (optional) |
How Components Work
The components prop accepts either React elements or render functions:
// React element - children are replaced with translation content<Trans i18nKey="auth.termsAgreement" components={{ TermsLink: <a href="/terms" className="underline" />, }}/>// Render function - full control over rendering<Trans i18nKey="common.highlight" components={{ bold: (chunks) => <strong className="font-bold">{chunks}</strong>, }}/>For the translation:
{ "termsAgreement": "By signing up, you agree to our <TermsLink>Terms</TermsLink>.", "highlight": "This is <bold>important</bold> text."}Translation Patterns
Flat Keys
Simple key-value pairs for straightforward translations:
{ "signUp": "Sign Up", "signIn": "Sign In", "forgotPassword": "Forgot Password?"}const t = useTranslations('auth');t('signUp'); // "Sign Up"Nested Keys
Organized hierarchical structure for related translations:
{ "routes": { "account": "Account", "members": "Members", "billing": "Billing" }, "roles": { "owner": { "label": "Owner" }, "member": { "label": "Member" } }}const t = useTranslations('common');t('routes.account'); // "Account"t('roles.owner.label'); // "Owner"Interpolation
Dynamic values are inserted using curly braces:
{ "greeting": "Hello, {name}!", "itemCount": "You have {count} items", "planRenewal": "Renews every {interval} at {price}"}t('greeting', { name: 'John' }); // "Hello, John!"t('itemCount', { count: 5 }); // "You have 5 items"t('planRenewal', { interval: 'month', price: '$9' });Pluralization
Use ICU message format for count-dependent text:
{ "invitations": "{count} {count, plural, one {invitation} other {invitations}} pending"}t('invitations', { count: 1 }); // "1 invitation pending"t('invitations', { count: 5 }); // "5 invitations pending"More complex plural rules:
{ "items": "{count, plural, =0 {No items} one {# item} other {# items}}"}Rich Text
For translations that need markup, use XML-like tags that map to components:
{ "termsAgreement": "By signing up, you agree to our <TermsLink>Terms of Service</TermsLink> and <PrivacyLink>Privacy Policy</PrivacyLink>."}<Trans i18nKey="auth.termsAgreement" components={{ TermsLink: <a href="/terms" className="underline" />, PrivacyLink: <a href="/privacy" className="underline" />, }}/>Getting Current Locale
In Components
import { useLocale } from 'use-intl';function MyComponent() { const locale = useLocale(); // locale = 'en', 'es', 'fr', etc.}Outside Components
The active locale is resolved on the server and exposed through the root route context (it's also used to set <html lang>):
import { Route as RootRoute } from '#/routes/__root';function MyComponent() { const { locale } = RootRoute.useRouteContext(); // locale = 'en', 'es', 'fr', etc.}On the server you can also call the detectLocale() server function directly (see the Overview).
Navigation
Use TanStack Router's Link (re-exported from @kit/i18n/navigation) for internal navigation — the router rewrite localizes its to automatically, so links carry the active locale's prefix with no extra work. For dynamic/string hrefs that can't be typed against the route tree (e.g. CMS URLs), use LocalizedLink from @kit/ui/localized-link. Never use a raw <a> for internal links — it bypasses the router and won't be localized. The application surface (dashboard/settings/admin) is never prefixed regardless.
Link Component
import { Link } from '@tanstack/react-router';<Link to="/settings">Settings</Link>// With params for dynamic routes<Link to="/blog/$slug" params={{ slug: 'hello-world' }}>Read post</Link>Programmatic Navigation
import { useRouter } from '@tanstack/react-router';function NavigationExample() { const router = useRouter(); const handleClick = () => { router.navigate({ to: '/dashboard' }); };}Redirects
Use redirect from @tanstack/react-router inside loaders or beforeLoad:
import { redirect } from '@tanstack/react-router';export const Route = createFileRoute('/_authenticated/protected')({ beforeLoad: async () => { const session = await getSession(); if (!session) { throw redirect({ to: '/auth/sign-in' }); } },});Client-side redirects are localized automatically by the router rewrite. For server-side (SSR) redirects to a public or auth route, localize the target explicitly with localizeUrl(target, locale), sourcing the locale from route context; redirects to the app surface need no change. See apps/web/src/lib/auth/guards.ts.
Checking if Translation Exists
Use the has method to check if a translation key exists:
const t = useTranslations('common');if (t.has('newFeature.title')) { return <h1>{t('newFeature.title')}</h1>;}return <h1>Default Title</h1>;This is useful for conditional rendering based on translation availability.
Date and Time Formatting
use-intl provides locale-aware date and time formatting via the useFormatter hook.
import { useFormatter } from 'use-intl';function DateDisplay({ date }: { date: Date }) { const format = useFormatter(); return ( <div> {/* Full date */} <p>{format.dateTime(date, { dateStyle: 'full' })}</p> {/* Relative time */} <p>{format.relativeTime(date)}</p> {/* Custom format */} <p>{format.dateTime(date, { year: 'numeric', month: 'long', day: 'numeric' })}</p> </div> );}Number Formatting
Format numbers according to locale conventions:
import { useFormatter } from 'use-intl';function PriceDisplay({ amount, currency }: { amount: number; currency: string }) { const format = useFormatter(); return ( <span> {format.number(amount, { style: 'currency', currency: currency })} </span> );}Next: Managing Translations →