Software localization

Software Localization with ChatGPT

We build and localize an app using ChatGPT to generate code while observing best prompting practices.

ChatGPT famously reached 100 million monthly active users two months after its launch, shattering adoption records. The LLM (Large Language Model) has taken the world by storm and excels at generating certain kinds of content, code being chief among them. However, utilizing the chatbot to extract quality code takes time and effort. In this guide, we walk through localizing a React demo app with the popular i18next library, sitting side-by-side with ChatGPT as we prompt and iterate to get the best code possible.

Versions used

The demo app we’ll build in this guide uses the following NPM packages. We’ll install them as needed (some will be pre-installed in the starter project).

Package Version Notes
Vite 5.1 Build tool and bundler
React 18.2 UI library
React Router 6.22 Used for multi-page routing
Prop Types 15.8 React component props validation
Tailwind CSS 3.4.1 Styling framework
i18next 23.10 Popular i18n library
react-i18next 14.1 React extensions for i18next
i18next-http-backend 2.5 Loads translations from network
i18next-browser-languagedetector 7.2 Detects user’s preferred locales

A note on ChatGPT versions

To generate build plans, design, and code, we’ve used ChatGPT 4. ChatGPT 4 is a paid service at the time of writing, but you can work along with the free Chat GPT 3.5 if you like. However, there are significant differences between GPT 3.5 and 4, so your mileage may vary.

🗒️ Note » We used The Professional Code (Auto programming) custom GPT, which prompts ChatGPT to act like a professional software developer. The custom GPT was taken down as we published the article. You don’t have to use the custom GPT, however. Plain vanilla ChatGPT will work fine for our purposes.

Our demo

We have a little app to localize in this guide. The fictional Migrant Taco is a forum for fans of EDM (electronic dance music).

🗒️ Note » We don’t implement any CRUD functionality since we want to focus on the i18n here.

Our app before localization
Our app before localization

🗒️ Note » We scaffolded the project using the Vite React template, which installed Vite and React.

The starter app (before localization) has the following file structure.

.
├── public
│   └── assets
│       ├── hot-albums.json
│       └── trending-posts.json
└── src
    ├── components
    │   ├── AlbumCard.jsx
    │   ├── Navbar.jsx
    │   └── PostTeaser.jsx
    ├── hooks
    │   ├── use-fetch.js
    │   ├── use-hot-albums.js
    │   └── use-trending-posts.js
    ├── pages
    │   ├── HomePage.jsx
    │   ├── HotAlbums.jsx
    │   └── TrendingPosts.jsx
    ├── App.jsx
    └── main.jsx
Code language: plaintext (plaintext)

🗒️ Note » Even the starter app was created using ChatGPT, but to focus on i18n we’re skipping the steps to build the largely straightforward starter app. ChatGPT is great at building small apps like this and excels at generating common boilerplate.

🔗 Resource »  Get the starter project from GitHub.

Hooks are used to load album and post JSON data from the network. Here are excerpts of the JSON:

// public/assets/hot-albums.json
[
  {
    "id": 1,
    "title": "Euphoric Nights",
    "artist": "DJ Pulse",
    "coverUrl": "https://picsum.photos/id/100/300/300",
    "releaseDate": "2024-02-15"
  },
  {
    "id": 2,
    "title": "Bass in the Shadows",
    "artist": "Bass Master",
    "coverUrl": "https://picsum.photos/id/101/300/300",
    "releaseDate": "2024-03-01"
  },
  // ...
]
Code language: JSON / JSON with Comments (json)
// public/assets/trending-posts.json
[
  {
    "id": 1,
    "title": "Top 10 EDM Tracks of 2024",
    "excerpt": "Discover the tracks that are setting the stage on fire in 2024.",
    "author": "DJ Reviewer",
    "date": "2024-02-20"
  },
  {
    "id": 2,
    "title": "The Evolution of EDM Festivals",
    "excerpt": "A deep dive into how EDM festivals have transformed over the years.",
    "author": "Festival Historian",
    "date": "2024-03-05"
  },
  // ...
]

Code language: JSON / JSON with Comments (json)

Simple hooks are used to load this JSON:

// src/hooks/use-hot-albums.js

// Handles JSON file fetching and error handling
import useFetch from "./use-fetch";

