# Using Translations

> How to use translations in Server Components, Client Components, page metadata, and with rich text interpolation using the Trans component.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/internationalization/using-translations*

---

MakerKit uses [next-intl](https://next-intl.dev/) 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`:

```typescript
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:

```typescript
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:

```typescript
const t = await getTranslations('common');

// Access common.routes.dashboard
t('routes.dashboard'); // "Dashboard"

// Or scope to the nested object
const tRoutes = await getTranslations('common.routes');
tRoutes('dashboard'); // "Dashboard"
```

Given this JSON structure:

```json title="apps/web/i18n/messages/en/common.json"
{
  "routes": {
    "account": "Account",
    "members": "Members",
    "billing": "Billing",
    "dashboard": "Dashboard"
  }
}
```

### Page Metadata

Use `getTranslations` in `generateMetadata`:

```typescript
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:

```typescript
'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`:

```typescript
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:

```json title="apps/web/i18n/messages/en/auth.json"
{
  "acceptTermsAndConditions": "I accept the <TermsOfServiceLink>Terms of Service</TermsOfServiceLink> and <PrivacyPolicyLink>Privacy Policy</PrivacyPolicyLink>"
}
```

The `Trans` component renders the links correctly:

```typescript
<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

```json
{
  "signUp": "Sign Up",
  "signIn": "Sign In",
  "forgotPassword": "Forgot Password?"
}
```

```typescript
const t = useTranslations('auth');
t('signUp'); // "Sign Up"
```

### Nested Keys

Organize related translations:

```json
{
  "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"
    }
  }
}
```

```typescript
const t = useTranslations('common');
t('otp.requestVerificationCode'); // "Request Verification Code"
t('otp.errors.invalidEmail'); // "Please enter a valid email address"
```

### Interpolation

Insert dynamic values:

```json
{
  "greeting": "Hello, {name}!",
  "itemCount": "You have {count} items",
  "getStartedWithPlan": "Get Started with {plan}"
}
```

```typescript
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:

```json
{
  "invitations": "{count} {count, plural, one {invitation} other {invitations}} pending",
  "members": "{count, plural, =0 {No members} one {1 member} other {# members}}"
}
```

```typescript
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:

```json
{
  "notification": "{type, select, message {New message from {sender}} mention {You were mentioned by {sender}} other {New notification}}"
}
```

```typescript
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

```json
{
  "clickToAcceptAs": "Click the button below to accept the invite as <b>{email}</b>"
}
```

```typescript
<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

```typescript
import { getLocale } from 'next-intl/server';

async function MyServerComponent() {
  const locale = await getLocale();
  // locale = 'en', 'es', 'fr', etc.
}
```

### Client-Side

```typescript
'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:

```typescript
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 awareness
redirect('/dashboard');

// Client-side navigation
const router = useRouter();
router.push('/settings');

// Switch locale explicitly
router.replace('/settings', { locale: 'es' });

// Get pathname without locale prefix
const pathname = usePathname();
// Returns '/dashboard' not '/es/dashboard'
```

## Date and Time Formatting

Use `next-intl` formatters or native `Intl` APIs for locale-aware formatting:

```typescript
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`:

```typescript
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

```typescript
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:

```typescript
// If 'auth.unknownKey' doesn't exist:
t('unknownKey'); // Returns "unknownKey" and logs warning
```

### Checking if a Key Exists

```typescript
const t = useTranslations('auth');

if (t.has('specialMessage')) {
  return <p>{t('specialMessage')}</p>;
}
```

### Custom Fallback

Use the `defaults` prop with `Trans`:

```typescript
<Trans
  i18nKey="feature.newLabel"
  defaults="New Feature"
/>
```

## Common Patterns

### Conditional Translations

```typescript
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

```typescript
'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

```typescript
<Trans
  i18nKey="auth.existingAccountHint"
  values={{ methodName: 'Email' }}
  components={{
    method: <strong />,
    signInLink: <Link href="/auth/sign-in" className="underline" />,
  }}
/>
```

For the translation:

```json
{
  "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 `useTranslations` in Server Components (it won't work)
- Using `Trans` for simple text without formatting (overhead)
- Hardcoding strings instead of using translation keys

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "What is the difference between getTranslations and useTranslations?", "answer": "getTranslations is an async function for Server Components that directly loads messages from the server. useTranslations is a React hook for Client Components that reads from the context provided by I18nClientProvider. Use getTranslations in pages and server components, useTranslations in 'use client' components."},
     {"question": "When should I use the Trans component?", "answer": "Use the Trans component from @kit/ui/trans when you need to embed JSX elements like links, bold text, or other components inside translated strings. For simple text without formatting, use the t() function directly as it has less overhead."},
     {"question": "How do I handle pluralization in translations?", "answer": "Use ICU message format in your JSON files. For example: \"{count, plural, one {1 item} other {# items}}\" will output '1 item' for count=1 and '5 items' for count=5. Pass the count value when calling t('key', { count: 5 })."},
     {"question": "Why are my translations showing the key name instead of the translation?", "answer": "This happens when the translation key doesn't exist in your JSON file. Check that: 1) The namespace matches the filename, 2) The key path is correct (use dot notation for nested keys), 3) The JSON file exists for the current locale. Missing translations log warnings in development."},
     {"question": "How do I get the current locale in my component?", "answer": "In Server Components, use await getLocale() from next-intl/server. In Client Components, use the useLocale() hook from next-intl. Both return the current locale string like 'en' or 'es'."},
     {"question": "Can I use translations in generateMetadata?", "answer": "Yes, use getTranslations in generateMetadata just like in Server Components. It's async, so await it: const t = await getTranslations('marketing'); return { title: t('pageTitle') };"}
   ]
/%}

---

**Previous:** [Setup Guide](./setup) | **Next:** [Managing Translations](./managing-translations)
