DEV Community

Cover image for Tutorial: Internationalize Your Next.js Static Site (with App Router)
RockyStrongo
RockyStrongo

Posted on

Tutorial: Internationalize Your Next.js Static Site (with App Router)

One of the amazing features that Next.js offers is Static Export. With static export enabled, Next.js generates static HTML/CSS/JS files based on your entire React application.

This approach has many benefits, particularly for performance and SEO. This also means that your app can be deployed and hosted on any web server that can serve HTML/CSS/JS static assets, such as a simple and affordable nginx configuration.

While working with static exports, one problem I came across is i18n and the ability to translate my app content into multiple languages. I found very few online resources about this topic, especially when working with the App Router introduced in Next.js version 13.

Let's work together to create a basic internationalized app that demonstrates how to achieve this!

You can find the working demo project here : https://github.com/RockyStrongo/next-i18n-static/

Step 1: Initialize the project

To create a new Next.js project, run the command below and follow the instructions.

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

We will use TypeScript, Tailwind and App Router for the purpose of this example.

We can now start replacing the default project with the content of our application.

Our requirements are as follows :

  • A Header with two links : Home and About
  • A home page including the text "Hello World"
  • An about page with the text "This is a fully translated static website"
  • All texts should be translated in English and French
  • A language switcher should be available in the Header

1 .1 Create a Header component :

Create a components folder and a Header component

// components/Header.tsx

import Link from "next/link";

