Internationalization Using React / NextJS 13 and ChatGPT
Tags: react, nextjs, chatgpt, internationalization
Published: 2023-02-27 12:07:22

Translating content into other languages used to be a very tedious task and required teams of translators and developers. And forget about maintaining or changing the content later. Thankfully, with the development of very smart AI engines, such as Google Translate and ChatGPT, this process can be automated quite easily.

In this tutorial, I'm going to show you how to utilize OpenAI's `text-davinci` API, more commonly known as ChatGPT, in conjunction with NextJS's fabulous automatic locale detection and Static Site Generation (SSG), to automate translation of your website content at built-time. Generating this content at built-time as opposed to run-time dramatically reduces API usage costs while speeding up content delivery.

You'll need an Open AI account and API key in order to utilize this solution. You can create an account and get a key for free, but the API response speed will be very slow and you will be limited to so many tokens per request, day, month, etc. This shouldn't be a problem, though, unless your site is very large with many paragraphs. A large site could take a very long time to build. For more information on creating an OpenAI account, see [this article](https://designly.biz/blog/post/create-a-telephone-ivr-using-chatgpt-nextjs-and-twilio).

The example app included in this article was generated using `create-next-app@latest`. It uses the following libraries:

| Package Name | Description |
| --------------- | ----------- |
| openai | Node.js library for the OpenAI API |
| cookies-next | Getting, setting and removing cookies on both client and server with next.js |
| tailwindcss | A utility-first CSS framework for rapidly building custom user interfaces. |
| postcss | Tool for transforming styles with JS plugins |
| autoprefixer | Parse CSS and add vendor prefixes to CSS rules using values from the Can I Use website |

If you like, you can simply clone the repo, which can be found [here](https://github.com/designly1/nextjs13-translate-example). Also, if you like, you can check out the [demo page](https://nextjs13-translate-example.vercel.app/).

For the purposes of brevity, I will not take you through the process of setting up tailwindcss or creating a NextJS app.  For more information about setting up tailwind, see [this article](https://designly.biz/blog/post/creating-a-modern-hero-image-using-nextjs-13-images-and-tailwind-css).

Now, onward to the code!

The first thing we'll do is create a config file for the locales we want to support:

```js
// localeNames.js

const localeNames = [
    {
        name: 'English (United States)',
        value: 'en-US'
    },
    {
        name: 'English (United Kingdom)',
        value: 'en-UK'
    },
    {
        name: "Chinese (Simplified, People's Republic of China)",
        value: 'zh-CN'
    },
    {
        name: 'French',
        value: 'fr'
    }
]

export default localeNames;
```

In this example, I've included only four languages, but you can include as many as you like. Note that NextJS uses the ISO 639 format for locales, which is `[language-code]-[COUNTRY-CODE]`. For a list of these codes, check out [this page](https://www.alchemysoftware.com/livedocs/ezscript/Topics/Catalyst/Language.htm). Also note that the country/region code is optional. As you can see, I did not include a region code for French, but I could have specified `fr-BL` for Belgium or `fr-CA` for Canada. Include as few or as many as you wish!

Next, we need to modify `next.config.js` to enable NextJS's automatic locale detection:

```js
// next.config.js

/** @type {import('next').NextConfig} */

const nextConfig = {
  reactStrictMode: true,
  i18n: {
    locales: ['en-US', 'en-UK', 'zh-CN', 'fr'],
    defaultLocale: 'en-US'
  }
}

module.exports = nextConfig
```

Note that there is no simple way to include our `localeNames.js` in our `next.config.js` file. I tried many different options and they all failed, so replicating the data, unfortunately is the only option. If anyone has a solution, please leave a comment!

Ok, next we'll create a utility function called `fetchPage()` to fetch the content for our page based on a slug. Your data could come from a headless CMS like WordPress or Contentful, or even a database, but in this example, I'm simply using a JS file:

```js
// fetchPage.js

import { Configuration, OpenAIApi } from "openai";
import homePage from "@/data/homePage";

const pages = {
    'home-page': homePage
}

const defaultLocale = 'en-US';

export default async function fetchPage(slug, locale) {
    if (locale === defaultLocale) return pages[slug];

    const configuration = new Configuration({
        apiKey: process.env.NEXT_PUBLIC_OPENAI_KEY,
    });

    const openai = new OpenAIApi(configuration);

    const translated = {};

    for (const k in pages[slug]) {
        const page = pages[slug];
        const string = "Translate the following to " + locale + ': ' + page[k];

        const completion = await openai.createCompletion({
            model: "text-davinci-003",
            prompt: string,
            max_tokens: 2048
        });

        const res = completion?.data?.choices[0]?.text;
        translated[k] = res;
    }

    return translated;
}
```

This function imports our page content, which is simple a JS object, then iterates over each key, making a completion request to ChatGPT. The hard limit is 2048 tokens for a free account so the page must be broken down into title, headings, paragraphs, etc., and then an API request made for each. This may, unfortunately, effect the way ChatGPT translates the document as the paragraphs are taken out of context. If anyone knows of a way to create a session on the API, where it can remember the previous completions (like how the ChatGPT web app works), please leave a comment!

And, last but not least, our home page component:

```js
// index.js

import React, { useRef } from 'react'
import Head from 'next/head'
import { setCookie } from 'cookies-next';
import { useRouter } from 'next/router';
import localeNames from '@/data/localeNames';
import fetchPage from '@/lib/fetchPage';

export default function Home({ page }) {
  const router = useRouter();

  const handleLocaleChange = (event) => {
    const locale = event.target.value;
    setCookie('NEXT_LOCALE', locale, { path: '/' });
    const { pathname, asPath, query } = router;
    router.push({ pathname, query }, asPath, { locale });
  }

  const Paragraphs = () => {
    const pars = [];
    for (const k in page) {
      if (k.match(/^par/)) {
        pars.push(<p className="text-lg mb-6 text-justify">{page[k]}</p>)
      }
    }
    return pars;
  }

  return (
    <>
      <Head>
        <title>{page.title}</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className="bg-slate-900 h-screen flex flex-col text-white">
        <div className="w-full md:w-[1200px] bg-slate-800 mx-auto md:px-10 md:py-10">
          <h1 className="font-bold text-2xl mb-6">{page.title}</h1>
          <div className="p-4 border-2 border-gray-400 rounded-md relative flex w-full mb-6">
            <select
              className="focus:outline-none w-full bg-transparent text-blue-200 [&>*]:bg-slate-700"
              name="locale"
              placeholder="Select Language"
              onChange={handleLocaleChange}
            >
              <option disabled="">** Select Language **</option>
              {
                localeNames.map((l) => (
                  <option key={l.value} value={l.value}>{l.name}</option>
                ))
              }
            </select>
          </div>
          <Paragraphs />
        </div>
      </div>
    </>
  )
}

export async function getStaticProps(props) {
  const { locale, locales, defaultLocale } = props;

  const page = await fetchPage('home-page', locale);

  return {
    props: {
      locale,
      locales,
      defaultLocale,
      page
    },
    revalidate: 10
  }
}
```

Some key things to note in this code:

1. Our `handleLocaleChange()` function uses next/router to change the selected locale. Notice that the path in the address bar is automatically updated. NextJS does all this for you automagically! Also, NextJS uses a special cookie called 'NEXT_LOCALE' to override the `Accept-Language` header sent by the user agent. So next time you navigate to "/" it should automatically redirect you to the locale stored in the cookie.
2. The `getStaticProps()` function runs on the server at build-time. NextJS is smart enough to create JSON files for each possible locale and the content comes from these static JSON files rather than the ChatGPT API at run-time. The `revalidate` key is for NextJS's incremental SSG feature, which is useless here because our content comes from a static file included in the source, but this is useful if you're using an external CMS, which will further reduce paid API usage by only updating pages that have changed automatically, when they change.

That's it! I hope you found this article useful, or at the very least, mildly entertaining. I keep finding more and more uses for ChatGPT every day. OpenAI is certainly on track to become a monster, Google-sized company in the years to come (might want to invest).

I'm very interested in your feedback about this article, its proposed solution, and ways to improve it. Please leave your comments!

Further reading:

[NextJS: Internationalized Routing](https://nextjs.org/docs/advanced-features/i18n-routing)

For more great articles, please read the [Designly Blog](https://designly.biz/blog).