const useHotAlbums = (limit = null) => {
  const { data, loading, error } = useFetch(
    "/assets/hot-albums.json"
  );
  
  const albums =
    data && limit ? data.slice(0, limit) : data;
    
  return { albums, loading, error };
};

export default useHotAlbums;

Code language: JavaScript (javascript)

A similar hook, useTrendingPosts, is used for trending posts. The useHotAlbums and useTrendingPosts hooks use a useFetch hook under the hood. The latter fetches JSON from the network and has some basic error handling. We’re skipping the code for useFetch here, but you can get it on GitHub.

Three pages are managed by React Router, a home page (screenshot above), a Hot Albums page, and a Trending Posts page. They use our hooks to load the JSON data and present it. Here’s the home page:

// src/pages/HomePage.jsx

import { Link } from "react-router-dom";
import AlbumCard from "../components/AlbumCard";
import PostTeaser from "../components/PostTeaser";
import useHotAlbums from "../hooks/use-hot-albums";
import useTrendingPosts from "../hooks/use-trending-posts";

const HomePage = () => {
  const {
    albums,
    loading: loadingAlbums,
    error: errorAlbums,
  } = useHotAlbums(3);

  const {
    posts,
    loading: loadingPosts,
    error: errorPosts,
  } = useTrendingPosts(4);

  if (loadingAlbums || loadingPosts)
    return <div>Loading...</div>;
    
  if (errorAlbums || errorPosts)
    return <div>Error loading data</div>;

  return (
    <div className="...">
      <section>
        <h2 className="...">
          Hot Albums
        </h2>
        <div className="...">
          {albums.map((album) => (
            <AlbumCard key={album.id} album={album} />
          ))}
        </div>
        <div className="...">
          <Link to="/hot-albums" className="...">
            View all 9 albums
          </Link>
        </div>
      </section>
      <section>
        <h2 className="...">
          Trending Posts
        </h2>
        <div className="...">
          {posts.map((post) => (
            <PostTeaser key={post.id} post={post} />
          ))}
        </div>
        <div className="...">
          <Link to="/trending-posts" className="...">
            View all 9 posts
          </Link>
        </div>
      </section>
    </div>
  );
};

export default HomePage;

Code language: JavaScript (javascript)

The home page uses an AlbumCard component and a PostTeaser component to display its fetched data. These are presentational components, and you can get their code from GitHub.

🗒️ Note » We omit CSS styles here for brevity. These Tailwind styles are available in the full code listings on GitHub.

React Router uses the HomePage and others (all similar to the home page) in the root App component. You can find the code on GitHub.

// src/App.jsx

import {
  BrowserRouter as Router,
  Routes,
  Route,
} from "react-router-dom";
import HomePage from "./pages/HomePage";
import HotAlbums from "./pages/HotAlbums";
import TrendingPosts from "./pages/TrendingPosts";
import Navbar from "./components/Navbar";

const App = () => {
  return (
    <Router>
      <div className="...">
        <Navbar />
        <main className="...">
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route
              path="/hot-albums"
              element={<HotAlbums />}
            />
            <Route
              path="/trending-posts"
              element={<TrendingPosts />}
            />
          </Routes>
        </main>
      </div>
    </Router>
  );
};

export default App;
Code language: JavaScript (javascript)

OK, this is our starting point. Let’s walk through how we localized it with ChatGPT.

🔗 Resource »  Get the starter project from GitHub.

The build plan

From this point, we will go through our specific experience localizing the demo app with ChatGPT.

🗒️ Note » ChatGPT is non-deterministic: It can give varying responses for the same prompt. We’ll try to focus on broad guidelines that can help you when using ChatGPT to generate software solutions.

If we jumped in head-first and asked ChatGPT to start generating code, we could go down branches of reasoning that miss important high-level information. This can be frustrating and lead to a lot of back-and-forth with the AI as we provide missing gaps in its knowledge about our specific app. It’s wise to do some high-level chain of thought prompting, getting ChatGPT to create a build plan that solves our problem before it generates any code.

The prompt

Here’s our initial prompt:

