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.
Steps to use translations
Learn how to use translations in your Next.js Supabase project.
Translation Architecture
The translation system supports:
- Server Components (RSC) - Access translations via
getTranslationsfromnext-intl/server - Client Components - Access translations via
useTranslationsfromnext-intl - 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 pagesUsing 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 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 'next-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
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=enThe 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:
- The browser's
Accept-Languageheader - Falls back to
NEXT_PUBLIC_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
apps/web/i18n/request.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 Server Actions?
What's the difference between Trans component and useTranslations hook?
How do I handle missing translations during development?
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
getTranslationsfromnext-intl/serverinstead ofwithI18nHOC andcreateI18nServerInstance - Client components use
useTranslationsfromnext-intlinstead ofuseTranslationfromreact-i18next - Translation files are in
apps/web/i18n/messages/{locale}/instead ofapps/web/public/locales/{locale}/ - Pluralization uses ICU format (
{count, plural, one {# item} other {# items}}) instead of i18next_one/_othersuffixes - Locale is determined by URL prefix, not cookies
For the full migration guide, see Upgrading from v2 to v3.
Related Documentation
- Adding Translations - Add new languages and namespaces
- Language Selector - Let users change their language
- Email Translations - Translate email templates