Using Translations
How to use translations in Server Components, Client Components, page metadata, and with rich text interpolation using the Trans component.
MakerKit uses next-intl for translations. Use getTranslations() in Server Components (async, returns the t function) and useTranslations() hook in Client Components. For rich text with embedded links or formatting, use the Trans component from @kit/ui/trans.
The translation APIs differ between Server and Client Components, but translation keys and JSON file patterns are the same across both.
Server Components
In React Server Components (the default in the App Router), use getTranslations:
import { getTranslations } from 'next-intl/server';async function SettingsPage() { const t = await getTranslations('settings'); return ( <div> <h1>{t('pageTitle')}</h1> <p>{t('pageDescription')}</p> </div> );}The namespace ('settings') maps to apps/web/i18n/messages/{locale}/settings.json.
Multiple Namespaces
Load multiple namespaces when needed:
async function DashboardPage() { const [tCommon, tBilling] = await Promise.all([ getTranslations('common'), getTranslations('billing'), ]); return ( <div> <h1>{tCommon('dashboardTabLabel')}</h1> <p>{tBilling('currentPlanLabel')}</p> </div> );}Nested Keys
Access nested translations with dot notation:
const t = await getTranslations('common');// Access common.routes.dashboardt('routes.dashboard'); // "Dashboard"// Or scope to the nested objectconst tRoutes = await getTranslations('common.routes');tRoutes('dashboard'); // "Dashboard"Given this JSON structure:
{ "routes": { "account": "Account", "members": "Members", "billing": "Billing", "dashboard": "Dashboard" }}Page Metadata
Use getTranslations in generateMetadata:
import { getTranslations } from 'next-intl/server';import type { Metadata } from 'next';export async function generateMetadata(): Promise<Metadata> { const t = await getTranslations('marketing'); return { title: t('pageTitle'), description: t('pageDescription'), };}Client Components
In Client Components, use the useTranslations hook:
'use client';import { useTranslations } from 'next-intl';function CreateOrganizationDialog() { const t = useTranslations('organizations'); return ( <Dialog> <DialogTitle>{t('createOrganizationDialogHeading')}</DialogTitle> <DialogDescription> {t('createOrganizationDialogDescription')} </DialogDescription> <Button>{t('createButton')}</Button> </Dialog> );}The hook reads from the context provided by I18nClientProvider in the root layout.
The Trans Component
For complex translations with JSX interpolation, use the Trans component from @kit/ui/trans:
import { Trans } from '@kit/ui/trans';// Basic usage<Trans i18nKey="auth.welcomeMessage" />// With interpolation values<Trans i18nKey="common.greeting" values={{ name: user.name }}/>// With component interpolation for rich text<Trans i18nKey="auth.acceptTermsAndConditions" components={{ TermsOfServiceLink: <a href="/terms" className="underline" />, PrivacyPolicyLink: <a href="/privacy" className="underline" />, }}/>The translation key uses dot notation: namespace.key.
Rich Text Example
Given this translation:
{ "acceptTermsAndConditions": "I accept the <TermsOfServiceLink>Terms of Service</TermsOfServiceLink> and <PrivacyPolicyLink>Privacy Policy</PrivacyPolicyLink>"}The Trans component renders the links correctly:
<Trans i18nKey="auth.acceptTermsAndConditions" components={{ TermsOfServiceLink: <a href="/legal/terms" className="underline" />, PrivacyPolicyLink: <a href="/legal/privacy" className="underline" />, }}/>Output: I accept the Terms of Service and Privacy Policy
Trans Component Props
| Prop | Type | Description |
|---|---|---|
i18nKey | string | Translation key with namespace (e.g., 'auth.signIn') |
values | Record<string, unknown> | Values for interpolation |
components | Record<string, ReactElement> | Components for rich text tags |
defaults | ReactNode | Fallback if translation is missing |
ns | string | Override namespace (optional) |
Translation Patterns
Simple Keys
{ "signUp": "Sign Up", "signIn": "Sign In", "forgotPassword": "Forgot Password?"}const t = useTranslations('auth');t('signUp'); // "Sign Up"Nested Keys
Organize related translations:
{ "otp": { "requestVerificationCode": "Request Verification Code", "sendVerificationCode": "Send Verification Code", "enterVerificationCode": "Enter Verification Code", "errors": { "invalidEmail": "Please enter a valid email address", "codeLength": "Code must be exactly 6 digits" } }}const t = useTranslations('common');t('otp.requestVerificationCode'); // "Request Verification Code"t('otp.errors.invalidEmail'); // "Please enter a valid email address"Interpolation
Insert dynamic values:
{ "greeting": "Hello, {name}!", "itemCount": "You have {count} items", "getStartedWithPlan": "Get Started with {plan}"}t('greeting', { name: 'John' }); // "Hello, John!"t('itemCount', { count: 5 }); // "You have 5 items"t('getStartedWithPlan', { plan: 'Pro' }); // "Get Started with Pro"Pluralization
Use ICU message format for count-dependent text:
{ "invitations": "{count} {count, plural, one {invitation} other {invitations}} pending", "members": "{count, plural, =0 {No members} one {1 member} other {# members}}"}t('invitations', { count: 1 }); // "1 invitation pending"t('invitations', { count: 5 }); // "5 invitations pending"t('members', { count: 0 }); // "No members"t('members', { count: 1 }); // "1 member"t('members', { count: 10 }); // "10 members"Select (Gender/Category)
Use ICU select for category-based text:
{ "notification": "{type, select, message {New message from {sender}} mention {You were mentioned by {sender}} other {New notification}}"}t('notification', { type: 'message', sender: 'Alice' }); // "New message from Alice"t('notification', { type: 'mention', sender: 'Bob' }); // "You were mentioned by Bob"Rich Text with Bold/Emphasis
{ "clickToAcceptAs": "Click the button below to accept the invite as <b>{email}</b>"}<Trans i18nKey="auth.clickToAcceptAs" values={{ email: 'user@example.com' }} components={{ b: <strong />, }}/>Output: Click the button below to accept the invite as user@example.com
Getting the Current Locale
Server-Side
import { getLocale } from 'next-intl/server';async function MyServerComponent() { const locale = await getLocale(); // locale = 'en', 'es', 'fr', etc.}Client-Side
'use client';import { useLocale } from 'next-intl';function MyClientComponent() { const locale = useLocale(); // locale = 'en', 'es', 'fr', etc.}Locale-Aware Navigation
Use navigation utilities from @kit/i18n/navigation instead of Next.js defaults:
import { Link, redirect, useRouter, usePathname } from '@kit/i18n/navigation';// Link automatically handles locale prefix<Link href="/settings">Settings</Link>// Renders as /settings (default locale) or /es/settings (Spanish)// Redirect with locale awarenessredirect('/dashboard');// Client-side navigationconst router = useRouter();router.push('/settings');// Switch locale explicitlyrouter.replace('/settings', { locale: 'es' });// Get pathname without locale prefixconst pathname = usePathname();// Returns '/dashboard' not '/es/dashboard'Date and Time Formatting
Use next-intl formatters or native Intl APIs for locale-aware formatting:
import { getFormatter, getLocale } from 'next-intl/server';async function DateDisplay({ date }: { date: Date }) { const format = await getFormatter(); return ( <time dateTime={date.toISOString()}> {format.dateTime(date, { dateStyle: 'long' })} </time> );}Or with native Intl:
import { getLocale } from 'next-intl/server';async function DateDisplay({ date }: { date: Date }) { const locale = await getLocale(); const formatted = new Intl.DateTimeFormat(locale, { dateStyle: 'long', }).format(date); return <time dateTime={date.toISOString()}>{formatted}</time>;}Number Formatting
import { getFormatter } from 'next-intl/server';async function PriceDisplay({ amount }: { amount: number }) { const format = await getFormatter(); return <span>{format.number(amount, { style: 'currency', currency: 'USD' })}</span>;}Error Handling
Missing translations fall back to the key name and log a warning in development:
// If 'auth.unknownKey' doesn't exist:t('unknownKey'); // Returns "unknownKey" and logs warningChecking if a Key Exists
const t = useTranslations('auth');if (t.has('specialMessage')) { return <p>{t('specialMessage')}</p>;}Custom Fallback
Use the defaults prop with Trans:
<Trans i18nKey="feature.newLabel" defaults="New Feature"/>Common Patterns
Conditional Translations
async function StatusBadge({ status }: { status: string }) { const t = await getTranslations('common'); const statusKey = `status.${status}`; const label = t.has(statusKey) ? t(statusKey) : status; return <Badge>{label}</Badge>;}Form Validation Messages
'use client';import { useTranslations } from 'next-intl';import { z } from 'zod';function useAuthValidation() { const t = useTranslations('auth.errors'); return z.object({ password: z .string() .min(8, t('minPasswordLength', { minLength: 8 })) .regex(/[A-Z]/, t('uppercasePassword')) .regex(/[0-9]/, t('minPasswordNumbers')), });}Translations with HTML Content
<Trans i18nKey="auth.existingAccountHint" values={{ methodName: 'Email' }} components={{ method: <strong />, signInLink: <Link href="/auth/sign-in" className="underline" />, }}/>For the translation:
{ "existingAccountHint": "You previously signed in with <method>{methodName}</method>. <signInLink>Already have an account?</signInLink>"}When to Use Each API
| Scenario | Use This | Why |
|---|---|---|
| Server Component | getTranslations() | Async, direct access to messages |
| Client Component | useTranslations() | Hook-based, reads from context |
| Rich text with links | Trans component | Handles JSX interpolation safely |
| Page metadata | getTranslations() | Works in generateMetadata() |
| Form validation | useTranslations() | Client-side, returns sync messages |
| Email templates | initializeEmailI18n() | Works outside React context |
Avoid:
- Using
useTranslationsin Server Components (it won't work) - Using
Transfor simple text without formatting (overhead) - Hardcoding strings instead of using translation keys
Frequently Asked Questions
What is the difference between getTranslations and useTranslations?
When should I use the Trans component?
How do I handle pluralization in translations?
Why are my translations showing the key name instead of the translation?
How do I get the current locale in my component?
Can I use translations in generateMetadata?
Previous: Setup Guide | Next: Managing Translations