Hello, I have a small website made with React, Vite,
react-router, and Tailwind CSS. I would like to
internationalize this site and localize it to English
(en) and Arabic (ar), with the option to add more
locales in the future. I want to use the react-i18next
library to do this. What are the steps I should take?
Please outline a general plan. Don't generate any code.
Output the plan in a way that I can easily copy and paste,
please.
Code language: plaintext (plaintext)

Note how we gave ChatGPT a good amount of context. We should be specific when prompting, or the AI will make many assumptions about our app, often derailing our efforts. So we tell which packages we’re using and what we generally want to accomplish. We could have been more specific here and will have to iterate more on the build plan in a moment.

The response

ChatGPT did a good job identifying the steps needed for localizing a React app with i18next. Here’s a summarized excerpt of its response:

To internationalize and localize a React website with
react-i18next for English and Arabic, ensuring future
scalability:

1. Install Dependencies: Include react-i18next,
i18next, and possibly i18next-browser-languagedetector for
automatic language detection.

2. Configuration: Set up a `locales` directory in `src`,
with subfolders for each language (e.g., en, ar),
containing `translation.json` files for key-value pairs.

3. Initialize react-i18next: In `src`, create `i18n.js`
to configure i18next with locale details, a fallback
language, and plugins like the language detector.

4. Integration with React: Use I18nextProvider or
useTranslation hook for translation function access.
Replace strings with translation keys using `t` function,
and change languages with `i18next.changeLanguage`.

5. RTL Layout for Arabic: Utilize Tailwind CSS's RTL
support by adding `dir="rtl"` to the root element for
Arabic.

[//]: # (omitting rest of response for brevity...)

Code language: Markdown (markdown)

🗒️ Note » We’re showing prompts and response as Markdown blocks here to differentiate them. The ChatGPT UI looks different, of course.

Validation and iteration

We wanted to load our translation files asynchronously from the network using the official i18next plugin (i18next-http-backend). We also wanted to localize our routes. So we told ChatGPT to update its plan:

Let's add the following to the plan:
- Loading translation files asynchronously from
  the network using the official i18next plugin
  to do so
- Localizing routes so that /en/foo loads the
  foo English page and /ar/foo load the same
  page in Arabic (foo is an example route here,
  of course)

Please output the updated plan.
Code language: Markdown (markdown)

ChatGPT amended the plan appropriately, adding the i18next-http-backend package to the install list in step 1. It also added a step to the build plan for localizing routes:

6. Localizing Routes: Configure react-router for routes
with language codes (e.g., /en/foo), dynamically update
i18next's language on navigation, and adjust UI and
language files accordingly.
Code language: Markdown (markdown)

Execution

At this point, we felt confident enough to have ChatGPT start generating specific instructions and code:

OK, let's move on to Step 1. Please walk me through
this step and show me the code.
Code language: Markdown (markdown)

ChatGPT generated NPM install instructions for needed packages, giving NPM and Yarn alternatives for commands. Here’s the NPM version:

npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector
Code language: Bash (bash)

The AI also explained what each package does. We asked it to continue:

Wonderful :) Let's go through step 2, please. Show me the code.
Code language: Markdown (markdown)

A generally good plan with code was generated. Here’s a summary:

2. Setting Up Translation Resources:

- Create a `locales` directory in your project's `src`
folder, and inside it, make subdirectories for each
language (e.g., `en`, `ar`).

- In each language subdirectory, add a `translation.json`
file containing translation key-value pairs.

- Use your IDE or file explorer to create and edit these
directories and files, ensuring key consistency across
languages for seamless translation.
Code language: Markdown (markdown)

ChatGPT gave us a helpful file hierarchy and example translation JSON. However, the AI made a mistake we didn’t immediately catch: It told us to create our locales directory under src. We wanted to load our translations from the network using i18next’s HTTP loader, so locales needed to go under the public directory. We asked ChatGPT to correct this and it generated an updated hierarchy:

Corrected example of the translation file hierarchy.
Corrected example of the translation file hierarchy.
ChatGPT generated helpful example translation files.
ChatGPT generated helpful example translation files.

🗒️ Note » We started prompting ChatGPT assuming we would use en-US and ar-EG for supported locales. We later changed this to en and ar, so if you see any disparity know that we landed on the latter set.

Generated code: setting up i18next

A quick prompt to move to step 3 of the build plan, setting up i18next, got ChatGPT generating its first block of production code.

