Implementing Multilingual i18n with Next.js 14


June 22, 2024 Program

Implementing Multilingual i18n with Next.js 14
Implementing Multilingual i18n with Next.js 14 and negotiator.

Introduction

πŸ”—

This article introduces how to set up multilingual functionality in a Next.js project, including setting the default language, managing and reading language data, and dynamically rendering content based on the user's requested language. The default language in the example is English (en), with support for Traditional Chinese (zh-tw).

Setup Steps

πŸ”—

Define Language Configuration:

πŸ”—

Create i18n.config.ts in the root directory.

ts
export const i18n = {
    defaultLocale: 'en',
    locales: ['en', 'zh-tw']
  } as const
  
export type Locale = (typeof i18n)['locales'][number]

Set the website's language and the default language.

Language Data Files:

πŸ”—

Create display information corresponding to each language (based on your own requirements).
src\dictionaries\zh-tw.json
src\dictionaries\en.json

Ex:

JSON
{
  "layout": {
    "brand": "Sun Note",
    "recentposts": "Recent Posts",
    "readmore": "Read more",
    "allposts": "All POSTS",
    "recentprojects": "Recent Projects",
    "categories": "CATEGORIES",
    "tags": "TAGS",
    "lastupdated":"Last updated"
  }
}

Then, display the corresponding language data based on the key.

Function to Get Language Data:

πŸ”—

Create a function to retrieve language content.

src\lib\dictionary.ts

ts
import 'server-only'
import type { Locale } from '../../i18n.config'

const dictionaries: { [key: string]: () => Promise<any> } = {
  'en': () => import('@/dictionaries/en.json').then(module => module.default),
  'zh-tw': () => import('@/dictionaries/zh-tw.json').then(module => module.default)
}

export const getDictionary = async (locale: Locale) => dictionaries[locale]()

Next, to implement multilingual functionality on a page, use getDictionary() to obtain the data for the corresponding language.

JS
import { getDictionary } from '@/lib/dictionary'

const { layout } = await getDictionary(lang);

Next.js Dynamic Routing:

πŸ”—

To obtain language data, it can be retrieved through the website's path using Next.js dynamic routing. Create [lang] in src\app\[lang], resulting in the actual project structure:

lua
my-next-app/
β”œβ”€β”€ node_modules/
β”œβ”€β”€ public/
β”‚   β”œβ”€β”€ favicon.ico
β”‚   └── vercel.svg
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   └── [lang]
β”‚   β”‚        β”œβ”€β”€ globals.css
β”‚   β”‚        β”œβ”€β”€ layout.tsx
β”‚   β”‚        └── page.tsx
β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   └── dictionary.ts
β”‚   β”œβ”€β”€ components/
β”‚   └── middleware.ts
β”œβ”€β”€ .gitignore
β”œβ”€β”€ i18n.config.ts
β”œβ”€β”€ package.json
β”œβ”€β”€ README.md
└── next.config.js

Pages under [lang] can then obtain the corresponding language through the URL.

Reading User's Default Language

πŸ”—

To determine the user's preferred system language and display the corresponding data, negotiator can be used to obtain language information from the Header.

It needs to be used in middleware, create src\middleware.ts

Code

JS
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { i18n } from '../i18n.config'
import { match as matchLocale } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

/**
 * Retrieves the preferred language from the request headers
 * @param request - The incoming request
 * @returns The matched language or undefined
 */
function getLocale(request: NextRequest): string | undefined {
    const negotiatorHeaders: Record<string, string> = {}
    request.headers.forEach((value, key) => (negotiatorHeaders[key] = value))

    // @ts-ignore locales are readonly
    const locales: string[] = i18n.locales

    const languages = new Negotiator({ headers: negotiatorHeaders }).languages()

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

    return locale
}
/**
 * Middleware function for handling language redirection
 * @param request - The incoming request
 * @returns A redirect response if needed
 */
export function middleware(request: NextRequest) {
  
    // Check if there is any supported locale in the pathname
    const { pathname } = request.nextUrl
    const pathnameHasLocale = i18n.locales.some(
        (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
    )

    if (pathnameHasLocale) {
        if(pathname.startsWith('/en')){
            const newPathname = pathname.replace(/^\/en/, '');
            request.nextUrl.pathname = newPathname
            return NextResponse.redirect(request.nextUrl)
        }
        return
    }
    // Rewrite to en/ content if there is no locale
    // const locale = getLocale(request)
    request.nextUrl.pathname = `/${i18n.defaultLocale}${pathname}`
    return NextResponse.rewrite(request.nextUrl)
}

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

Detailed Explanation:

getLocale() can obtain the user's preferred language from the request. If it does not match the website's available languages, it returns the default language.

The logic of middleware mainly involves checking the URL based on the request to see if it contains a language code. If the language code does not match any of the languages offered by the website, it redirects to the default language.

Since the default language of the website is English en, we do not want en to appear in the URL. Therefore, some modifications were made. If the URL contains en, it redirects to a URL without a language code. If the original URL does not contain any language information, it rewrites the content corresponding to en. The entire middleware does not use getLocale() because we want the content corresponding to the URL to be displayed without redirection.

Conclusion

πŸ”—

Through the methods described above, multilingual support is implemented, and content corresponding to the user's requested language is dynamically presented, enhancing the user experience. Finally, a slight modification was made in the middleware to allow users to access the site through a URL without a language code and still display the default language content without encountering a page not found error.

Reference

Next.jsReactWeb



Avatar

Alvin

Software engineer, interested in financial knowledge, health concepts, psychology, independent travel, and system design.

Related Posts