Published on

Best Practices for Implementing Internationalization (i18n) in Next.js Static Exports

Authors

Context


Recently, I decided to rewrite one of my side projects using Next.js. I've been learning Next.js and wanted to practice by cleaning up my old code.

However, I encountered a challenge: while there are many i18n libraries available, few support static exports. I found a few libraries that claimed to support this feature, but their documentation was sparse, with only brief instructions. This led to a lengthy research process.

Here are the solutions I found:

next-international

Note: The following code examples are in Typescript and are for Next.js using the App Router.

Installation

First, install next-international.

For npm:

npm install next-international

For yarn:

yarn add next-international

Create server.ts and client.ts

Create the following file structure:

project/
└── locales/
    ├── server.ts
    └── client.ts

server.ts and client.ts:

// server.ts

import { createI18nServer } from 'next-international/server'

export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
    en: () => import('./en'),
    zh: () => import('./zh'),
});
// client.ts

"use client"
import { createI18nClient } from 'next-international/client'

export const { useI18n, useScopedI18n, I18nProviderClient } = createI18nClient({
    en: () => import('./en'),
    zh: () => import('./zh'),
})

Prepare the language files

Create file {language name}.ts under locales folder. Like:

project/
└── locales/
    ├── en.ts
    └── zh.ts
// Example language file: en.ts

export default {
    "header": {
        "home": "Home",
        "blog": "blog"
    },
    "siteName": "Adyingdeath Blog"
} as const

Your current file structure looks like this:

project/
└── locales/
    ├── server.ts
    ├── client.ts
    ├── en.ts
    └── zh.ts

Modify Your Existing Files

Move all your routes into an app/[locale]/ folder:

// Before moving
project/
└── app/
    ├── layout.tsx
    └── page.tsx

// After moving
project/
└── app/
    └── [locale]/
        ├── layout.tsx
        └── page.tsx

Note: The [locale] placeholder is a literal string, not a variable reference. You should name the folder exactly as [locale].

If your app contains client components, wrap the lowest level components with I18nProviderClient within a layout:

// app/[locale]/layout.tsx
import { I18nProviderClient } from '../../locales/client'

/* other codes */

export default async function RootLayout({
    params,
    children,
}: Readonly<{
    params: Promise<{ locale: string }>;
    children: React.ReactNode;
}>) {
    const { locale } = await params;
    setStaticParamsLocale(locale);

    return (
        <I18nProviderClient locale={locale}>
            {children}
        </I18nProviderClient>
    );
}

Note: Add a params property to the layout to capture the [locale] value from the URL. For example, visiting /en/ sets [locale] to en, allowing the layout to access the language parameter via params.

export default async function RootLayout({
    params,
    children,
}: Readonly<{
    params: Promise<{ locale: string }>;
    children: React.ReactNode;
}>)

Add setStaticParamsLocale and getStaticParams

Add the following:

  • Within the root layout, call the setStaticParamsLocale function, passing it the locale page parameter.
  • Export a generateStaticParams function from the root layout.

Note: You can also add setStaticParamsLocale and getStaticParams to individual pages. If you prefer not to add them to the root layout, you can include them in the specific pages where needed. However, keep in mind that these functions can only be used in server components.

// app/[locale]/layout.tsx
import { setStaticParamsLocale } from 'next-international/server'
import { getStaticParams } from '../../locales/server'

export default async function RootLayout({ params: { locale } }: { params: Promise<{ locale: string }> }) {
	const { locale } = await params;
    setStaticParamsLocale(locale);

	return (
        /* ... */
    )
}

export function generateStaticParams() {
	return getStaticParams()
}

Now, your layout.tsx should be something like this:

// app/[locale]/layout.tsx
/* other imports */
import { I18nProviderClient } from '../../locales/client'
import { setStaticParamsLocale } from 'next-international/server'
import { getStaticParams } from '../../locales/server'

/* other code */

export default async function RootLayout({ params: { locale } }: { params: Promise<{ locale: string }> }) {
	const { locale } = await params;
    setStaticParamsLocale(locale);

	return (
        <I18nProviderClient locale={locale}>
            {children}
        </I18nProviderClient>
    );
}

export function generateStaticParams() {
	return getStaticParams()
}

Translate Now

Inside server components:

// Example page.tsx
import { getI18n } from '@/locales/server'

export default async function Page() {
    const t = await getI18n();

    return (
        <div>{t("siteName")}</div>
    )
}

Inside client components:

// Example Header.tsx
"use client";

import { useI18n } from '@/locales/client';

export default function Header() {
    const t = useI18n();

    return (
        <div className='w-full flex items-center p-2 gap-4'>
            {t("header.blog")}
        </div>
    )
}

Static Exports

Inside your next.config.ts, set output to export:

const nextConfig: NextConfig = {
  /* config options here */
  "output": "export",
};

Then, export it to out folder in your project root:

npm run build