// src/i18n.js

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  // Load translation using http -> see /public/locales.
  // We will serve our locales directory as a static asset
  .use(HttpBackend)
  // Detect user language
  .use(LanguageDetector)
  // Pass the i18n instance to react-i18next.
  .use(initReactI18next)
  // Initialize i18next
  .init({
    // The lng and fallbackLng options define your
    // application's default language and fallback language
    // in case the user's language can't be detected or
    // isn't supported.
    fallbackLng: "en",
    // if you're using a language detector, you can remove
    // this line
    lng: "en",
    detection: {
      // Order and from where user language should be
      // detected. Here we list possible options for
      // detection.
      order: [
        "querystring",
        "cookie",
        "localStorage",
        "sessionStorage",
        "navigator",
        "htmlTag",
        "path",
        "subdomain",
      ],
      // Keys or params to lookup language from
      lookupQuerystring: "lng",
      lookupCookie: "i18next",
      lookupLocalStorage: "i18nextLng",
      // Cache user language on
      caches: ["localStorage", "cookie"],
      // Exclude cache for specific languages
      // languages to not persist (cookie, localStorage)
      excludeCacheFor: ["cimode"],
    },
    // Path where resources get loaded from, or a function
    // returning a path:
    // function(lngs, namespaces) { return customPath; }
    // The function receives the lngs and namespaces as
    // the arguments (in case you need to load languages
    // and namespaces dynamically).The returned path will
    // interpolate lng, ns if provided like giving a static
    // path.
    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json",
    },
    // Have a common namespace used around the full app
    ns: ["translation"],
    defaultNS: "translation",

    // Boolean or function to enable/disable adding keys
    // not found in translation to backend
    saveMissing: true,

    // Set to true if you're using a language detector
    debug: true,

    interpolation: {
      // Not needed for React as it escapes by default
      escapeValue: false,
    },
  });

export default i18n;

Code language: JavaScript (javascript)

Interestingly, this reads like it was taken from the documentation of the various packages we’re using. We cleaned this code up, removing comments and using implicit defaults rather than writing them explicitly. We also removed the saveMissing: true option, since that would cause our back-end to write missing translations to our translation files, something we don’t support in our app and would likely lead to errors.

Here’s our cleaned-up i18n.js:

// src/i18n.js

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import HttpBackend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: "en",
    interpolation: {
      escapeValue: false,
    },
  });

export default i18n;

Code language: JavaScript (javascript)

ChatGPT also asked us to import this new file into our entry file, src/main.jsx.

// src/main.jsx

  import React from "react";
  import ReactDOM from "react-dom/client";
  import App from "./App.jsx";
  import "./index.css";
+ import "./i18n.js";

  ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  );
Code language: Diff (diff)

Integrating i18next into components

As part of step 3, ChatGPT inadvertently gave us an example of integrating i18next into our components via the useTranslation hook. It gave us an alternative place to import our i18n.js file, the root App component.

// Example from ChatGPT

import React from 'react';
import './App.css';
import './i18n'; // Import i18n configuration
import { useTranslation } from 'react-i18next';

function App() {
  const { t } = useTranslation();

  return (
    <div className="App">
      <header className="App-header">
        {t('welcomeMessage')}
      </header>
    </div>
  );
}

export default App;
Code language: JavaScript (javascript)

We had already imported i18n.js in main.jsx, and used the example as a guide to localize our Navbar component. We first moved all hard-coded strings in the component to our translation files:

// public/locales/en/translation.json
{
  "appName": "Migrant Taco",
  "nav": {
    "home": "Home",
    "hotAlbums": "Hot Albums",
    "trendingPosts": "Trending Posts"
  }
}
Code language: JSON / JSON with Comments (json)
// public/locales/ar/translation.json
{
  "appName": "ميجرانت تاكو",
  "nav": {
    "home": "الرئيسية",
    "hotAlbums": "الألبومات الساخنة",
    "trendingPosts": "المنشورات الرائجة"
  }
}
Code language: JSON / JSON with Comments (json)

Afterwards, we replaced the occurrences of these strings with calls to the t() translation function:

