# Automating i18n Support in React with GPT 4

By Mohammad AbuAboud · 2024-08-16 · Source: https://www.activepieces.com/blog/react-i18n

---
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](https://github.com/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](https://www.npmjs.com/package/react-i18next) and [lingui](https://www.npmjs.com/package/@lingui/react).

We were looking for the following:
- **Runtime Translation:** Interestingly, in Angular, it was compile-time, and we had [Monaco](https://www.npmjs.com/package/monaco-editor), 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](https://www.npmjs.com/package/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.
```typescript
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](https://crowdin.com/project/activepieces).

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!