export default function Header() {
    return (
        <div className="bg-gray-200 w-screen shadow">
            <nav className="container flex px-2 py-2 gap-5 ">
                <Link href="/">Home</Link>
                <Link href="/about">About</Link>
            </nav>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

1.2 Update the homepage with 'Hello World'

// app/page.tsx

export default function Home() {
  return (
    <div>
      Hello World!
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

1.3 Include the header in the app layout :

// app/layout.tsx

import './globals.css'
import type { Metadata } from 'next'
import Header from '@/components/Header'

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className='bg-gray-100'>
        <Header />
        <div className='p-5'>
          {children}
        </div>
      </body>
    </html>
  )
}
Enter fullscreen mode Exit fullscreen mode

1.4 Create the about page :

// app/about/page.tsx

export default function AboutPage() {
    return (
        <div>
            This application is a fully translated static website
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

1.5 Update the next.config.js file to enable static export

Thanks to this config, when running npm run build, Next.js will generate static HTML/CSS/JS files in the out folder.

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
}

module.exports = nextConfig
Enter fullscreen mode Exit fullscreen mode

At this stage, we have implemented the basic features of our app, but we haven't incorporated any translation or i18n logic yet. All the text is currently hardcoded.

Step 2: internationalize the project

We will be using the library next-intl, which is frequently maintained and popular in the Next.js community. It is mentioned in the Next.js official documentation in the internationalization section.

npm install next-intl
Enter fullscreen mode Exit fullscreen mode

2.1 Create translation files

File messages/en.json

{
    "Homepage": {
        "helloWorld": "Hello World !"
    },
    "AboutPage": {
        "aboutText": "This is a fully translated static website"
    },
    "Header": {
        "home": "Home",
        "about": "About"
    }
}
Enter fullscreen mode Exit fullscreen mode

File messages/fr.json

{
    "HomePage": {
        "helloWorld": "Bonjour tout le monde !"
    },
    "AboutPage": {
        "aboutText": "Ceci est un site statique complètement traduit."
    },
    "Header": {
        "home": "Accueil",
        "about": "A propos"
    }
}
Enter fullscreen mode Exit fullscreen mode

2.2 Update the file structure

├── messages​
│   ├── en.json​
│   └── fr.json​
└── app​
   └── [locale]​
        ├── layout.tsx​
        └── page.tsx​
       └── layout.tsx​
       └── page.tsx​
       └── not-found.tsx​
Enter fullscreen mode Exit fullscreen mode

First, create the [locale] folder and move the existing page.tsx file, layout.tsx file and the about folder inside it. Do not forget to update the imports.

Create an app/not-found.tsx file, this will be our error page in case a user enters an incorrect url.

// app/not-found.tsx

export default function NotFound() {
    return (
        <div>
            Custom 404 Page
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Create a new /app/layout.tsx file :

// app/layout.tsx

import {ReactNode} from 'react';

type Props = {
  children: ReactNode;
};

// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({children}: Props) {
  return children;
}
Enter fullscreen mode Exit fullscreen mode

create a new app/page.tsx file :
In this page, we redirect the users to the default language, in our case English.

NOTE: with static export, no default locale can be used without a prefix. We have to redirect incoming requests to the default language. As explained in the docs.

// app/page.tsx

import {redirect} from 'next/navigation';

export default function RootPage() {
  redirect('/en');
}
Enter fullscreen mode Exit fullscreen mode

2.3 Update app/[locale]/layout.tsx

// app/[locale]/layout.tsx

import '../globals.css'
import type { Metadata } from 'next'
import Header from '@/components/Header'
import { ReactNode } from 'react'
import { notFound } from 'next/navigation'
import { NextIntlClientProvider } from 'next-intl'

type Props = {
  children: ReactNode
  params: { locale: string }
}

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

//function to get the translations
async function getMessages(locale: string) {
  try {
    return (await import(`../../messages/${locale}.json`)).default
  } catch (error) {
    notFound()
  }
}

//function to generate the routes for all the locales
export async function generateStaticParams() {
  return ['en', 'fr'].map((locale) => ({ locale }))
}

export default async function RootLayout({
  children,
  params: { locale },
}: Props) {
  const messages = await getMessages(locale)

  return (
    <html lang="en">
      <body className='bg-gray-100'>
        <NextIntlClientProvider locale={locale} messages={messages}>
          <Header />
          <div className='p-5'>
            {children}
          </div>
        </NextIntlClientProvider>
      </body>
    </html>
  )
}
Enter fullscreen mode Exit fullscreen mode

What we have done here :

  • Add a getMessages function to get the translations
  • Add a generateStaticParams function to generate the static routes for all the locales
  • Add the context provider NextIntlClientProvider to make our translations available in all of the app pages

2.4 Update the pages/components to use the translations

// app/page.tsx

'use client'
import { useTranslations } from 'next-intl'

export default function HomePage() {
  const t = useTranslations('HomePage')

  return (
    <div>
      {t('helloWorld')}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

What we have done here :

  • Add 'use client' (as of today, translations with next-intl are only supported in client components)
  • Import the useTranslations hook and use it in our jsx

Apply the same to other pages/components :

//app/[locale]/about/page.tsx

'use client'
import { useTranslations } from 'next-intl'

export default function AboutPage() {
    const t = useTranslations('AboutPage')

    return (
        <div>
            {t('aboutText')}
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode
// components/Header.tsx
'use client'
import Link from "next/link";
import { useTranslations } from 'next-intl'

export default function Header() {
    const t= useTranslations('Header')
    return (
        <div className="bg-gray-200 w-screen shadow">
            <nav className="container flex px-2 py-2 gap-5 ">
                <Link href="/">{t('home')}</Link>
                <Link href="/about">{t('about')}</Link>
            </nav>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

2.5 Update the links to take into account the locale prefix

Instead of using the Link provided by Next.js or the element, always use the component 'next-intl/link' provided by next-intl :

// components/Header.tsx

'use client'
import Link from 'next-intl/link';
import { useTranslations } from 'next-intl'

export default function Header() {
    const t= useTranslations('Header')
    return (
        <div className="bg-gray-200 w-screen shadow">
            <nav className="container flex px-2 py-2 gap-5 ">
                <Link href="/">{t('home')}</Link>
                <Link href="/about">{t('about')}</Link>
            </nav>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

If you run your app, you will notice that you are immediately redirected from http://localhost/3000 to http://localhost/3000/en

The header links are taking into account the 'en' prefix. If you change it from 'en' to 'fr', you will see your website translated in French.

We are almost done ! All is left in our requirement list is the local switcher!

Step 3 - Add a locale switcher

3.1 Create the necessary translations.

File messages/en.json

    "LocaleSwitcher": {
        "locale": "{locale, select, fr {French} en {English} other {Unknown}}"
    }
Enter fullscreen mode Exit fullscreen mode

File messages/fr.json

    "LocaleSwitcher": {
        "locale": "{locale, select, fr {français} en {anglais} other {inconnu}}"
    }
Enter fullscreen mode Exit fullscreen mode

This syntax from next-intl will help us to implement a multiple value select switcher. You can find the documentation for message syntax here: https://next-intl-docs.vercel.app/docs/usage/messages#rendering-messages

3.2 Create a locale switcher component

// components/LocaleSwitcher.tsx

'use client'
import { useLocale, useTranslations } from 'next-intl';
import { usePathname, useRouter } from 'next-intl/client';

export default function LocaleSwitcher() {
    const t = useTranslations('LocaleSwitcher')
    const locale = useLocale();
    const router = useRouter();
    const pathname = usePathname();

    const onLocaleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const newLocale = e.target.value;
        router.replace(pathname, { locale: newLocale });
    }

    return (
        <select
            defaultValue={locale}
            onChange={onLocaleChange}
        >
            {['en', 'fr'].map((lang) => (
                <option key={lang} value={lang}>
                    {t('locale', { locale: lang })}
                </option>
            ))}
        </select>
    )
}
Enter fullscreen mode Exit fullscreen mode

As a last step, add the locale switcher to the Header component and the job is done ! Our static application is internationnalized 🥳

To build the app, run :

npm run build

The static export is generated in the out folder.

Test the generated output by running :

npx serve@latest out

Through the combination of Next.js's features, the App Router, and the next-intl library, you'll create performant, SEO-friendly, and language-friendly web applications that reach a global user base. Embrace internationalization and elevate your Next.js projects to new heights!

Top comments (2)

Collapse
 
weti09 profile image
Tim

This is not SEO friendly as you can't use or translate your own titles for each page.

Collapse
 
mohamedramadan profile image
Mohammad Ramadan

Great, but is there a better way considering the SEO?