# Setup Guide

> Configure internationalization in your MakerKit application with middleware, locales, and translation files.

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

---

This guide walks through adding a new language to your MakerKit application. The i18n system is already configured out of the box with English; you just need to enable additional locales.

## Quick Setup (Add a New Language)

Adding Spanish support takes three steps:

### 1. Enable the Locale

Edit `packages/i18n/src/locales.tsx`:

```typescript
import { defaultLocale } from './default-locale';

export const locales: string[] = [
  defaultLocale,
  'es', // Spanish - uncomment or add this
  // 'fr', // French
  // 'de', // German
];
```

### 2. Create Translation Files

Copy the English translations and translate them:

```bash
# Create the Spanish locale folder
mkdir -p apps/web/i18n/messages/es

# Copy all English files as templates
cp apps/web/i18n/messages/en/*.json apps/web/i18n/messages/es/
```

Then edit each JSON file in `apps/web/i18n/messages/es/` to add Spanish translations.

### 3. Restart the Dev Server

```bash
pnpm dev
```

Visit `/es/dashboard` to see your Spanish translations. The language selector will automatically appear in user settings when multiple locales are configured.

## Configuration Files Explained

### Middleware (`apps/web/proxy.ts`)

The middleware handles locale detection and routing:

```typescript title="apps/web/proxy.ts"
import createNextIntlMiddleware from 'next-intl/middleware';
import { routing } from '@kit/i18n/routing';

async function proxy(request: NextRequest) {
  // Create the i18n middleware with your routing config
  const handleI18nRouting = createNextIntlMiddleware(routing);

  // Process the request
  const response = handleI18nRouting(request);

  // ... additional middleware logic (auth, CSP, etc.)

  return response;
}
```

The middleware:

- Detects locale from URL, cookies, or `Accept-Language` header
- Redirects to the localized URL when needed
- Sets the `NEXT_LOCALE` cookie for subsequent requests

### Request Configuration (`apps/web/i18n/request.ts`)

This file defines which namespaces to load and how:

```typescript title="apps/web/i18n/request.ts"
import { getRequestConfig } from 'next-intl/server';
import { routing } from '@kit/i18n/routing';

// Define your translation namespaces
const namespaces = [
  'common',
  'auth',
  'account',
  'organizations',
  'billing',
  'marketing',
  'settings',
  'goodbye',
  'errors',
] as const;

export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale;

  // Validate locale, fallback to default
  if (!locale || !routing.locales.includes(locale as never)) {
    locale = routing.defaultLocale;
  }

  // Load all namespace files
  const messages = await loadMessages(locale);

  return {
    locale,
    messages,
    timeZone: 'UTC',
    // Error handling for missing translations
    onError(error) {
      if (process.env.NODE_ENV === 'development') {
        console.warn(`[Dev Only] i18n error: ${error.message}`);
      }
    },
    getMessageFallback(info) {
      return info.key; // Return the key as fallback
    },
  };
});
```

### Root Layout (`apps/web/app/[locale]/layout.tsx`)

The layout loads messages and provides them to the app:

```typescript title="apps/web/app/[locale]/layout.tsx"
import { hasLocale } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';

import { RootProviders } from '@components/root-providers';
import { routing } from '@kit/i18n/routing';

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params;

  // Validate locale
  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  // Load all messages for this locale
  const messages = await getMessages({ locale });

  return (
    <html lang={locale}>
      <body>
        <RootProviders locale={locale} messages={messages}>
          {children}
        </RootProviders>
      </body>
    </html>
  );
}
```

## Environment Variables

### `NEXT_PUBLIC_DEFAULT_LOCALE`

Set the default locale (used when no locale is detected):

```bash title=".env"
NEXT_PUBLIC_DEFAULT_LOCALE=en
```

If not set, defaults to `'en'`.

### Changing the Default Locale

To make Spanish the default:

First, update the environment variable:

```bash {% title=".env" %}
NEXT_PUBLIC_DEFAULT_LOCALE=es
```

Ensure Spanish is first in your locales array:

