Internationalization in Next.JS

How to implement internationalization in Next.js

FRONTEND

12 min read

Lets implement internationalization in Next.JS

Learn how to build a sample app in Next.js and level up your Next.js i18n skills to implement internationalization.

Next.js has built-in support for internationalized (i18n) routing since v10.0.0. You can provide a list of locales, the default locale, and domain-specific locales and Next.js will automatically handle the routing.

Next.js by default support two types of routing based on internationalization

About Next JS

Next.js is a powerful frontend framework that offers seamless internationalization and translation capabilities. With Next.js, developers can easily build multilingual websites, ensuring that their content is accessible to a global audience. The framework provides robust tools and APIs for managing translations, allowing developers to organize and maintain their localized content efficiently. Whether you need to translate user interface elements, static content, or dynamic data, Next.js has you covered. The internationalization features in Next.js make it simple to serve different language versions of your site based on user preferences or location. Additionally, Next.js supports popular translation libraries, making it easy to integrate with existing localization workflows. With its comprehensive internationalization and translation capabilities, Next.js empowers developers to create inclusive and user-friendly web applications across the globe.

SETUP

Lets first create a nextjs app from scratch. We will add 2 flows in the app - login and registration based on button click from the main page

Use the command npx create-next-app@latest, and create the app

We'll use the below libraries for implementing internationalization

  • i18next

  • react-i18next

  • i18next-browser-languagedetector

Once the project is created, we will add the structure as shown in the app folder.

app

│ favicon.ico

│ globals.css

│ layout.tsx

│ page.tsx

│

├───components

│ SwitchLocale.tsx

│

└───[locale]

│ home.tsx

│ layout.tsx

│ page.tsx

│

├───login

│ page.tsx

│

└───registration

page.tsx

Let's add the project files

login/page.tsx

const Login = async ({ params }: { params: { locale: 'en' | 'es' } }) => {

return (

<div className='flex flex-col items-center justify-center px-6 py-8'>

<div className='w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700'>

<div className='p-6 space-y-4 md:space-y-6 sm:p-8'>

<form className='space-y-4 md:space-y-6'>

<div>

<label htmlFor='email' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>Email</label>

<input type='email' name='email' id='email'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

placeholder='name@company.com' required />

</div>

<div>

<label htmlFor='password' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>Password</label>

<input type='password' name='password' id='password' placeholder='••••••••'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

required />

</div>

<button type='submit'

className='w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'

>Login </button>

<p className='text-sm font-light text-gray-500 dark:text-gray-400'>Don't have an account yet?<a href='#'>Sign up</a></p>

<button type='submit'

className='w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'>Back</button>

</form>

</div>

</div>

</div>

);

};

export default Login;

registration/page.tsx

const Registration = async ({ params }: { params: { locale: 'en' | 'es' } }) => {

return (

<div className='flex flex-col items-center justify-center px-6 py-8'>

<div className='w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700'>

<div className='p-6 space-y-4 md:space-y-6 sm:p-8'>

<form className='space-y-4 md:space-y-6'>

<div>

<label htmlFor='name' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>Name</label>

<input type='name' name='name' id='name' placeholder='xyz'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500' required />

</div>

<div>

<label htmlFor='email' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>Email</label>

<input type='email' name='email' id='email'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

placeholder='name@company.com' required />

</div>

<div>

<label htmlFor='password' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>{t('login.password')}</label>

<input type='password' name='password' id='password' placeholder='••••••••'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

required />

</div>

<button type='submit'

className='w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'>Register </button>

<button type='submit'

className='w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'>Back</button>

</form>

</div>

</div>

</div>

);

};

export default Registration ;

Setup i18n configuration for internationalization

Lets create a new folder called i18n in the root (outside app) and add the below files to set up the i18n configuration that will be used by our applciation for proper translations

i18n/settings.ts

import type {InitOptions} from 'i18next';

export const fallbackLng = 'en';

export const locales = [fallbackLng, 'es'] as const;

export type Locales = (typeof locales)[number];

export const defaultNS = 'common';

export function getOptions(lang = fallbackLng, ns = [defaultNS]): InitOptions {

return {

// debug: true, // Set to true to see console logs

supportedLngs: locales,

fallbackLng,

lng: lang,

fallbackNS: defaultNS,

defaultNS,

ns,

};

}

Lets add a backend file to lazy load the namsepace files

i18n/backend.ts

import { BackendModule, InitOptions, ReadCallback, Services } from "i18next";

const backend: BackendModule = {

type: "backend",

init: function (

services: Services,

backendOptions: object,

i18nextOptions: InitOptions<object>

): void {},

read: function (

language: string,

namespace: string,

callback: ReadCallback

): void {

import(`./locales/${language}/${namespace}.json`).then((object) => {

callback(null, object);

});

},

};

export default backend;

Lets add a server.ts file to import the translations for server side components

i18n/server.ts

import {initReactI18next} from 'react-i18next/initReactI18next';

import {getOptions, Locales} from './settings';

