Making a multilingual site with Next.js - Part 3
Tags: webdev, react, nextjs, javascript
Published: 2021-04-05 21:56:44

If you ended up here for this third part and did not see the [first](https://blog.elvessousa.com.br/post/next-intl) nor the [second](https://blog.elvessousa.com.br/post/next-intl-2), I highly suggest you take a look at those first. In the previous section, we dealt with the creation and listing of content for the languages and ended the project there.

However, some commented that it would be interesting to add translated slugs, for example: in English the "about" page open at `site.com/en/about` and its corresponding Portuguese version open at `site.com/pt/sobre`. In this article, I show you how we can create such functionality. Let's start!

### But first...

In the previous articles, the function for changing languages was implemented. But when the page was refreshed, it returned to the default language, which caused a certain annoyance. This behavior is not the best, so it is important to solve this issue. Fortunately, it is not difficult at all to implement, with just a few lines of code.

#### Local Storage

**Local Storage** is a way that JavaScript provides us to save information in the user's browser, so that it will be available on a next visit. Many use it to make simple authentication or to save options, such as light and dark modes, for example.

The logic used here does not differ from that of a theme change, the change is that language will be saved instead. Small modifications to only two files are needed. The files are: the `Header` component and the`LanguageProvider` language context. If you fell from another dimension and did not see the two previous articles and nothing makes sense to you until now, I warned you at the beginning of the article! Go there and check the previous articles, and then come back here!

Here is the code for the **Header** component:

```tsx
import { useContext } from "react"
import { useRouter } from "next/router"

import Navigation from "../Navigation"
import Logo from "../Logo"
import { LanguageContext, locales } from "../../intl/LanguageProvider"

interface Props {
  className?: string
  children?: React.ReactNode
}

const Header: React.FC<Props> = ({ className, children }) => {
  const headerClass = className || "header"
  const [locale, setLocale] = useContext(LanguageContext)
  const router = useRouter()

  function handleLocaleChange(language: string) {
    if (!window) {
      return
    }

    const regex = new RegExp(`^/(${locales.join("|")})`)
    localStorage.setItem("lang", language) // This line saves the language option!
    setLocale(language)

    router.push(router.pathname, router.asPath.replace(regex, `/${language}`))
  }

  return (
    <header className={headerClass}>
      <Logo link={`/`} />
      <Navigation />
      {children}
      <div className="lang">
        <button onClick={() => handleLocaleChange("en")}>EN</button>
        <button onClick={() => handleLocaleChange("pt")}>PT</button>
      </div>
    </header>
  )
}

export default Header
```

In _Header_, the method `localStorage.setItem ('lang', language)` was used to save the language choice by clicking on the corresponding button. What this method does is basically add a _'lang'_ key with the acronym of the chosen language. You can check this in the **Application** area of your browser's inspector, in the **Local Storage** section.

The _LanguageProvider_ is as follows:

```tsx
import { createContext, useEffect, useState } from "react"

export const defaultLocale = "pt"
export const locales = ["pt", "en"]
export const LanguageContext = createContext([])

export const LanguageProvider: React.FC = ({ children }) => {
  const [locale, setLocale] = useState("pt")

  useEffect(() => {
    if (!window) {
      return
    }
    // Captures the language information saved by the Header component
    const language = localStorage.getItem("lang") || locale
    setLocale(language)
  }, [locale])

  return (
    <LanguageContext.Provider value={[locale, setLocale]}>
      {children}
    </LanguageContext.Provider>
  )
}
```

Here the `localStorage.getItem ('lang')` method captures the saved information from the language choice, and applies it if it exists. Now when updating the page, the language you selected stays there.

### Finally... Let's create the translated slugs...

Nothing prevents you from creating files in the `/pages` folder, with the desired title, such as `/kontakt.tsx` for a contact page in German. It will work perfectly, but let's be honest: it is not the best way to do the job. We should be able to provide a way for pages to be created dynamically, with a standard template, changing the content and slug according to the language.

If you think about it, a similar thing is done with our posts area in this project. To achieve this, just modify the library we created for the posts (`/lib/posts.ts`) to include our new translated pages. But avoid duplicate code, instead of creating a `/lib/pages.ts` file with practically the same content as `/lib/posts`, I decided to unify everything in a single library that I called `lib/files.ts`.

The content of this file is as follows:

```typescript
import fs from "fs"
import path from "path"
import matter, { GrayMatterFile } from "gray-matter"
import remark from "remark"
import html from "remark-html"

const postsDirectory = path.resolve(process.cwd(), "content", "posts")
const pagesDirectory = path.resolve(process.cwd(), "content", "pages")

// Collects all file names in the folders specified with the sctructure ['en/filename.md']
export function getAllFileNames(directoryPath: string, filesList = []) {
  const files = fs.readdirSync(directoryPath)

  files.forEach((file) => {
    if (fs.statSync(`${directoryPath}/${file}`).isDirectory()) {
      filesList = getAllFileNames(`${directoryPath}/${file}`, filesList)
    } else {
      filesList.push(path.join(path.basename(directoryPath), "/", file))
    }
  })

  const filteredList = filesList.filter((file) => file.includes(".md"))
  return filteredList
}

// Sorts posts by date
export function getSortedPostData() {
  const fileNames = getAllFileNames(postsDirectory)

  const allPostsData = fileNames.map((fileName) => {
    const id = fileName.split("/")[1].replace(/\.md$/, "")
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, "utf-8")
    const frontMatter: GrayMatterFile<string> = matter(fileContents)

    return {
      id,
      ...(frontMatter.data as {
        lang: string
        date: string
        category: string
      }),
    }
  })

  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

// IDs for posts or pages
export function getAllIds(type = "post") {
  const dir = type === "page" ? pagesDirectory : postsDirectory
  const fileNames = getAllFileNames(dir)

  return fileNames.map((fileName) => ({
    params: {
      id: fileName.split("/")[1].replace(/\.md$/, ""),
      lang: fileName.split("/")[0],
    },
  }))
}

// Collects data from the markdown file and makes it available
export async function getContentData(id: string, type = "post") {
  const dir = type === "page" ? pagesDirectory : postsDirectory
  const fullPath = path.join(dir, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, "utf-8")
  const frontMatter = matter(fileContents)

  const processedContent = await remark().use(html).process(frontMatter.content)

  const contentHtml = processedContent.toString()

  return {
    id,
    ...(frontMatter.data as { date: string; title: string }),
    contentHtml,
  }
}
```

I created a `type` argument in some of the functions that will be used by both posts and pages. This because this argument identifies the directory in which the files will be read. By default, I left it configured to always search for posts. Since the file name has changed and so have the functions, it is necessary to update the _imports_ in the files that use the new library.

### Template for the dynamic page

Here is another page with a special name, to create a dynamic route. In this the parameter will be the 'id' of the file, which is captured by the function `getAllIds()` of the file `lib/files`. The file will be called `[lang]/[id].tsx`. Below is the complete code of the file.

```tsx
import { GetStaticProps, GetStaticPaths, NextPage } from "next"

import { getAllIds, getContentData } from "../../lib/files"
import Layout from "../../components/Layout"

interface PageProps {
  locale: string
  pageData: {
    lang: string
    title: string
    slug: string
    date: string
    category?: string
    contentHtml: string
  }
}

const SitePage: NextPage<PageProps> = ({ pageData }) => {
  const { title, contentHtml } = pageData

  return (
    <Layout title={title}>
      <article className="post-content">
        <h1>{title}</h1>
        <div
          className="post-text"
          dangerouslySetInnerHTML={{ __html: contentHtml }}
        />
      </article>
    </Layout>
  )
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  // Here is the argument to informa "page" as type,
  // so Next.js can search for page files, ignoring posts.
  const pageData = await getContentData(`/${params.lang}/${params.id}`, "page")

  return {
    props: {
      locale: params?.lang || "pt",
      pageData,
    },
  }
}

export const getStaticPaths: GetStaticPaths = async () => {
  // Here is the argument to informa "page" as type,
  // so Next.js can search for page files, ignoring posts.
  const paths = getAllIds("page")

  return {
    paths,
    fallback: false,
  }
}

export default SitePage
```

With this file, it is already possible to support pages created through Markdown. The markdown files use the following structure:

```markdown
---
lang: pt
title: "Sobre"
---

Site made to showcase the creation of a bilingual website using Next.js. The tutorial is in an article on my blog. Feel free to view the source code, fork it, or even use it in your projects.
```

To better organize the files, I created a directory called `/content` in the root of the project, and in it another two: `posts` and `pages`. These will receive the markdown files in the directories for each language supported on the website. With the code presented here, the creation of the pages is fully automated and based on this structure.

### Wrapping it up

I believe that now we already have a very functional example of a multilingual website using Next.js. You can create content for many languages and let the user choose one to use in your site.

Comments, suggestions and questions are welcome, leave it below. I also provided the link to the complete project repo on GitHub, in case you want to see the complete code. If you encounter an error, you can leave your issue there too.

See you!

### Links

- [Portuguese Version of this article](https://blog.elvessousa.com.br/post/next-multilingue-3)
- [First part of this tutorial](https://blog.elvessousa.com.br/post/next-intl)
- [Second part of this tutorial](https://blog.elvessousa.com.br/post/next-intl-2)
- [Repo on GitHub](https://github.com/elvessousa/next-intl)
- [Site made with this code](https://next-intl-ten.vercel.app/)

---

If this article helped you in some way, consider [donating](https://www.buymeacoffee.com/elvessousa). This will help me to create more content like this!