```typescript {% title="packages/i18n/src/locales.tsx" %}
export const locales: string[] = [
  defaultLocale, // Now 'es' from env
  'en',
  'fr',
];
```

Now `/dashboard` serves Spanish, and `/en/dashboard` serves English.

## Locale Detection Order

The middleware detects locale in this order:

1. **URL path** - `/es/dashboard` explicitly requests Spanish
2. **Cookie** - `NEXT_LOCALE` cookie from previous preference
3. **Accept-Language header** - Browser's language preference
4. **Default locale** - Fallback from `NEXT_PUBLIC_DEFAULT_LOCALE`

### Disabling Auto-Detection

If you want to disable browser-based detection:

```typescript title="packages/i18n/src/routing.ts"
export const routing = defineRouting({
  locales,
  defaultLocale,
  localePrefix: 'as-needed',
  localeDetection: false, // Disable auto-detection
});
```

With detection disabled, users always start on the default locale unless they explicitly navigate to a prefixed URL.

## TypeScript Configuration

For type-safe translations, create a declaration file:

```typescript title="apps/web/types/i18n.d.ts"
import type en from '@/i18n/messages/en/common.json';

type Messages = typeof en;

declare global {
  interface IntlMessages extends Messages {}
}
```

This enables autocomplete for translation keys in your IDE.

## Common Setup Issues

### Missing Translations Warning

If you see warnings about missing translations:

```
[Dev Only] i18n error: Missing message: auth.newKey
```

Add the missing key to all locale files:

```json title="apps/web/i18n/messages/en/auth.json"
{
  "newKey": "Your translation here"
}
```

### Locale Not Found (404)

If `/es/dashboard` returns 404:

1. Check that `'es'` is in your `locales` array
2. Verify the middleware matcher doesn't exclude the path
3. Ensure you restarted the dev server after adding the locale

### Middleware Not Running

Check the matcher in `apps/web/proxy.ts`:

```typescript
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|images|api/*).*)',
  ],
};
```

Add any paths that should be excluded from i18n routing.

### Translations Not Updating

In development, translation files are cached. Try:

1. Hard refresh (Cmd+Shift+R)
2. Clear `.next` cache: `rm -rf .next`
3. Restart the dev server

## Production Checklist

Before deploying a multilingual app:

- [ ] All translation files exist for each locale
- [ ] No missing translation keys (check dev console)
- [ ] `NEXT_PUBLIC_DEFAULT_LOCALE` is set correctly
- [ ] Email templates have translations (see [Email Translations](./email-translations))
- [ ] Language selector is visible in settings
- [ ] SEO metadata is translated (titles, descriptions)
- [ ] Date/number formatting uses locale-aware APIs

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How do I change the default language?", "answer": "Set the NEXT_PUBLIC_DEFAULT_LOCALE environment variable in your .env file. For example, NEXT_PUBLIC_DEFAULT_LOCALE=es makes Spanish the default. The default locale won't have a URL prefix, so /dashboard serves Spanish content."},
     {"question": "Why isn't my new locale working?", "answer": "Check that: 1) The locale is added to packages/i18n/src/locales.tsx, 2) Translation JSON files exist in apps/web/i18n/messages/{locale}/, 3) You restarted the dev server after making changes. Also verify the middleware matcher doesn't exclude your routes."},
     {"question": "How do I disable automatic locale detection?", "answer": "Set localeDetection: false in packages/i18n/src/routing.ts. With detection disabled, users always start on the default locale unless they explicitly navigate to a prefixed URL like /es/dashboard."},
     {"question": "Where is the i18n middleware configured?", "answer": "The middleware is in apps/web/proxy.ts. It uses createNextIntlMiddleware from next-intl/middleware with the routing configuration from @kit/i18n/routing. The middleware handles locale detection, URL prefixing, and cookie management."},
     {"question": "How do I get TypeScript autocomplete for translation keys?", "answer": "Create a declaration file at apps/web/types/i18n.d.ts that imports your translation JSON and extends IntlMessages. This enables autocomplete for translation keys in your IDE."}
   ]
/%}

---

**Previous:** [Overview](./overview) | **Next:** [Using Translations](./using-translations)