// src/components/Navbar.jsx
+ import { useTranslation } from "react-i18next";
  import { Link } from "react-router-dom";

  const Navbar = () => {
+   const { t } = useTranslation();

    return (
      <nav className="...">
        <div className="...">
          <h1 className="...">
-           Migrant Taco
+           {t("appName")}
          </h1>
          <ul className="...">
            <li>
              <Link to="/" className="...">
-               Home
+               {t("nav.home")}
              </Link>
            </li>
+          <!-- Localize other link text... -->
          </ul>
        </div>
      </nav>
    );
  };

  export default Navbar;
Code language: Diff (diff)

After the update, switching our locale to ar revealed our Navbar in Arabic.

English and Arabic versions of our Navbar.
English and Arabic versions of our Navbar.

🗒️ Note » To quickly test a locale when developing, set lng: "ar" in the i18n.init() options (found in the src/i18n.js file). Note that when using the language detector plugin, this will cause the set language to be stored in your local storage to be used in future visits. Deleting the local storage entry should start you fresh when testing.

🔗 Resource »  Get the full code for the Navbar component from GitHub.

The language switcher

Although not part of the build plan, we wanted to add a language switcher UI to our app. Even though the language detector plugin will try to match the user’s preferred locales (set in her browser) to our app’s locales, we often want to provide a manual override for the user.

The prompt

We prompted ChatGPT to generate the code for the switcher:

Please generate the locale switcher. Use a
`<select>` dropdown instead of buttons. Use
Tailwind CSS styles to style the `<select>`. 
Give the component a background of bg-gray-200
and text-slate-900. Keep in mind that I want to
place the localeswitcher into this navbar:

