Next.js 13.4 Localization — (Internationalization) using App Router

Atul Kumar
4 min readJun 30, 2023

Here we are going to implement multi-language support to our next.js application.

Step: 1 —
Create the next.js project using npx create-next-app@latest and it will create the next.js project boilerplate. As we are going to implement localization using an App router so make sure you have selected an app router during the creation of the project.

Use App Router (recommended)? … No / Yes — -> Yes

Now Project tree will look like this.

Next.js App Router Structure

Create a dictionaries directory at the root level which will contain our translation files, so here we are going to support two languages Hindi and English will create JSON files for each. like hi.json and en.json.


"form": {
"name": "name",
"email": "email",
"city": "city"


"form": {
"name": "नाम",
"email": "ईमेल",
"city": "शहर"

Step 3 — Create a file called getDictionary.js


const dictionaries = {
en: () => import("./dictionaries/en.json").then(r => r.default),
hi: () => import("./dictionaries/hi.json").then(r => r.default)

export const getDictionary = (lang) => {
return dictionaries[lang]();

step 4 — Create [lang] directory inside src/app and move all routes inside [lang] directory.

Now we are able to get lang (selected language) in params from routes /en or /en/about in side params so here en is our lang.

export default function RootLayout({ children, params }) {

console.log(params, 'params')

return (
<html lang={params.lang}>
<body className={inter.className}>{children}</body>

Now we going to use translations in / route


import { getDictionary } from "../../../getDictionary"

export default async function Home({params}) {
const lang = await getDictionary(params.lang);

console.log(lang, 'params for home')

return (
<input type="text"/>
<input type="email"/>
<input type="text"/>

Now the page will look like

hi lang route

en lang route

Handle auto routes based on browser language settings to get this feature we need to create a middleware file inside the src directory.

import { NextResponse } from 'next/server'

import { i18n } from '../i18n'

import { match as matchLocale } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

function getLocale(request) {
// Negotiator expects plain object so we need to transform headers
const negotiatorHeaders = {}
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))

// @ts-ignore locales are readonly
const locales = i18n.locales

// Use negotiator and intl-localematcher to get best locale
let languages = new Negotiator({ headers: negotiatorHeaders }).languages(

const locale = matchLocale(languages, locales, i18n.defaultLocale)

return locale

export function middleware(request) {
const pathname = request.nextUrl.pathname

// // `/_next/` and `/api/` are ignored by the watcher, but we need to ignore files in `public` manually.
// // If you have one
// if (
// [
// '/manifest.json',
// '/favicon.ico',
// // Your other files in `public`
// ].includes(pathname)
// )
// return

// Check if there is any supported locale in the pathname
const pathnameIsMissingLocale = i18n.locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`

// Redirect if there is no locale
if (pathnameIsMissingLocale) {
const locale = getLocale(request)

// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,

export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],

Las Step — Create i18n.js config file where we are deciding which is the default language if not match with browser settings.

export const i18n = {
defaultLocale: 'en',
locales: ['hi', 'en']