Handling i18n within app dir

With app dir you create and control the route segments related to i18n. Combining generateStaticParams with layouts or pages we can get what we need for an internationalized app.

Creating the files

We will create a [lang] directory as a parent of all the routes in our app that need to be internationalized. In case all your app needs to be, create it on the root of the app directory. We will also create a dictionaries directory with the translations for each language.

|/app
|__/[lang]
|____/dictionaries
|______/en.json
|______/es.json
|____/page.js

Using translations

Now, inside the [lang]/page.js file we will have access to the lang param that we can use to import the translations and use them in our page.

export default async function Home({ params }) {
  const { home } = await import(`./dictionaries/${params.lang}.json`);
  
  return (
    <h1>{home.title}</h1>
  )
}

If we want to generate these pages statically at build time we can add generateStaticParams to our page.

export async function generateStaticParams() {
  return [{ lang: "es" }, { lang: "en" }, { lang: "de" }];
}

Now this page will generate all three languages statically at build time. We can see the example below.

Welcome to Next.js 13!

Because all our components are server components, all these translations were handled on the server and just the HTML output is sent to the frontend, so no need to filter out the unused keys. But keep in mind that the object is kept in memory on the server so ensure you split your translations files in something that does not affect your server resources.

Usage in client components

Once we reach a client component we lose the ability to get translations from the server, so the safest approach would be to load the translations from the nearest server component parent and send them through props to our client component. Once translations are there, they can be drilled to other components, handled by a context + hook or whatever solution you currently use for handling client side translations.

export default async function Home({ params }) {
  const { counter } = await import(`./dictionaries/${params.lang}.json`);
  
  return (
    <div>
      <h1>{home.title}</h1>
      <Counter translations={counter} />
    </div>
  )
}
'use client'

import { useState } from "react";

export default function Counter({ translations }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count => count - 1)}>{translations.decrement}</button>
      <span>{count}</span>
      <button onClick={() => setCount(count => count + 1)}>{translations.increment}</button>
    </div>
  )
}

And it will display like this.

0