Building a minimal i18n library for react app - Part 1
Tags: i18n, translation, internationalization, react
Published: 2023-08-18 05:30:42

## Introduction

In this blog post, we will explore the process of integrating a basic translation functionality into your React application. However, it's important to clarify that this approach falls short of the capabilities offered by third-party plugins like [react-i18next](https://react.i18next.com/). If your requirements are confined to the following items, then the insights shared here can be quite valuable.

- Basic translation support
- Willing to craft your custom translation solution
- Simply interested to know how these translation libraries work under the hood

When I examine the source code of well-known third-party libraries, I often find them intricate and convoluted. Understanding them proves challenging, almost as if they're composed in a different programming language altogether. The complexity far surpasses what I would have done to address a similar issue. so, here is my simple take on solving it.

## Key features of this library

### Dynamic values

- To support dynamic values inside the translated content, provide the dynamic values object as the second argument to the `t` helper as shown below.

**Examples:**

```json
{
  "greeting": "Hello, ${name}"
}
```

```js
import { useTranslation } from './translations'

export const App = () => {
  const { t } = useTranslation()
  return <h1>{t('greeting', { name: 'Vinodh' })}</h1>
}
```

### Default translation fallback

- If the translation key is missing in the selected locale JSON then the value from the default locale is returned as a fallback.

**Examples:**

#### en.json

```json
{
  "greeting": "Hello, ${name}",
  "learnMore": "Click on the Vite and React logos to learn more"
}
```

#### fr.json

```json
{
  "greeting": "Bonjour, ${name}"
}
```

#### Component

```js
import { useTranslation } from './translations'

export const App = () => {
  const { t } = useTranslation()
  return (
    <>
      <h1>{t('greeting', { name: 'Vinodh' })}</h1>
      <p>{t('learnMore')}</p>
    </>
  )
}
```

#### Result

- The compiled HTML for the French translation will be something like this.

```html
<h1>Bonjour, Vinodh</h1>
<p>Click on the Vite and React logos to learn more</p>
```

As the 'learnMore' key is not present in the `fr.json`, the corresponding default locale `en.json` value is returned.

### Pluralization

- Pluralization is supported using a special property called `count`.
- To make it work, you need to provide both zero, one, and many values to your JSON files.

Examples:

```json
{
  "items": {
    "zero": "Your cart is empty",
    "one": "You have one item in your cart",
    "many": "You have {count} items in your cart"
  }
}
```

```js
import { useState } from 'react'
import { useTranslation } from './translations'

export const Cart = () => {
  const { t } = useTranslation()
  const [itemsCount, setItemsCount] = useState(0)
  return (
    <>
      <h1>{t('items', { count: itemsCount })}</h1>
      <button onClick={() => setItemsCount((count) => count + 1)}>Add item</button>
    </>
  )
}
```

### HTML support:

- Supports HTML sanitization by default.
- To support HTML tags, add them in the translation params alone and explicitly set the "escapeValue" translation param to true.
- HTML tags inside the translations JSON are not supported as it will make the JSON vulnerable to security attacks.

Examples:

```json
{
  "greeting": "Hello, ${name}. Your role is ${role}"
}
```

- Using the `t` helper

```js
import { useTranslation } from './translations'

export const App = () => {
  const { t } = useTranslation()
  const role = 'Admin'

  return (
    <>
      {t(
        'greeting',
        {
          name: '<span className="font-medium">Vinodh</span>',
          role: `<span className="text-gray">${role}</span>`,
        },
        {
          escapeValue: false,
        }
      )}
    </>
  )
}
```

- To simplify this usage, the library has a custom component that escapes the param values by default internally.

```js
  <RenderTranslationWithHtml
    tKey={'greeting'}
    params={{
      name: '<span className="ca-font-medium">Vinodh</span>',
      role: `<span className="ca-font-medium">${role}</span>`
    }}
  />,
```

Now that we know what we are going to build, let's start with our app setup.

## App setup

Let's quickly create a react app using [Vite](https://vitejs.dev/) bundler.

```bash
pnpm create vite i18n-app --template react-ts
cd i18n-app
pnpm install && pnpm update
pnpm run dev
```

Your app should be available at `http://localhost:5173/`.

PS: If you want to create a react app with complete bells and whistles then check out this [Create](https://dev.to/vinomanick/modern-way-to-create-a-react-app-using-vite-part-1-32ol) a react app using Vite](https://dev.to/vinomanick/modern-way-to-create-a-react-app-using-vite-part-1-32ol) blog.

## Translation files

- Create a `locales` folder that is going to have all our translation JSONs and create the translation JSONs for 'English' and 'French' language.

```bash
mkdir locales
touch locales/en.json
touch locales/fr.json
```

- Update the `en.json` with the following content.

```json
{
  "app": {
    "heading": "Translation supported app"
  }
}
```

- Update the `fr.json` with the following content.

```json
{
  "app": {
    "heading": "Application prise en charge par la traduction"
  }
}
```

- Create an `index.ts` file that is going to export a translation map that contains the supported locales and its corresponding dynamic import loader.

```bash
touch locales/index.ts
```

```js
/* eslint-disable @typescript-eslint/no-explicit-any */
export const TRANSLATIONS_MAPPING: Record<string, () => Promise<any>> = {
  en: () => import('./en.json'),
  fr: () => import('./fr.json'),
}
```

## Translations Library

- Create a `translations` folder inside the src to keep all our translation-related code.

```bash
mkdir src/translations
```

- Create an interface file to store all our common interfaces.

```bash
mkdir src/translations/translations.interfaces.ts
```

```js
/* eslint-disable @typescript-eslint/no-explicit-any */

export interface TranslationsObject {
  [key: string]: any;
}

export interface TranslationsMap {
  [x: string]: () => Promise<TranslationsObject>;
}

export interface TranslateParams {
  count?: number;
  [key: string]: any;
}

export interface TranslateOptions {
  escapeValue?: boolean;
}
```

### Setting up the context

We are going to need a React context to store our translations object which can be used in all the child components.

```bash
touch src/translations/translations-context.tsx
```

```js
import { createContext } from 'react'
import { TranslationsObject } from './translations.interfaces'

interface TContext {
  locale: string;
  isLoading: boolean;
  setLocale: (language: string) => void;
  translations: TranslationsObject;
  defaultLocale: string;
}

export const TranslationsContext =
  createContext <
  TContext >
  {
    locale: '',
    isLoading: true,
    setLocale: () => null,
    translations: {},
    defaultLocale: '',
  }
```

Our basic setup is done for the translations library, let's start building our Translation provider and "useTranslation" hook in the next part.