DEV Community

Vinodh Kumar
Vinodh Kumar

Posted on

Building a minimal i18n library for react app - Part 1

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. 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:

{
  "greeting": "Hello, ${name}"
}
Enter fullscreen mode Exit fullscreen mode
import { useTranslation } from './translations'

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

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

{
  "greeting": "Hello, ${name}",
  "learnMore": "Click on the Vite and React logos to learn more"
}
Enter fullscreen mode Exit fullscreen mode

fr.json

{
  "greeting": "Bonjour, ${name}"
}
Enter fullscreen mode Exit fullscreen mode

Component

import { useTranslation } from './translations'

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

Result

  • The compiled HTML for the French translation will be something like this.
<h1>Bonjour, Vinodh</h1>
<p>Click on the Vite and React logos to learn more</p>
Enter fullscreen mode Exit fullscreen mode

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:

{
  "items": {
    "zero": "Your cart is empty",
    "one": "You have one item in your cart",
    "many": "You have {count} items in your cart"
  }
}
Enter fullscreen mode Exit fullscreen mode
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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "greeting": "Hello, ${name}. Your role is ${role}"
}
Enter fullscreen mode Exit fullscreen mode
  • Using the t helper
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,
        }
      )}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • To simplify this usage, the library has a custom component that escapes the param values by default internally.
  <RenderTranslationWithHtml
    tKey={'greeting'}
    params={{
      name: '<span className="ca-font-medium">Vinodh</span>',
      role: `<span className="ca-font-medium">${role}</span>`
    }}
  />,
Enter fullscreen mode Exit fullscreen mode

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 bundler.

pnpm create vite i18n-app --template react-ts
cd i18n-app
pnpm install && pnpm update
pnpm run dev
Enter fullscreen mode Exit fullscreen mode

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 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.
mkdir locales
touch locales/en.json
touch locales/fr.json
Enter fullscreen mode Exit fullscreen mode
  • Update the en.json with the following content.
{
  "app": {
    "heading": "Translation supported app"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Update the fr.json with the following content.
{
  "app": {
    "heading": "Application prise en charge par la traduction"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Create an index.ts file that is going to export a translation map that contains the supported locales and its corresponding dynamic import loader.
touch locales/index.ts
Enter fullscreen mode Exit fullscreen mode
/* eslint-disable @typescript-eslint/no-explicit-any */
export const TRANSLATIONS_MAPPING: Record<string, () => Promise<any>> = {
  en: () => import('./en.json'),
  fr: () => import('./fr.json'),
}
Enter fullscreen mode Exit fullscreen mode

Translations Library

  • Create a translations folder inside the src to keep all our translation-related code.
mkdir src/translations
Enter fullscreen mode Exit fullscreen mode
  • Create an interface file to store all our common interfaces.
mkdir src/translations/translations.interfaces.ts
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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.

touch src/translations/translations-context.tsx
Enter fullscreen mode Exit fullscreen mode
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: '',
  }
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)