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 path
const t = useTranslations('settings.personalSettings.name');
const title = t('title');
// Option 2: Access from a higher namespace with dot notation
const 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

PropTypeDescription
i18nKeystringTranslation key with namespace prefix (e.g., 'auth.login.title')
valuesobjectValues to interpolate into the translation
componentsobjectReact elements or functions for rich text
defaultsReactNodeFallback content if translation not found
nsstringOverride 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).

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.

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 →