In this article, I will illustrate how to implement next-intl localization in a Next.js 14 application.
Localization is a crucial feature for any application aiming to cater to a global audience, ensuring that users can interact with content in their preferred language.
Step 1: Create a fresh Next.js project.
npx create-next-app i18n-next-app cd i18n-next-app yarn add next@latest
Step 2: Install the next-intl package:
yarn add next-intl
Step 3: Adding the locale route segment
*** First, we need to follow the documents about locale codes and dynamic route segments.
Next, the subdirectory called [locale] is created under src/app.
*** Note, the module import should be updated if needed.
Step 4: Adding the translation message files
First, create a new directory called locales under src
In this tutorial, we are supporting en-us (English USA) and vi (Vietnamese).
Continue, add the content for translation files
// src/locales/en-us/about.ts export default { title: 'Title', description: 'Description', } as const;
// src/locales/vi/about.ts export default { title: 'Tiêu đề', description: 'Miêu tả', } as const;
// src/locales/vi/index.ts // src/locales/vi/index.ts import about from './about'; export default { about };
Step 5: Setting up configuration files
// next.config.mjs import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); /** @type {import('next').NextConfig} */ const nextConfig = {}; export default withNextIntl(nextConfig);
Create a new file called i18n.config.ts
import { createSharedPathnamesNavigation } from 'next-intl/navigation'; export const locales = ['en-us', 'vi'] as const; export type Locale = (typeof locales)[number]; export const localeNames: Record<Locale, string> = { 'en-us': 'Tiếng anh', vi: 'Vietnamese', }; export const { Link, usePathname, useRouter } = createSharedPathnamesNavigation({ locales });
The next-intl uses the i18n.ts file to load translations.
// src/global.d.ts type Messages = (typeof import('./locales/en-us'))['default']; declare interface IntlMessages extends Messages {}
// src/i18n.ts import { getRequestConfig } from 'next-intl/server'; import { notFound } from 'next/navigation'; import { type Locale, locales } from ' ./i18n.config'; export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale as Locale)) { return notFound(); } const messages = await import(`./locales/${locale}/index.ts`); return { messages: messages.default, }; });
Step 6: Create a localization middleware
// src/middlewares/with-localization.ts import createMiddleware from 'next-intl/middleware'; import { NextMiddleware } from 'next/server'; import { locales } from '../i18n.config'; import { MiddlewareFactory } from './types'; export const withLocalization: MiddlewareFactory = (next: NextMiddleware) => createMiddleware({ defaultLocale: 'vi', locales, localeDetection: false, });
// src/middleware.ts import { stackMiddlewares } from './middlewares/stack-middlewares'; import { withLocalization } from './middlewares/with-localization'; export default stackMiddlewares([ withLocalization, ]); export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
Step 7: How to retrieve translated contents
// src/hooks/useTextDirection.ts import { useLocale } from 'next-intl'; import { isRtlLang } from 'rtl-detect'; export type TextDirection = 'ltr' | 'rtl'; export default function useTextDirection(): TextDirection { const locale = useLocale(); return isRtlLang(locale) ? 'rtl' : 'ltr'; }
// src/[locale]/layout.tsx import { Metadata, Viewport } from 'next'; import { NextIntlClientProvider, useMessages } from 'next-intl'; import { unstable_setRequestLocale } from 'next-intl/server'; import { locales } from './src/i18n.config'; import useTextDirection from '@/hooks/useTextDirection'; import './globals.css'; export async function generateMetadata(): Promise<Metadata> { return { title: { default: site.header, template: `${site.header} | %s`, }, }; } export function generateStaticParams() { return locales.map((locale) => ({ locale })); } export default function RootLayout({ children, params: { locale }, }: Readonly<{ children: React.ReactNode; params: { locale: string }; }>) { unstable_setRequestLocale(locale); const dir = useTextDirection(); const messages = useMessages(); return ( <html lang={locale} dir={dir} className={combineClasses([roboto.className])}> <head></head> <body> <NextIntlClientProvider messages={messages}> {children} </NextIntlClientProvider> </body> </html> ); }
// src/app/[locale]/about.page.tsx import { getTranslations } from 'next-intl/server'; export async function generateMetadata({ params: { locale }, }: { params: { locale: string }; }): Promise<Metadata> { const t = await getTranslations({ locale, }); return { title: t('about.title'), }; } export default async function About({ params: { locale } }: { params: { locale: string } }) { const t = useTranslations('about'); return ( <Layout> {t('description')} </Layout> ); }
Vietnamese: https://www.dinhthanhcong.info/vi/about
English: https://dinhthanhcong.info/en-us/about
By following these guidelines, you can build a Next.js 14 application that meets the technical requirements of localization and delivers a superior user experience for a diverse audience.
Good luck to you, I hope this post is of value to you!!!!
Reference Documents: