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:
- Components - Access translations via the
Transcomponent from@kit/ui/transor theuseTranslationshook fromuse-intl - Server functions & route
head()- Build a translator withgetTranslatorfrom~/lib/i18n/translator - Cookie-based locale - The active locale is stored in a
localecookie 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 pagesUsing 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 useTranslationsconst 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 functionconst 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 languageif (locale === 'en') { // English-specific logic}// Translate with valuesconst 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=enThe 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:
- The
localecookie (set when the user picks a language) - The browser's
Accept-Languageheader - Falls back to
VITE_DEFAULT_LOCALE
Troubleshooting
Missing Translation Warning
If you see a missing translation warning, check:
- The key exists in your translation file
- All interpolation values are provided
- 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:
- Restart the development server
- Clear browser cache
- Check for JSON syntax errors in translation files
Frequently Asked Questions
How do I switch languages programmatically?
Why are my translations not showing?
Can I use translations in a server function?
What's the difference between Trans component and useTranslations hook?
How do I handle missing translations during development?
Related Documentation
- Adding Translations - Add new languages and namespaces
- Language Selector - Let users change their language
- Email Translations - Translate email templates