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

In v2, Makerkit used i18next and react-i18next for internationalization. In v3, the system uses next-intl. Key differences:

  • Translation keys use dot notation (namespace.key) instead of colon notation (namespace:key)
  • Interpolation uses single braces ({var}) instead of double braces ({{var}})
  • Server components use getTranslations from next-intl/server instead of withI18n HOC and createI18nServerInstance
  • Client components use useTranslations from next-intl instead of useTranslation from react-i18next
  • Translation files are in apps/web/i18n/messages/{locale}/ instead of apps/web/public/locales/{locale}/
  • Pluralization uses ICU format ({count, plural, one {# item} other {# items}}) instead of i18next _one/_other suffixes
  • Locale is determined by URL prefix, not cookies

For the full migration guide, see Upgrading from v2 to v3.