Next.js Internationalization (i18n) Without Locale Routing - Complete Guide with next-intl
Learn how to implement multi-language support in Next.js 15 without changing URLs. Master automatic browser language detection, user preference persistence with cookies, dynamic metadata translation, and seamless integration with both client and server components using next-intl. Includes complete code examples, best practices, and troubleshooting guide for building production-ready multilingual applications.
Next.js Internationalization (i18n) Without Locale Routing
Complete Guide to Implementing Multi-Language Support Using next-intl
Table of Contents
- Overview
- Prerequisites
- Project Setup
- Installation
- Configuration
- Creating Translation Files
- Implementation
- Browser Language Detection
- User Preference Management
- Dynamic Metadata
- Complete Code Examples
Overview
This documentation demonstrates how to implement multi-language support in a Next.js application without changing the URL structure. Unlike traditional i18n implementations that use route prefixes (e.g., /en/page, /fr/page), this approach maintains clean URLs while still providing full internationalization capabilities.
Key Features
- No URL changes - Routes remain the same regardless of language
- Automatic browser language detection - Detects user's default browser language on first visit
- User preference persistence - Remembers user's language choice using cookies
- Dynamic page metadata - Updates page titles based on selected language
- Works with both client and server components
Prerequisites
- Node.js installed
- Basic knowledge of Next.js and React
- Next.js 15 (or compatible version)
Project Setup
Step 1: Create a New Next.js Project
pnpm create next-app@latest .
Answer the configuration prompts:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- Source directory: No
- App Router: Yes
- Turbo pack: No (optional)
- Import Alias: No (or customize as needed)
Step 2: Verify Installation
pnpm dev
Visit http://localhost:3000 to ensure the project is running correctly.
Installation
Install next-intl Package
pnpm add next-intl
This package will appear in your package.json file under dependencies.
Configuration
Step 1: Configure next.config.ts
Open next.config.ts and add the following configuration:
import type { NextConfig } from "next";
// @ts-expect-error - next-intl requires this import style
const createNextIntlPlugin = require("next-intl/plugin");
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
/* config options here */
};
export default withNextIntl(nextConfig);Important Notes:
- The
requiresyntax is recommended by next-intl documentation for.ts/.jsconfig files - The TypeScript error can be safely ignored using the
@ts-expect-errorcomment - For
.mjs/.mtsextensions, you can use ES6 import syntax
Step 2: Create i18n Request Configuration
Create a new folder and file: i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers";
export default getRequestConfig(async () => {
// Get stored language preference from cookies
const cookieStore = await cookies();
const cookieLocale = cookieStore.get("my_nextapp_locale")?.value;
// Use cookie locale if it exists, otherwise default to 'en'
const locale = cookieLocale || "en";
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});Key Points:
cookies()must be awaited in Next.js 15- The cookie name (
my_nextapp_locale) should be unique to avoid conflicts - Messages are dynamically imported based on the selected locale
Creating Translation Files
Step 1: Create Messages Directory
Create a messages folder in your project root.
Step 2: Create Language Files
Create JSON files for each supported language:
messages/en.json
{
"tabTitles": {
"home": "Create Next App"
},
"homePage": {
"listOne": "Get started by editing",
"listTwo": "Save and see your changes instantly."
}
}messages/fr.json
{
"tabTitles": {
"home": "Créer une application Next"
},
"homePage": {
"listOne": "Commencez par éditer",
"listTwo": "Enregistrez et voyez vos modifications instantanément."
}
}Important Guidelines:
- Keys must be identical across all language files
- Only values should differ
- Use consistent structure and nesting
- Add as many language files as needed (e.g.,
de.json,es.json, etc.)
Implementation
Step 1: Set Up the Layout
Modify app/layout.tsx:
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getLocale } from "next-intl/server";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const locale = await getLocale();
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
<div className="mx-auto flex h-screen max-w-6xl flex-col">
<Navbar />
<div className="flex flex-grow items-center justify-center">
{children}
</div>
</div>
</NextIntlClientProvider>
</body>
</html>
);
}Key Changes:
- Wrap children with
NextIntlClientProvider - Pass
messagesandlocalefrom server functions - Layout function must be
async
Step 2: Create the Navbar Component
Create components/Navbar.tsx:
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
export default function Navbar() {
const [locale, setLocale] = useState("");
const router = useRouter();
useEffect(() => {
// Check if user has a stored language preference
const cookieLocale = document.cookie
.split("; ")
.find((row) => row.startsWith("my_nextapp_locale="))
?.split("=")[1];
if (cookieLocale) {
// Use stored preference
setLocale(cookieLocale);
} else {
// Detect browser's default language
const browserLocale = navigator.language.slice(0, 2);
setLocale(browserLocale);
// Store browser language as initial preference
document.cookie = `my_nextapp_locale=${browserLocale}; path=/`;
router.refresh();
}
}, [router]);
const changeLanguage = (newLocale: string) => {
setLocale(newLocale);
document.cookie = `my_nextapp_locale=${newLocale}; path=/`;
router.refresh();
};
return (
<nav className="flex items-center justify-between p-4">
<h1 className="text-2xl font-bold">Logo</h1>
<div className="flex gap-2">
<button
onClick={() => changeLanguage("en")}
className={`rounded px-4 py-2 ${
locale === "en" ? "bg-blue-500 text-white" : "bg-gray-200"
}`}
>
EN
</button>
<button
onClick={() => changeLanguage("fr")}
className={`rounded px-4 py-2 ${
locale === "fr" ? "bg-blue-500 text-white" : "bg-gray-200"
}`}
>
FR
</button>
</div>
</nav>
);
}Component Features:
- Detects browser language on first load
- Stores user preference in cookies
- Refreshes page when language changes
- Visual indication of selected language
Step 3: Use Translations in Pages
Modify app/page.tsx:
import { useTranslations } from "next-intl";
export default function Home() {
const t = useTranslations("homePage");
return (
<main className="flex flex-col items-center gap-8">
<div>
<ol>
<li>
<code>{t("listOne")}</code> app/page.tsx
</li>
<li>{t("listTwo")}</li>
</ol>
</div>
</main>
);
}Usage Notes:
useTranslations('homePage')accesses thehomePageobject from JSON files- Works in both client and server components
- To use in client components, add
'use client'directive
Browser Language Detection
How It Works
The language detection follows this priority:
-
First Visit:
- Checks if cookie
my_nextapp_localeexists - If not, detects browser's default language using
navigator.language - Stores detected language in cookie
- Sets this as active language
- Checks if cookie
-
Subsequent Visits:
- Reads language from cookie
- Uses stored preference (ignores browser default)
Implementation in Navbar
useEffect(() => {
const cookieLocale = document.cookie
.split("; ")
.find((row) => row.startsWith("my_nextapp_locale="))
?.split("=")[1];
if (cookieLocale) {
setLocale(cookieLocale);
} else {
const browserLocale = navigator.language.slice(0, 2);
setLocale(browserLocale);
document.cookie = `my_nextapp_locale=${browserLocale}; path=/`;
router.refresh();
}
}, [router]);User Preference Management
Storing Language Preference
When a user changes language:
const changeLanguage = (newLocale: string) => {
setLocale(newLocale);
document.cookie = `my_nextapp_locale=${newLocale}; path=/`;
router.refresh();
};Steps:
- Update local state
- Save to cookie
- Refresh router to reload with new language
Cookie Details
- Name:
my_nextapp_locale(customize as needed) - Path:
/(accessible across entire site) - Persistence: Cookie persists across browser sessions
- Scope: Domain-specific (won't conflict with other sites)
Dynamic Metadata
Updating Page Titles Based on Language
To support translated page titles, remove static metadata from layout.tsx and implement generateMetadata in each page:
Remove from layout.tsx:
// DELETE THIS:
export const metadata = {
title: "Create Next App",
description: "...",
};Add to page.tsx:
import { getMessages } from "next-intl/server";
import type { AbstractIntlMessages } from "next-intl";
export async function generateMetadata({
params,
}: {
params: { locale: string };
}) {
const messages = (await getMessages()) as AbstractIntlMessages;
const title = messages.tabTitles?.home;
return {
title,
};
}How It Works:
generateMetadatais a Next.js function that runs on the server- Fetches messages based on current locale
- Extracts appropriate title from translation files
- Updates page metadata dynamically
Complete Code Examples
Full Project Structure
my-nextjs-app/
├── app/
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ └── Navbar.tsx
├── i18n/
│ └── request.ts
├── messages/
│ ├── en.json
│ └── fr.json
├── next.config.ts
└── package.json
Complete i18n/request.ts
import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers";
export default getRequestConfig(async () => {
const cookieStore = await cookies();
const cookieLocale = cookieStore.get("my_nextapp_locale")?.value;
const locale = cookieLocale || "en";
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});Complete Navbar Component
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
export default function Navbar() {
const [locale, setLocale] = useState("");
const router = useRouter();
useEffect(() => {
const cookieLocale = document.cookie
.split("; ")
.find((row) => row.startsWith("my_nextapp_locale="))
?.split("=")[1];
if (cookieLocale) {
setLocale(cookieLocale);
} else {
const browserLocale = navigator.language.slice(0, 2);
setLocale(browserLocale);
document.cookie = `my_nextapp_locale=${browserLocale}; path=/`;
router.refresh();
}
}, [router]);
const changeLanguage = (newLocale: string) => {
setLocale(newLocale);
document.cookie = `my_nextapp_locale=${newLocale}; path=/`;
router.refresh();
};
return (
<nav className="flex items-center justify-between p-4">
<h1 className="text-2xl font-bold">Logo</h1>
<div className="flex gap-2">
<button
onClick={() => changeLanguage("en")}
className={`rounded px-4 py-2 ${
locale === "en" ? "bg-blue-500 text-white" : "bg-gray-200"
}`}
>
EN
</button>
<button
onClick={() => changeLanguage("fr")}
className={`rounded px-4 py-2 ${
locale === "fr" ? "bg-blue-500 text-white" : "bg-gray-200"
}`}
>
FR
</button>
</div>
</nav>
);
}Complete Page with Translations and Metadata
import { useTranslations } from "next-intl";
import { getMessages } from "next-intl/server";
import type { AbstractIntlMessages } from "next-intl";
export async function generateMetadata() {
const messages = (await getMessages()) as AbstractIntlMessages;
const title = messages.tabTitles?.home;
return { title };
}
export default function Home() {
const t = useTranslations("homePage");
return (
<main className="flex flex-col items-center gap-8">
<div>
<ol>
<li>
<code>{t("listOne")}</code> app/page.tsx
</li>
<li>{t("listTwo")}</li>
</ol>
</div>
</main>
);
}Best Practices
1. Translation File Organization
- Keep keys consistent across all language files
- Use nested objects for logical grouping
- Use descriptive key names
- Consider creating separate files for large applications:
messages/ ├── en/ │ ├── common.json │ ├── home.json │ └── about.json └── fr/ ├── common.json ├── home.json └── about.json
2. Cookie Management
- Use a unique cookie name to avoid conflicts
- Set appropriate path (
/for site-wide access) - Consider adding expiration dates for long-term storage
- Handle cookie consent if required by regulations (GDPR, etc.)
3. Type Safety
// Create a type for your messages
type Messages = typeof import("./messages/en.json");
// Use it for type-safe translations
const t = useTranslations<Messages>("homePage");4. Adding More Languages
To add a new language:
- Create new message file:
messages/de.json - Copy structure from existing file
- Translate all values
- Add button to Navbar:
<button onClick={() => changeLanguage("de")}>DE</button>
5. Server vs Client Components
- Server Components: Can use
useTranslationsdirectly - Client Components: Add
'use client'directive and useuseTranslations - Both approaches work identically with this setup
Troubleshooting
Common Issues
1. Translations not updating:
- Ensure
router.refresh()is called after language change - Check that cookie is being set correctly
- Verify JSON files have correct structure
2. TypeScript errors in next.config.ts:
- Use
@ts-expect-errorcomment as shown - This is expected and safe with next-intl
3. Cookie not persisting:
- Verify cookie name matches in all locations
- Check browser settings for cookie blocking
- Ensure
path=/is set
4. Browser language not detected:
- Check
navigator.languagebrowser compatibility - Fallback to default language if detection fails
- Use
.slice(0, 2)to get language code only
Conclusion
This implementation provides a clean, URL-friendly approach to internationalization in Next.js. It automatically detects user preferences while respecting their choices across sessions. The setup works seamlessly with both client and server components, making it suitable for any Next.js application architecture.
Key Advantages
- ✅ Clean URLs (no
/enor/frprefixes) - ✅ Automatic browser language detection
- ✅ Persistent user preferences
- ✅ Works with Server and Client Components
- ✅ Dynamic metadata support
- ✅ Easy to add new languages
- ✅ Type-safe with TypeScript
Additional Resources
Document Version: 1.0
Last Updated: December 2024
Next.js Version: 15.x
next-intl Version: Latest

