Setup Guide

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

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:

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:

# 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

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:

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:

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:

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):

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:

.env

NEXT_PUBLIC_DEFAULT_LOCALE=es

Ensure Spanish is first in your locales array:

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:

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:

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:

{
"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:

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)
  • [ ] Language selector is visible in settings
  • [ ] SEO metadata is translated (titles, descriptions)
  • [ ] Date/number formatting uses locale-aware APIs

Frequently Asked Questions

How do I change the default language?
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.
Why isn't my new locale working?
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.
How do I disable automatic locale detection?
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.
Where is the i18n middleware configured?
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.
How do I get TypeScript autocomplete for translation keys?
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 | Next: Using Translations