import backend from './backend';

import { createInstance } from 'i18next';

// Initialize the i18n instance

const initI18next = async (lang: Locales, ns: string[]) => {

const i18nInstance = createInstance();

await i18nInstance .use(initReactI18next) .use(backend) .init(getOptions(lang, ns));

return i18nInstance;

};

// It will accept the locale and namespace array for i18next to know what file to load

export async function buildTranslation(lang: Locales, ns: string[]) {

const i18nextInstance = await initI18next(lang, ns);

return {t: i18nextInstance.getFixedT(lang, Array.isArray(ns) ? ns[0] : ns), };

}

Please note that namespaces used throughout refer to localized translation files that we will maintain in the project.

Lets add few namespace files for login. The backend.ts will import the translation files as and when required

Lets add a common namespace file to hold all the translations for both login and registration flow. Lets add couple of languages - english and spanish to implement the internationalization feature

i18n/locales/en/common.json - This is to hold the english translations

{

"login": {

"email": "Email",

"password": "Password",

"remember_me": "Remember me",

"sign_up": "Sign up",

"no_account": "Don't have an account yet?",

"back": "Back",

"sign_in": "Log In",

"login": "Login"

},

"registration": {

"email": "Email",

"password": "Password",

"remember_me": "Remember me",

"sign_up": "Sign up",

"no_account": "Don't have an account yet?",

"back": "Back",

"sign_in": "Log In",

"login": "Login"

}

}

i18n/locales/es/common.json - This is to hold the spanish translations

{

"login": {

"email": "Correo electrónico",

"password": "Contraseña",

"remember_me": "Remember me",

"sign_up": "Inscribirse",

"no_account": "¿Aún no tienes una cuenta?",

"back": "Atrás",

"sign_in": "Acceso",

"login": "Acceso"

},

"registration": {

"name": "Nombre",

"email": "Correo electrónico",

"password": "Contraseña",

"register": "Registro",

"back": "Atrás"

}

}

That's all is required to set up the i18n configuration in the application.

Once i18n is wired in the app, and translation files are added, we need to make the changes in login and registration file to accept the translations based on locale

login/page.tsx

import { buildTranslation } from '@/i18n/server';

const Login = async ({ params }: { params: { locale: 'en' | 'es' } }) => {

const { t } = await buildTranslation(params.locale, ['common']);

return (

<div className='flex flex-col items-center justify-center px-6 py-8'>

<div className='w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700'>

<div className='p-6 space-y-4 md:space-y-6 sm:p-8'>

<form className='space-y-4 md:space-y-6'>

<div>

<label htmlFor='email' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>{t('login.email')}</label>

<input type='email' name='email' id='email'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

placeholder='name@company.com' required />

</div>

<div>

<label htmlFor='password' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>{t('login.password')}</label>

<input type='password' name='password' id='password' placeholder='••••••••'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

required />

</div>

<button type='submit'

className='w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'

>{t('login.login')}</button>

<p className='text-sm font-light text-gray-500 dark:text-gray-400'>{t('login.no_account')} <a href='#'>Sign up</a></p>

<button type='submit'

className='w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'>{t('login.back')}</button>

</form>

</div>

</div>

</div>

);

};

export default Login;

login/registration.tsx

import { buildTranslation } from '@/i18n/server';

const Registration = async ({ params }: { params: { locale: 'en' | 'es' } }) => {

const { t } = await buildTranslation(params.locale, ['common']);

return (

<div className='flex flex-col items-center justify-center px-6 py-8'>

<div className='w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700'>

<div className='p-6 space-y-4 md:space-y-6 sm:p-8'>

<form className='space-y-4 md:space-y-6'>

<div>

<label htmlFor='name' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>Name</label>

<input type='name' name='name' id='name' placeholder='xyz'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500' required />

</div>

<div>

<label htmlFor='email' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>{t('registration.email')}</label>

<input type='email' name='email' id='email'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

placeholder='name@company.com' required />

</div>

<div>

<label htmlFor='password' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>{t('registration.password')}</label>

<input type='password' name='password' id='password' placeholder='••••••••'

className='bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'

required />

</div>

<button type='submit'

className='w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'>{t('registration.register')}</button>

<button type='submit'

className='w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'>{t('registration.back')}</button>

</form>

</div>

</div>

</div>

);

};

export default Registration ;

We are all done with the login changes, you can refer the login flow to create namespace files for registration flow and add the corresponding translations. Lets add page.tsx and layout files to integrate the login and registration flows.

[locale]/page.tsx

import { HomeFlow } from './home';

const Home = async ({ params }: { params: { locale: 'en' | 'es' } }) => {

return (

<section className='bg-gray-50 dark:bg-gray-900'>

<div className='flex flex-col items-center px-6 py-8 mx-auto md:h-screen lg:py-0'>

<div className='w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700'>

<div className='p-6 space-y-4 md:space-y-6 sm:p-8'>

<h1 className='text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white'>

Choose action

</h1>

</div>

<HomeFlow />

</div>

</div>

</section>

);

};

