Automating i18n Support in React with GPT 4

By
on

Five weeks ago, we decided to migrate Activepieces from Angular to React. In that time, we launched a public beta with all community features except internationalization (i18n). Although we initially postponed i18n, user feedback has made it clear that adding language support quickly is essential.

The History of i18n in Activepieces

I18n Contribution

In October 2023, while we were still using Angular, something amazing happened. Damien Hebert (@Doskyft), one of our top contributors, sent us a huge pull request with over 10,000 lines of code (https://github.com/activepieces/activepieces/pull/2644). He added i18n attributes to every code file.

These attributes make easy for a machine to extract out the translation files, making it simpler to support multiple languages in our app.

Crowd Source Translation

Following Damien’s contribution, we searched for a platform to handle crowd-sourced translations and found ** Crowdin.com** to be the best fit. Crowdin is a collaborative tool that allows multiple translators to work together on a project. We uploaded our files and asked our community for help, and thanks to their efforts, Activepieces quickly expanded to support 12+ languages.

Technical Details

I woke up on Friday, and since it was a chill day for me, I decided to think about i18n. First, I started by evaluating libraries. Several libraries were considered, including react-i18next and lingui.

We were looking for the following:

  • Runtime Translation: Interestingly, in Angular, it was compile-time, and we had Monaco, a heavy code editor that is around 50 MB. We suddenly found our Docker image size quickly growing to over 2 GB because each assets folder was duplicated across languages, resulting in a Monaco copy for each language.

  • CrowdIn Support

So we ended up going with react-i18next. The next challenge was to apply the t() wrapper around strings in the React codebase. We decided to use the OpenAI GPT-4 API to help with that.

I wrote a simple script that traverses a folder, rewrites the files, and applies the react-i18next wrapper around the strings.

Here is the script. I used the chalk package because I love printing colorful log statements.

  • This script recursively searches for .tsx files in the specified directory and processes them using OpenAI's GPT-4 model.
  • It modifies React components by wrapping strings with t() for localization.
import * as fs from 'fs-extra';
import * as path from 'path';
import chalk from 'chalk';
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: 'OPEN_AI_KEY' });

const isDirectory = (filePath: string) => fs.statSync(filePath).isDirectory();

const getTsxFiles = (dir: string): string[] =>
    fs.readdirSync(dir).flatMap((file) => {
        const fullPath = path.join(dir, file);
        return isDirectory(fullPath) ? getTsxFiles(fullPath) : fullPath.endsWith('.tsx') ? [fullPath] : [];
    });

const processAndUpdateTsxFiles = async (files: string[]) => {
    for (const file of files) {
        try {
            console.log(chalk.blue(`Processing: ${file}`));
            const content = fs.readFileSync(file, 'utf-8');
            const prompt = `
                Apply react-i18next to this TypeScript React component.
                - Wrap strings with t() 
                - Don't wrap non-English strings or specific attributes like displayName.
                - Use import { t } from 'i18next'.
                \n\n${content}`;
                
            const response = await openai.chat.completions.create({
                model: 'gpt-4',
                messages: [{ role: 'system', content: prompt }],
            });

            const updatedContent = response.choices[0].message.content;
            if (!updatedContent) throw new Error('Empty response');

            fs.writeFileSync(file, updatedContent, 'utf-8');
            console.log(chalk.green(`Updated: ${file}`));
        } catch (error) {
            console.error(chalk.red(`Error processing ${file}: ${error}`));
        }
    }
};

const targetPath = path.resolve(process.argv[2] || '');
const tsxFiles = getTsxFiles(targetPath);

console.log(chalk.green(`Found ${tsxFiles.length} .tsx file(s):`));
processAndUpdateTsxFiles(tsxFiles).then(() => console.log(chalk.blue('All files processed.')));

This was applied to each folder, followed by a manual review of the changes. The review process went quickly, considering there are only about 600 strings across 100+ small React components in Activepieces (https://github.com/activepieces/activepieces/pull/5378).

In about two hours, the new strings were uploaded to Crowdin project.

The community quickly responded with enthusiasm, eager to help translate the new strings.

It's truly a privilege to be part of such a wonderful community!