Internationalize NextJs URLs with next-translate (Part 1)
Tags: nextjs, i18n, javascript
Published: 2023-03-17 12:19:04

I18n URLs have been available in all major static sites generator but is somehow lacking in NextJs and this is definitely **a pity 😔**.

I'm french and always create [my websites](http://indiehackers.com/martinratinaud/history) in at least french and english.

Here is a detailed explanation on how any developer can achieve this in **less than 10 min** and finally be able to handle urls such as

- /
- /fr/accueil

## TLDR

Repository with all source code can be found **[on GitHub](https://github.com/martinratinaud/next-translate-i18n-routes)**

## Prerequisites

_This tutorial is using the excellent [next-translate](https://github.com/aralroca/next-translate) library but can be adapted for next-i18next._

- having next-translate already setup in your project

## What will be done

- Generate `rewrites` rules that will make the URLs adapt accordingly to the language selected by the user
- Supercharge the `nextTranslate` function to use those `rewrites`
- Write a custom `Link` component that will use those `rewrites` to generate corresponding i18n slugs and use them for navigation

## Procedure

In all the following steps, files and functions created will be put in a `modules/I18n` folder.
This is a practice I came up with after many years of programming and that helps a lot for separating parts of applications (In this case, all I18n related logic).
I will soon write a blog post about it.

### Specify permalinks

First thing to be done is to specify the permalinks we want to use.
Let's create a `modules/I18n/permalinks.json`

```json
{
  "/": {
    "fr": "/accueil"
  }
}
```

**NOTE**: this is not an ideal solution for me as it separates the actual page (the jsx file) from the definition of its permalinks and it would be better to have an `export const permalinks` from within the page. This problem ias addressed in the [part 2](https://dev.to/martinratinaud/internationalize-nextjs-urls-with-next-translate-part-2-3cf5) of this article (and you can contact me also if you want more info).

### Supercharge `nextTranslate` function

The goal here is to transform the permalinks we created into rewrite rules so that NextJS can rewrite correctly the URL depending on the language.

**TLDR** See commit [on GitHub](https://github.com/martinratinaud/next-translate-i18n-routes/commit/c03e2d74ad8c19e052e1ebb5b23e9babe65492d1)

Create a `modules/I18n/next.config.js` with

```js
const nextTranslate = require('next-translate-plugin');
const fs = require('fs');
const permalinks = require('./permalinks.json');

/**
 * 
 * Transforms
{
  "/": {
    "fr": "/accueil"
  }
}
 * into
[
  {
    source: '/fr/accueil',
    destination: '/fr',
    locale: false
  }
]
*/
const permalinksToRewriteRules = (permalinks) =>
  Object.entries(permalinks).reduce(
    (acc, [originalSlug, permalinks]) => [
      ...acc,
      ...Object.entries(permalinks).reduce(
        (acc2, [locale, i18nSlug]) => [
          ...acc2,
          {
            source: `/${locale}${i18nSlug}`,
            destination: `/${locale}${originalSlug}`,
            locale: false,
          },
        ],
        []
      ),
    ],
    []
  );

module.exports = (nextConfig) => {
  const nextTranslateConfig = nextTranslate(nextConfig);

  return {
    ...nextTranslateConfig,
    async rewrites() {
      const existingRewrites = nextTranslateConfig.rewrites
        ? await nextTranslateConfig.rewrites()
        : [];
      return [...permalinksToRewriteRules(permalinks), ...existingRewrites];
    },
  };
};
```

and replace the call to the function in `next.config.js`

```diff
- const nextTranslate = require('next-translate-plugin')
+ const nextTranslate = require('./src/modules/I18n/next.config');
```

Awesome, now, if you reload your server, you will be able to access

- http://localhost:3000/fr/accueil

Now, let's adapt the Link component to take this new URL into account

### Adapt Link component

Goal is to be able to navigate directly to the nice URLs defined earlier.

**TLDR**: See commit [on GitHub](https://github.com/martinratinaud/next-translate-i18n-routes/commit/24da9244238f65af9c9ab3077f74edc558b698b7)

A new `modules/I18n` component called `Link` has to be created and all imports of `next/link` has to be modified.

Yes, that's really a pain point, I admit but i could not find a way to do other wise.

This is in fact not a big problem as a simple "search and replace" will work

```diff
- import Link from 'next/link';
+ import { Link } from 'modules/I18n';
```

First, the `permalinks` variable has to be exposed to the frontend in order to be used by the `Link` component that will be created.

In nextJs, this is done with

```diff
  return {
    ...nextTranslateConfig,
+    publicRuntimeConfig: {
+      ...nextTranslateConfig.publicRuntimeConfig,
+      permalinks, // add it to publicRuntimeConfig so it can be used by the Link component
+    },
    async rewrites() {
    ...
```

How the nextJS built in `Link` component works is it will build up the URL from the `href` and the passed (or existing) `locale`.

This means a link to `/` in `fr` will lead to `/fr`

This component will create a map of URLs to route directly to the corresponding correct URL `/fr/accueil`

```ts
import React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';

import getConfig from 'next/config';

const { publicRuntimeConfig } = getConfig();
const permalinks: { [key: string]: { [key: string]: string } } =
  publicRuntimeConfig.permalinks || {};

/**
 * Formats permalinks
{
  "/": {
    "fr": "/accueil"
  }
}
 * into
{
  "/fr/": "/fr/accueil",
  "/en/accueil": "/"
} 
 */
export const i18nFallbackUrls: { [key: string]: string } = Object.entries(
  permalinks
).reduce(
  (acc, [originalSlug, permalinks]) => ({
    ...acc,
    ...Object.entries(permalinks || {}).reduce(
      (acc2, [locale, permalink]) => ({
        ...acc2,
        [`/${locale}${originalSlug}`]: `/${locale}${permalink}`,
        [`/en${permalink}`]: originalSlug,
      }),
      {}
    ),
  }),
  {}
);

const I18nLink = ({ href, locale, ...props }: any) => {
  const router = useRouter();
  const wantedLocale = locale || router.locale;
  let i18nProps: any = {
    href,
    locale,
  };

  if (i18nFallbackUrls[`/${wantedLocale}${href}`]) {
    i18nProps = {
      href: i18nFallbackUrls[`/${wantedLocale}${href}`],
      locale: false,
    };
  }

  return <Link {...i18nProps} {...props} />;
};

export default I18nLink;
```

And **voila!**. It's all done and here is how it looks like


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lxja36rhdydohaswfhdm.gif)


## Reference

Check out the [Github Repo](https://github.com/martinratinaud/next-translate-i18n-routes)


## Who am I?
My name is Martin Ratinaud and I’m a senior software engineer and remote enthusiast, contagiously happy and curious.
I create websites like this one for [staking crypto](https://www.stakingcrypto.io/) and [many more...](http://indiehackers.com/martinratinaud/history)

Check my [LinkedIn](https://fr.linkedin.com/in/martinratinaud) and get in touch!

Also, I'm looking for a remote job. Hire me !