Using translations in your Next.js Supabase project

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

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

Translation Architecture

The translation system supports:

  1. Server Components (RSC) - Initialize translations per-request with withI18n
  2. Client Components - Access translations via React context with useTranslation
  3. Server-side rendering - Hydrate translations from server to client

Translation files are stored in apps/web/public/locales/[language]/[namespace].json. The default structure includes:

apps/web/public/locales/
└── 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 Server Components

Server Components require explicit initialization because they render in parallel. Without initialization, translations may not be available when the component renders.

Wrapping Pages with withI18n

Always wrap your page components with withI18n:

import { withI18n } from '~/lib/i18n/with-i18n';
function HomePage() {
return <div>Your page content</div>;
}
export default withI18n(HomePage);

The withI18n higher-order component:

  1. Creates an i18n server instance before rendering
  2. Loads all configured namespaces
  3. Makes translations available to the component tree

Using the Trans Component

After initialization, use the Trans component for translations:

import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
function HomePage() {
return (
<div>
<h1>
<Trans i18nKey="common:homeTabLabel" />
</h1>
<p>
<Trans i18nKey="common:homeTabDescription" />
</p>
</div>
);
}
export default withI18n(HomePage);

Import the Trans component from @kit/ui/trans - not directly from react-i18next. The Makerkit wrapper handles server/client differences.

Using Translations in Metadata

For page metadata, initialize the i18n instance directly:

import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
export async function generateMetadata() {
const i18n = await createI18nServerInstance();
return {
title: i18n.t('common:homeTabLabel'),
};
}
function HomePage() {
return (
<Trans i18nKey="common:homeTabLabel" />
);
}
export default withI18n(HomePage);

Using Translations in Client Components

Client Components receive translations through the I18nProvider in the root layout.

Using the useTranslation Hook

The useTranslation hook provides access to the translation function:

'use client';
import { useTranslation } from 'react-i18next';
export function MyComponent() {
const { t } = useTranslation();
return (
<button onClick={() => alert(t('common:cancel'))}>
{t('common:cancel')}
</button>
);
}

Specifying Namespaces

Load specific namespaces for better code splitting:

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

Using Trans in Client Components

The Trans component also works in Client Components:

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

Working with Translation Keys

Key Format

Translation keys follow the pattern namespace:keyPath:

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

Interpolation

Pass dynamic values to translations:

{
"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 } = useTranslation();
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

const { t, i18n } = useTranslation();
// Check current language
if (i18n.language === 'en') {
// English-specific logic
}
// Translate with fallback
const label = t('optional:key', { defaultValue: 'Fallback text' });

Pluralization

{
"itemCount": "{{count}} item",
"itemCount_other": "{{count}} items"
}
t('common:itemCount', { count: 1 }); // "1 item"
t('common:itemCount', { count: 5 }); // "5 items"

Date and Number Formatting

For dates and numbers, use the standard Intl APIs alongside translations:

const { i18n } = useTranslation();
const formattedDate = new Intl.DateTimeFormat(i18n.language).format(date);
const formattedNumber = new Intl.NumberFormat(i18n.language).format(1234.56);

Server Actions

For Server Actions, initialize translations at the start:

'use server';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
export async function myServerAction() {
const i18n = await createI18nServerInstance();
const t = i18n.t;
// 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)
NEXT_PUBLIC_DEFAULT_LOCALE=en
# Language priority: 'user' or 'application'
# 'user': Respect browser Accept-Language header
# 'application': Always use default locale
NEXT_PUBLIC_LANGUAGE_PRIORITY=application

When languagePriority is set to user, the system checks:

  1. The lang cookie (set when user changes language)
  2. The browser's Accept-Language header
  3. Falls back to NEXT_PUBLIC_DEFAULT_LOCALE

Troubleshooting

Missing Translation Warning

If you see Missing interpolation value for key, check:

  1. The key exists in your translation file
  2. All interpolation values are provided
  3. The namespace is loaded

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

Server Component Translation Missing

Ensure the page is wrapped with withI18n:

// Wrong - translations may not be available
export default function Page() { ... }
// Correct
export default withI18n(Page);

Frequently Asked Questions

How do I switch languages programmatically?
Use i18n.changeLanguage('es') from the useTranslation hook, then call window.location.reload() to refresh with the new language. The language is persisted in the 'lang' cookie automatically.
Why are my translations not showing?
Check that the namespace is registered in defaultI18nNamespaces in i18n.settings.ts, the JSON file exists in public/locales/[language]/, and for Server Components, verify the page is wrapped with withI18n.
Can I use translations in Server Actions?
Yes, import createI18nServerInstance from ~/lib/i18n/i18n.server and call it at the start of your server action. Then use the returned i18n.t function for translations.
What's the difference between Trans component and useTranslation hook?
Trans is a React component that renders translated strings directly in JSX, supporting interpolation and HTML. useTranslation 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.