[//]: # (pasted code for Navbar component)
Code language: Markdown (markdown)

We asked ChatGPT to add Tailwind styles to the switcher since it’s notoriously difficult to style <select> dropdowns across browsers. (It didn’t do a perfect job styling the <select>: the dropdown looks OK on the Chromium-based Arc browser and Firefox but needs work in Safari).

The response

ChatGPT’s first take on creating the locale switcher was inlining it directly in the Navbar. We prompted it to extract the switcher to a component, and got this code back:

// LocaleSwitcher.js
import React from 'react';
import { useTranslation } from 'react-i18next';

const LocaleSwitcher = () => {
  const { i18n } = useTranslation();

  const changeLanguage = (event) => {
    i18n.changeLanguage(event.target.value);
  };

  return (
    <select
      onChange={changeLanguage}
      value={i18n.language}
      className="bg-gray-200 text-slate-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
    >
      <option value="en">English</option>
      <option value="ar">العربية</option>
    </select>
  );
};

export default LocaleSwitcher;

Code language: JavaScript (javascript)

While generally good, the generated code used i18n.language for setting the dropdown’s value, whereas we want i18n.resolvedLanguage here. language will always be the detected user locale whereas resolvedLanguage will be a locale our app supports that’s best-matched to the detected locale. Since the value has to match one of our supported locales (en|ar) to work with the dropdown, we corrected this in the code before we added it to our app:

// src/components/LocaleSwitcher.jsx

  import { useTranslation } from "react-i18next";

  const LocaleSwitcher = () => {
    const { i18n } = useTranslation();

    const changeLanguage = (event) => {
      i18n.changeLanguage(event.target.value);
    };

    return (
      <select
        onChange={changeLanguage}
-       value={i18n.language}
+       value={i18n.resolvedLanguage}
        className="..."
      >
        <option value="en">English</option>
        <option value="ar">العربية</option>
      </select>
    );
  };

  export default LocaleSwitcher;
Code language: Diff (diff)

After placing the LocaleSwitcher into our Navbar, we got an easy UX enhancement to our app.

The locale switcher shown switching between Arabic and English versions of our site.

Interpolation and plurals

Still at step 4, we wanted to handle dynamic values in our translation messages. The “view all” buttons on our home page needed this treatment, as well as plural support.

A button labelled "View all 9 albums."

The prompt

A simple prompt that describes the problem got us started.

I have text in my app that reads "View all
9 albums". The 9 here is a dynamic value
known at runtime. How do I localize this
text?
Code language: Markdown (markdown)

The response

ChatGPT had a good starting skeleton to solve the problem. Here’s its response, summarized for brevity:

- Add translation keys with placeholders for dynamic values
in your JSON files (e.g., `"viewAllAlbums": "View all
{{count}} albums"`).

- Use the `t` function with interpolation to insert dynamic
values (e.g., `{t('viewAllAlbums', { count: albumCount })}`).

- For pluralization, define singular and plural keys in your
translation files. `react-i18next` automatically selects the
appropriate form based on the count.

Code language: Markdown (markdown)

Validation and iteration

The AI correctly identified that we’re dealing with a plural value but used i18next’s outdated singular and plural forms. Here are the translation messages it suggested:

{
  "viewAllAlbums": "View all {{count}} album",
  "viewAllAlbums_plural": "View all {{count}} albums"
}
Code language: JSON / JSON with Comments (json)

This will work for languages with simple plurals like English, and won’t work with newer versions of i18next that no longer use simple singular/plural messages. ChatGPT identified the former problem noting “that Arabic and other languages have more complex pluralization rules, which i18next supports through its [newer] pluralization feature. You might need to add additional keys for these rules, depending on the language.”

So we immediately asked:

How do I handle Arabic pluralization in
my translations?
Code language: JSON / JSON with Comments (json)

ChatGPT responded with the newer type of pluralization, which works for all languages, including Arabic.

{
  "viewAllAlbums_zero": "لم يتم عرض الألبومات",
  "viewAllAlbums_one": "عرض البوم واحد",
  "viewAllAlbums_two": "عرض البومان",
  "viewAllAlbums_few": "عرض {{count}} ألبومات",
  "viewAllAlbums_many": "عرض {{count}} ألبوم",
  "viewAllAlbums_other": "عرض كل {{count}} ألبوم"
}
Code language: JSON / JSON with Comments (json)

We added the above JSON to our Arabic translation file, public/locales/en/translation.json, nesting them under the home object. We used the same format for English:

// public/locales/en/translation.json
{
  "appName": "Migrant Taco",
  //...
  "home": {
    "viewAllAlbums_one": "View {{count}} album",
    "viewAllAlbums_other": "View {{count}} albums",
  },
  // ...
}
Code language: JSON / JSON with Comments (json)

Of course, we had to update the button text in our HomePage component to use the pluralized translations.

// src/pages/HomePage.jsx

  import { Link } from "react-router-dom";
  // ...
+ import { useTranslation } from "react-i18next";

  const HomePage = () => {
+   const { t } = useTranslation();

    // ...

    return (
      <div className="...">
        <section>
          {/* ... */}
          <div className="...">
            <Link to="/hot-albums" className="...">
-             View all 9 albums
+             {t("home.viewAllAlbums", { count: 9 })}
            </Link>
          </div>
        </section>
        {/* ... */}
      </div>
    );
  };

  export default HomePage;

Code language: Diff (diff)

When we switched our app to Arabic, we could see the correct plural form showing for any count value.

The "view all 9 albums" button translated to Arabic. The "9" is show in Western Arabic numerals.

🔗 Resource »  Our pluralization guide covers the subject in detail.

The translation messages ChatGPT outputted didn’t specify the number format for Arabic, so the Arabic translation above uses the wrong numeral system. We corrected this manually:

// public/locales/ar/translation.json
{
  "appName": "ميجرانت تاكو",
  // ...
  "home": {
    // ...
    "viewAllAlbums_zero": "لا توجد ألبومات",
    "viewAllAlbums_one": "عرض البوم واحد",
    "viewAllAlbums_two": "عرض البومان",
-   "viewAllAlbums_few": "عرض {{count}} ألبومات",
+   "viewAllAlbums_few": "عرض {{count, number}} ألبومات",
-   "viewAllAlbums_many": "عرض {{count}} ألبوم",
+   "viewAllAlbums_many": "عرض {{count, number}} ألبوم",
-   "viewAllAlbums_other": "عرض {{count}} ألبوم",
+   "viewAllAlbums_other": "عرض {{count, number}} ألبوم",
    // ...
  },
  // ...
}
Code language: Diff (diff)

With this change, our Arabic plurals showed the count variable in the correct, Eastern Arabic numeral system.

The "view all albums" button showing its Arabic label with the number "3" as the correct Easter Arabic numeral.

🔗 Resource » Our Concise Guide to Number Localization covers numeral systems, currency formatting, and other number localization goodness.

Localized routes

When we arrived at step 6 of our build plan, localizing routes, we had to iterate quite a bit to get ChatGPT to do exactly what we wanted. The AI did devise a good plan to implement the new feature. In summary:

- Prefix routes with language codes (e.g., `/en/about`).

- Update react-router to capture language codes from URLs and
set the active language in i18next.

- Use a wrapper component or hook to set i18next's language
based on the route's lang parameter.

- Ensure links include the current language code in the URL,
possibly using a helper function or custom link component.

- Provide a method for users to switch languages, updating
the URL and content to the selected language.
Code language: Markdown (markdown)

Here’s ChatGPT’s first take on route prefixing with react-router:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
// Import other components and pages as needed

const App = () => {
  const { i18n } = useTranslation();

  return (
    <Router>
      <Routes>
        <Route path="/:lang" element={<Layout />}>
          <Route index element={<HomePage />} />
          <Route path="about" element={<AboutPage />} />
          // Define other nested routes
        </Route>
      </Routes>
    </Router>
  );
};

export default App;

Code language: JavaScript (javascript)

This was meant as an example, but it assumed we have a Layout component in our app (we don’t). ChatGPT also didn’t factor in initializing the new :lang param if the route didn’t include a locale. We’ll address these two points shortly.

The LocalizedLink component ChatGPT offered as a drop-in replacement for react-router’s built-in Link was exactly what we needed. We used it almost as-is in our app.

// src/components/LocalizedLink.jsx

import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

const LocalizedLink = ({ to, ...props }) => {
  const { i18n } = useTranslation();
  const lang = i18n.language;

  return <Link to={`/${lang}${to}`} {...props} />;
};

Code language: JavaScript (javascript)

The AI also generated a LocaleWrapper component that was meant to wrap each of our pages:

// src/components/LocaleWrapper.jsx

import { useParams } from 'react-router-dom';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';

const LocaleWrapper = ({ children }) => {
  const { lang } = useParams();
  const { i18n } = useTranslation();

  useEffect(() => {
    if (lang) {
      i18n.changeLanguage(lang);
    }
  }, [lang, i18n]);

  return children;
};

export default LocaleWrapper;
Code language: JavaScript (javascript)

We were confused about using this component in our app and didn’t have enough info to tie the solution together. We prompted further:

- I don't have a `<Layout>` element in my app; I'd like to use
something "blank" here instead
- How does the `:lang` param get initialized on first load?
- How do I use the [LocaleWrapper] component in my app?

Please provide detailed code for the localized routes
solution with accommodation for the above.
Code language: Markdown (markdown)

At this point, ChatGPT generated a much more robust LocaleWrapper, which checked for supported locales and used a fallback if the lang param was not supported:

// src/components/LocaleWrapper.jsx

import React, { useEffect } from "react";
import { useParams, Navigate } from "react-router-dom";
import { useTranslation } from "react-i18next";

const LocaleWrapper = ({ children }) => {
  const { lang } = useParams();
  const { i18n } = useTranslation();

  useEffect(() => {
    // If there's a lang param and it's different
    // from the current language, update it
    if (lang && i18n.language !== lang) {
      // Extend this list as needed
      const supportedLanguages = ["en", "ar"];
      const fallbackLanguage = "en";
      const newLang = supportedLanguages.includes(lang)
        ? lang
        : fallbackLanguage;
      i18n.changeLanguage(newLang);
    }
  }, [lang, i18n]);

  // Redirect to a default language route if no
  // lang param is present
  if (!lang) {
    const detectedLang = i18n.language || "en";
    return <Navigate to={`/${detectedLang}`} replace />;
  }

  return <>{children}</>;
};

export default LocaleWrapper;
Code language: JavaScript (javascript)

The LLM also introduced a RootRedirect component for us to add to our routes. RootRedirect redirects the root / route to our default locale /en, forcing a locale on all routes.

// src/components/RootRedirect.jsx

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";

const RootRedirect = () => {
  const navigate = useNavigate();
  const { i18n } = useTranslation();

  useEffect(() => {
    // Determine the default or detected language
    const defaultOrDetectedLang = i18n.language || "en";
    // Redirect to the route with the default or detected language
    navigate(`/${defaultOrDetectedLang}`, {
      replace: true,
    });
  }, [navigate, i18n.language]);

  return null;
};

export default RootRedirect;
Code language: JavaScript (javascript)

We followed ChatGPT’s direction and added the new components to our root App:

// src/App.jsx

import {
  BrowserRouter as Router,
  Routes,
  Route,
} from "react-router-dom";
import HomePage from "./pages/HomePage";
import HotAlbums from "./pages/HotAlbums";
import TrendingPosts from "./pages/TrendingPosts";
import Navbar from "./components/Navbar";
import LocaleWrapper from "./components/LocaleWrapper";
import RootRedirect from "./components/RootRedirect";

const App = () => {
  return (
    <Router>
      <div className="...">
        <Navbar />
        <main className="...">
          <Routes>
           <Route path="/" element={<RootRedirect />} />

           <Route
             path="/:lang"
             element={<LocaleWrapper />}
            >
              <Route index element={<HomePage />} />
              <Route
                path="hot-albums"
                element={<HotAlbums />}
              />
              <Route
                path="trending-posts"
                element={<TrendingPosts />}
              />
            </Route>
          </Routes>
        </main>
      </div>
    </Router>
  );
};

export default App;
Code language: JavaScript (javascript)

At this point, we discovered an error where the LocaleWrapper wasn’t rendering its children. We prompted ChatGPT about this, and it correctly realized that its generated code targeted older versions of react-router. New versions don’t use React children but an Outlet> component instead:

// src/components/LocaleWrapper.jsx

  import { useEffect } from "react";
  import {
    useParams,
    Navigate,
+   Outlet,
  } from "react-router-dom";
  import { useTranslation } from "react-i18next";
  import { supportedLngs, fallbackLng } from "../i18n";

- const LocaleWrapper = ({ children} ) => {
+ const LocaleWrapper = () => {
    const { lang } = useParams();
    const { i18n } = useTranslation();

    useEffect(() => {
      // ...
    }, [lang, i18n]);

-   // While we're at it, let's remove this redirection
-   // logic that ChatGPT generated. Our `<RootRedirect>`
-   // component is already taking care of this. 
-   if (!lang) {
-     const detectedLang = i18n.language || fallbackLng;
-     return <Navigate to={`/${detectedLang}`} replace />;
-   }

-   return <>{children}</>;
+   return <Outlet />;
  };

  export default LocaleWrapper;

Code language: Diff (diff)

This fixed the rendering error.

🗒️ Note » We had to iterate further with ChatGPT to ensure that routes without locales redirect to localized routes e.g. /trending-posts/en/trending-posts. We also wanted to address the 404 Not Found case. ChatGPT was able to help us solve these problems, and you can see the final code in our GitHub repo.

🔗 Resource » We continued with ChatGPT, localizing dates, numbers, and more. See the complete project on GitHub.

🔗 Resource » You can view the entire conversation with ChatGPT on GitHub. Go to the conversation directory and run npx serve .

An animation showing navigation between the different pages and translations of our app.
Our completed app, with Chinese added as well

🔗 Resource » We wanted to focus on working with ChatGPT in this tutorial. We have a complete tutorial for React and i18next if you want to dive deeper.

Key takeaways

Working with ChatGPT to generate code can be both frustrating and rewarding. A few important practices can increase our chances of success:

  • Know your stuff! — Think of ChatGPT as a junior friend with incredibly wide-reaching knowledge. You must know your tech well to guide it to an appropriate solution. Otherwise, you’re at the mercy of whatever it generates, for better or worse.
  • Create a plan (without code) — Before solving a complex problem, prompt ChatGPT to give you a step-by-step plan without any code. This chain of thought prompt will reveal the AI’s approach to your problem early and highlight any misunderstandings that could create problems.
  • Give appropriate context  ChatGPT knows all about React, i18next, and many other popular technologies. However, it doesn’t know your app and how you want it to work. Give the AI enough details to guide it.
  • Verify! — ChatGPT will hallucinate and make mistakes; it doesn’t always know the latest versions of libraries you’re using. Make sure to review and test the code it gives you thoroughly.
  • Iterate — When you don’t get exactly what you want, tell ChatGPT and ask it to help with your specific issues.

Wrapping up

We hope you enjoyed this adventure in coding with an LLM. And we hope you learned a thing or two. Stay tuned for more software localization tutorials. Happy coding.