Using translations in your Next.js 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 next-intl 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) - Access translations via getTranslations from next-intl/server
  2. Client Components - Access translations via useTranslations from next-intl
  3. URL-based locale routing - Locale is determined by the URL prefix (e.g., /en/home, /es/home)

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

apps/web/i18n/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 Server Components

Server Components can access translations directly using getTranslations from next-intl/server.

Using getTranslations

import { getTranslations } from 'next-intl/server';
export default async function HomePage() {
const t = await getTranslations('common');
return (
<div>
<h1>{t('homeTabLabel')}</h1>
<p>{t('homeTabDescription')}</p>
</div>
);
}

Using the Trans Component

The Trans component renders translated strings directly in JSX:

import { Trans } from '@kit/ui/trans';
export default 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 handles server/client differences.

Using Translations in Metadata

For page metadata, use getTranslations directly:

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

Using Translations in Client Components

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

Using the useTranslations Hook

The useTranslations hook provides access to the translation function:

'use client';
import { useTranslations } from 'next-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:

'use client';
import { useTranslations } from 'next-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:

'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 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 'next-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

next-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 Actions

For Server Actions, use getTranslations from next-intl/server:

'use server';
import { getTranslations } from 'next-intl/server';
export async function myServerAction() {
const t = await getTranslations('common');
// Use translations
const message = t('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

The locale is determined by the URL prefix (e.g., /en/, /es/). When a user visits the root URL, they are redirected to their preferred locale based on:

  1. The browser's Accept-Language header
  2. Falls back to NEXT_PUBLIC_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 apps/web/i18n/request.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?
Use router.replace() with the new locale from @kit/i18n/navigation. The locale is part of the URL path (e.g., /en/ to /es/), so changing language means navigating to the equivalent URL with a different locale prefix.
Why are my translations not showing?
Check that the namespace is registered in the namespaces array in apps/web/i18n/request.ts, the JSON file exists in apps/web/i18n/messages/{locale}/, and verify the key uses dot notation (namespace.key not namespace:key).
Can I use translations in Server Actions?
Yes, import getTranslations from next-intl/server and call it at the start of your server action. Then use the returned t() function for translations.
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.

Upgrading from v2