DEV Community

Martin Ratinaud
Martin Ratinaud

Posted on • Edited on

Internationalize NextJs URLs with next-translate (Part 1)

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 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

Prerequisites

This tutorial is using the excellent 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

{
  "/": {
    "fr": "/accueil"
  }
}
Enter fullscreen mode Exit fullscreen mode

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 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

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

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];
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

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

- const nextTranslate = require('next-translate-plugin')
+ const nextTranslate = require('./src/modules/I18n/next.config');
Enter fullscreen mode Exit fullscreen mode

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

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

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

- import Link from 'next/link';
+ import { Link } from 'modules/I18n';
Enter fullscreen mode Exit fullscreen mode

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

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

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

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;
Enter fullscreen mode Exit fullscreen mode

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

Image description

Reference

Check out the Github Repo

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 and many more...

Check my LinkedIn and get in touch!

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

Top comments (0)