export default Home;

[locale]/layout.tsx

import React from 'react';

const Layout = ({ children }: Readonly<{ children: React.ReactNode }>) => {

return (<>{children}</>);

};

export default Layout;

[locale]/home.tsx

'use client';

import { useParams, useRouter } from 'next/navigation';

export const HomeFlow = () => {

const router = useRouter();

const locale = useParams()?.locale;

const showLogin = () => {

router.replace(`/${locale}/login`);

};

const showRegister = () => {

router.replace(`/${locale}/registration`);

};

return (

<div className='items-center flex flex-col justify-center items-center px-6 py-8'>

<button

onClick={showLogin}

className='w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'

>

Login

</button>

<button

onClick={showRegister}

className='w-full text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800'

>

Register

</button>

</div>

);

};

Let's start the server now

npm run dev

Once started, let's navigate to http://localhost:3000/en. You should get the below page

Lets try to add a language selector now in the home page. Lets add a component called LangChooser.tsx

components/SwitchLocale.tsx

'use client';

import React, { useState } from 'react';

import {

useParams,

useRouter,

useSelectedLayoutSegments,

} from 'next/navigation';

const SwitchLocale = () => {

const router = useRouter();

const urlSegments = useSelectedLayoutSegments();

const lang = useParams()['locale'];

const [locale, setLocale] = useState(lang);

const handleLocaleChange = (val: string) => {

router.push(`/${val}/${urlSegments.join('/')}`);

};

return (

<div className='flex flex-col p-6 pt-10 bg-blue-500 bg-opacity-50'>

<div className='-mt-6 flex gap-1.5 flex flex-row justify-center items-center px-6 py-8'>

{' '}

<div>

<button className={locale == 'en' ? 'underline text-white' : 'no-underline'} onClick={() => handleLocaleChange('en')}>

{' '}

ENGLISH

</button>

</div>

<div>

<button className={locale == 'es' ? 'underline text-white' : 'no-underline'} onClick={() => handleLocaleChange('es')}>

{' '}

SPANISH

</button>

</div>

</div>

</div>

);

};

export default SwitchLocale;

Modify the layout file to include the component as below

[locale]/layout.tsx

import React from 'react';

import SwitchLocale from '../components/SwitchLocale';

const Layout = ({ children }: Readonly<{ children: React.ReactNode }>) => {

return (<><SwitchLocale/>{children}</>);

};

export default Layout;

Rerun the server now - npm run dev

Once started, let's navigate to http://localhost:3000/en. You should get the below page

Let's click on Login and Register Page and try to change the language to english and spanish. The pages will be translated accordingly

Thats it!!!!!!!!! We have enabled the translations implementation for server side components.

But although the login and registration pages are translated propely, we are still not able to do the translations for Login and Register button in the home page. The reason being the home component is a client component using 'use client'.

Lets check how to enable the translations for client side components.

Lets add a file named client.ts in i18n folder

i18n/client.ts

'use client';

import {useEffect} from 'react';

import i18next, {i18n} from 'i18next';

import {initReactI18next, useTranslation as useTransAlias} from 'react-i18next';

import LanguageDetector from 'i18next-browser-languagedetector';

import {type Locales, getOptions, locales} from './settings';

import backend from './backend';

const runsOnServerSide = typeof window === 'undefined';

// Initialize i18next for the client side

i18next .use(initReactI18next).use(LanguageDetector) .use(backend )

.init({ ...getOptions(),

lng: undefined, // detect the language on the client

detection: {

order: ['path'],

},

preload: runsOnServerSide ? locales : [],

});

export function useTranslation(lng: Locales, ns: string[]) {

const translator = useTransAlias(ns);

const {i18n} = translator;

// Run when content is rendered on server side

if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {

i18n.changeLanguage(lng);

} else {

useTransCustom(i18n, lng)

}

return translator;

}

function useTransCustom(i18n: i18n, lng: Locales) {

if (!lng || i18n.resolvedLanguage === lng) return;

i18n.changeLanguage(lng);

}

Lets modify home.tsx to add the translations

[locale]/home.tsx

'use client'

import { useTranslation } from "@/i18n/client";

import { useParams, useRouter } from "next/navigation";

export const HomeFlow = () => {

const router = useRouter();

const locale = useParams()?.locale;

const showLogin = () => {

router.replace(`/${locale}/login`)

}

const showRegister = () => {

router.replace(`/${locale}/register`)

}

const { t } = useTranslation(locale as 'en' | 'ms', ['login'])

return (

<div>

<button onClick={showLogin} type="button" className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">{t('login')}</button>

<button onClick={showRegister} type="button" className="focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800">Register</button>

</div>

)

}

Rerun the server now - npm run dev

Once started, let's navigate to http://localhost:3000/en. You should be able to see the button label change on language change

That's all we have in this post. We have implemented both the server side and client side translations using i18next libraries

Client side Translations