Using translations in your Tanstack Start Supabase project

Learn how to use translations in Server Components, Client Components, and Server Actions with Makerkit's next-intl-based translation system.

Makerkit uses use-intl for internationalization, abstracted behind the @kit/i18n package. This abstraction ensures future changes to the translation library won't break your code.

Steps to use translations

Learn how to use translations in your Tanstack Start Supabase project.

Translation Architecture

The translation system supports:

  1. Components - Access translations via the Trans component from @kit/ui/trans or the useTranslations hook from use-intl
  2. Server functions & route head() - Build a translator with getTranslator from ~/lib/i18n/translator
  3. Cookie-based locale - The active locale is stored in a locale cookie and resolved server-side on each request

Translation files are stored in packages/i18n/src/messages/{locale}/. The default structure includes:

packages/i18n/src/messages/
└── en/
├── common.json # Shared UI strings
├── auth.json # Authentication flows
├── account.json # Account settings
├── teams.json # Team management
├── billing.json # Billing and subscriptions
└── marketing.json # Marketing pages

Using Translations in Components

Route components render translated strings with the Trans component from @kit/ui/trans.

Using the Trans Component

The Trans component renders translated strings directly in JSX:

import { Trans } from '@kit/ui/trans';
function HomePage() {
return (
<div>
<h1>
<Trans i18nKey="common.homeTabLabel" />
</h1>
<p>
<Trans i18nKey="common.homeTabDescription" />
</p>
</div>
);
}

Import the Trans component from @kit/ui/trans - the Makerkit wrapper reads the use-intl context under the hood.

Using Translations in a Route head()

To set the document <title> from the translation catalog, build a translator with getTranslator (it runs outside React render, so it is synchronous):

import { createFileRoute } from '@tanstack/react-router';
import { getTranslator } from '#/lib/i18n/translator';
export const Route = createFileRoute('/dashboard/')({
head: () => ({
meta: [{ title: getTranslator()('common.homeTabLabel') }],
}),
});

Using Translations in Client Components

Components receive translations through the I18nProvider (a wrapper around use-intl's IntlProvider) mounted in the root providers.

Using the useTranslations Hook

The useTranslations hook provides access to the translation function:

import { useTranslations } from 'use-intl';
export function MyComponent() {
const t = useTranslations();
return (
<button onClick={() => alert(t('common.cancel'))}>
{t('common.cancel')}
</button>
);
}

Specifying Namespaces

Load specific namespaces for scoped access:

import { useTranslations } from 'use-intl';
export function BillingComponent() {
const t = useTranslations('billing');
// Keys without namespace prefix
return <span>{t('subscriptionSettingsTabLabel')}</span>;
}

Using Trans in Client Components

The Trans component also works in Client Components:

import { Trans } from '@kit/ui/trans';
export function WelcomeMessage() {
return (
<p>
<Trans i18nKey="common.signedInAs" />
</p>
);
}

Working with Translation Keys

Key Format

Translation keys use dot notation namespace.keyPath:

// Simple key
<Trans i18nKey="common.cancel" />
// Nested key
<Trans i18nKey="common.routes.home" />
// With namespace in useTranslations
const t = useTranslations('auth');
t('signIn'); // Equivalent to 'auth.signIn'

Interpolation

Pass dynamic values to translations using single braces:

{
"pageOfPages": "Page {page} of {total}",
"showingRecordCount": "Showing {pageSize} of {totalCount} rows"
}
import { Trans } from '@kit/ui/trans';
// Using Trans component
<Trans
i18nKey="common.pageOfPages"
values={{ page: 1, total: 10 }}
/>
// Using t function
const t = useTranslations();
t('common.showingRecordCount', { pageSize: 25, totalCount: 100 });

Nested Translations

Access nested objects with dot notation:

{
"routes": {
"home": "Home",
"account": "Account",
"billing": "Billing"
},
"roles": {
"owner": {
"label": "Owner"
},
"member": {
"label": "Member"
}
}
}
<Trans i18nKey="common.routes.home" />
<Trans i18nKey="common.roles.owner.label" />

HTML in Translations

For translations containing HTML, use the Trans component with components prop:

{
"clickToAcceptAs": "Click the button below to accept the invite as <b>{email}</b>"
}
<Trans
i18nKey="auth.clickToAcceptAs"
values={{ email: user.email }}
components={{ b: <strong /> }}
/>

Common Patterns

Conditional Translations

import { useTranslations, useLocale } from 'use-intl';
const t = useTranslations();
const locale = useLocale();
// Check current language
if (locale === 'en') {
// English-specific logic
}
// Translate with values
const label = t('optional.key', { name: 'World' });

Pluralization

use-intl uses ICU message format for pluralization:

{
"itemCount": "{count, plural, one {# item} other {# items}}"
}
t('common.itemCount', { count: 1 }); // "1 item"
t('common.itemCount', { count: 5 }); // "5 items"

Date and Number Formatting

Use the standard Intl APIs alongside translations:

const locale = useLocale();
const formattedDate = new Intl.DateTimeFormat(locale).format(date);
const formattedNumber = new Intl.NumberFormat(locale).format(1234.56);

Server Functions

Prefer returning translation keys from server functions and rendering them on the client with Trans — this keeps translation concerns on the client. When you do need to resolve a string server-side, build a translator with getTranslator:

import { createServerFn } from '@tanstack/react-start';
import { authActionMiddleware } from '@kit/action-middleware';
import { getTranslator } from '#/lib/i18n/translator';
export const myFunction = createServerFn({ method: 'POST' })
.middleware(authActionMiddleware)
.handler(async () => {
const t = getTranslator();
// Use translations
const message = t('common.genericServerError');
return { error: message };
});

Environment Variables

Configure language behavior with these environment variables:

# Default language (fallback when user preference unavailable)
VITE_DEFAULT_LOCALE=en

The active locale is resolved server-side on each request (via the detectLocale server function in apps/web/src/lib/i18n/i18n.functions.ts) using the following precedence:

  1. The locale cookie (set when the user picks a language)
  2. The browser's Accept-Language header
  3. Falls back to VITE_DEFAULT_LOCALE

Troubleshooting

Missing Translation Warning

If you see a missing translation warning, check:

  1. The key exists in your translation file
  2. All interpolation values are provided
  3. The namespace is registered in the message registry at packages/i18n/src/messages/index.ts

Translations Not Updating

If translations don't update after editing JSON files:

  1. Restart the development server
  2. Clear browser cache
  3. Check for JSON syntax errors in translation files

Frequently Asked Questions

How do I switch languages programmatically?
Call useChangeLocale() from @kit/i18n/navigation with the new locale. It writes the locale cookie and reloads so the server re-resolves the locale on the next render.
Why are my translations not showing?
Check that the namespace is registered in the message registry in packages/i18n/src/messages/index.ts, the JSON file exists in packages/i18n/src/messages/{locale}/, and verify the key uses dot notation (namespace.key not namespace:key).
Can I use translations in a server function?
Yes, import getTranslator from ~/lib/i18n/translator and call it to build a synchronous t() function. Prefer returning translation keys from server functions and rendering them on the client with Trans where possible.
What's the difference between Trans component and useTranslations hook?
Trans is a React component that renders translated strings directly in JSX, supporting interpolation and HTML. useTranslations is a hook that returns a t() function for programmatic access to translations, useful for attributes, conditionals, or non-JSX contexts.
How do I handle missing translations during development?
Missing translations log warnings to the console. Use [TODO] prefixes in your JSON values to make untranslated strings searchable. The system falls back to the key name if no translation is found.