How to manage internationalization in React ? react-intl like

If you make an application which will be used all over the world, you probably want to handle internationalization for texts, dates and numbers.

It already exists libraries to do that like react-intl, LinguiJS or i18next. In this article we will do our own implementation which is similar to react-intl one.

React context

Before starting to code, it's important to know React context and understand its use.

Basically, it permits to put some data (object, callback, ...) in a Context which will be accessible through a Provider to all children component of this provider. It's useful to prevent props drilling through many components.

This code:

function App() {
  return (
    <div>
      Gonna pass a prop through components
      <ChildFirstLevel myProp="A prop to pass" />
    </div>
  );
}

function ChildFirstLevel({ myProp }) {
  return <ChildSecondLevel myProp={myProp} />;
}

function ChildSecondLevel({ myProp }) {
  return <ChildThirdLevel myProp={myProp} />;
}

function ChildThirdLevel({ myProp }) {
  // Some process with myProp
  // It's the only component that needs the props

  return <p>This component uses myProp</p>;
}

Can become:

import { createContext, useContext } from "react";

const MyContext = createContext();

function App() {
  return (
    <MyContext.Provider value="A prop to pass">
      <div>
        Gonna pass a value with react context
        <ChildFirstLevel />
      </div>
    </MyContext.Provider>
  );
}

function ChildFirstLevel() {
  return <ChildSecondLevel />;
}

function ChildSecondLevel() {
  return <ChildThirdLevel />;
}

function ChildThirdLevel() {
  const myProp = useContext(MyContext);
  // Some process with myProp
  // It's the only component that needs the props

  return <p>This component uses myProp</p>;
}

This is just an example that would probably not exist in real application

For more information the React documentation is awesome.

I18n implementation

Creation of the Provider

The first step is to create the React context with the Provider which will provides our utilities callback in next parts. This provider will take in parameter the locale which will be used for the current user, which could be the value of navigator.language for example.

import { createContext, useContext, useMemo } from "react";

const I18nContext = createContext();

const useI18nContext = () => useContext(I18nContext);

function I18nProvider({ children, locale }) {
  const value = useMemo(
    () => ({
      locale,
    }),
    [locale]
  );

  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
}

Note: I have memoized the value, because in the project some components could be memoized because having performance problem (costly/long render)

In the next parts we will add some utilities functions in the context to get our value in function of the locale

Translation messages

Implementation

For our example we will just do an object of translations by locale with locale. Translations will be values by key.

const MESSAGES = {
  en: {
    title: 'This is a title for the application',
    body: 'You need a body content?'
  },
  fr: {
    title: 'Ceci est le titre de l\'application',
    body: 'Besoin de contenu pour le body?'
  }
};

These translations will be passed to our Provider (but not put in the context).

In applications, these translations can be separated in file for specific language for example: messages-fr.properties, messages-en.properties, ... Then you can build an object from these files ;)

Now let's implement the method to get a message from its key in the Provider:

// The messages are passed to the Provider
function I18nProvider({ children, locale, messages }) {

  // The user needs to only pass the messageKey
  const getMessage = useCallback((messageKey) => {
     return messages[locale][messageKey];
  }, [locale, messages]);

  const value = useMemo(() => ({
     locale,
     getMessage,
  }), [locale, getMessage]);

  return (
     <I18nContext.Provider value={value}>
       {children}
     </I18nContext.Provider>
  );
}

It can happen that there is no translation in the current locale (maybe because you do translate messages from a specific enterprise). So it can be useful to give a defaultLocale to fallback to with locale and/or a defaultMessage. The Provider becomes:

// Pass an optional defaultLocale to the Provider
function I18nProvider({
  children,
  locale,
  defaultLocale,
  messages,
}) {
  // Fallback to the `defaultMessage`, if there is no
  // defaultMessage fallback to the `defaultLocale`
  const getMessage = useCallback(
    ({ messageKey, defaultMessage }) => {
      return (
        messages[locale]?.[messageKey] ??
        defaultMessage ??
        messages[defaultLocale][messageKey]
      );
    },
    [locale, messages, defaultLocale]
  );

  const value = useMemo(
    () => ({
      locale,
      getMessage,
    }),
    [locale, getMessage]
  );

  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
}

Get a message value

There is multiple possibilities to get a message:

  • get the function getMessage with useI18nContext
const { getMessage } = useI18nContext();

const title = getMessage({ messageKey: 'title' });
  • implements a component I18nMessage that has messageKey and defaultMessage
function I18nMessage({ messageKey, defaultMessage }) {
  const { getMessage } = useI18nContext();

  return getMessage({ messageKey, defaultMessage });
}

// Use
<I18nMessage messageKey="title" />
  • implements an HOC withI18n that injects getMessage to our component
function withI18n(WrappedComponent) {
  const Component = (props) => {
    const { getMessage } = useI18nContext();

    return (
      <WrappedComponent
        {...props}
        getMessage={getMessage}
      />
    );
  };
  Component.displayName = "I18n" + WrappedComponent.name;

  return Component;
}

function Title({ getMessage }) {
  const title = getMessage({ messageKey: "title" });

  return <h1>title</h1>;
}

const I18nConnectedTitle = withI18n(Title);

Dates handling

Ok, now let's handle Date formatting. In function of the country (or locale) a date does not have the same displayed format. For example:

// Watch out the month is 0-based
const date = new Date(2021, 5, 23);

// In en-US should be displayed
"6/23/2021"

// In fr-FR should be displayed
"23/06/2021"

// In en-IN should be displayed
"23/6/2021"

Note: The time is also not formatted the same way in function of the locale

To implements this feature, we are gonna use the Intl.DateTimeFormat API which is accessible on all browsers.

Note: Intl relays on the Common Locale Data Repository

Implementations

For the implementation we are gonna expose to the user the possibility to use all the option of the Intl API for more flexibility.

The previous I18nProvider becomes:

function I18nProvider({
  children,
  locale,
  defaultLocale,
  messages,
}) {
  const getMessage = useCallback(
    ({ messageKey, defaultMessage }) => {
      return (
        messages[locale]?.[messageKey] ??
        defaultMessage ??
        messages[defaultLocale][messageKey]
      );
    },
    [locale, messages, defaultLocale]
  );

  const getFormattedDate = useCallback(
    (date, options = {}) =>
      Intl.DateTimeFormat(locale, options).format(date),
    [locale]
  );

  const value = useMemo(
    () => ({
      locale,
      getMessage,
      getFormattedDate,
    }),
    [
      locale,
      getMessage,
      getFormattedDate,
    ]
  );

  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
}

Note: I will not describe the usage, because it's exactly the same as translations but you can see the final implementation in the codesandbox in conclusion.

Number format handling

If you want to manage numbers, price, ... in your project, it can be useful to format these entities in the right one not to disturb users.

For example:

  • separator symbol is not the same
  • the place and the symbol of the currency can be different
  • ...
const number = 123456.789;

// In en-US should be displayed
"123,456.789"

// In fr-FR should be displayed
"123 456,789"

// In en-IN should be displayed
"1,23,456.789"

To do that we are gonna use the API Intl.NumberFormat which works on all browsers.

Implementations

If you look at the documentation of Intl.NumberFormat, you can see that there is a tone of options available in second parameter, so in our implementation (like with date formatting) we will pass an options object.

Our I18nProvider becomes then:

function I18nProvider({
  children,
  locale,
  defaultLocale,
  messages,
}) {
  const getMessage = useCallback(
    ({ messageKey, defaultMessage }) => {
      return (
        messages[locale]?.[messageKey] ??
        defaultMessage ??
        messages[defaultLocale][messageKey]
      );
    },
    [locale, messages, defaultLocale]
  );

  const getFormattedDate = useCallback(
    (date, options = {}) =>
      Intl.DateTimeFormat(locale, options).format(date),
    [locale]
  );

  const getFormattedNumber = useCallback(
    (number, options = {}) =>
      Intl.NumberFormat(locale, options).format(number),
    [locale]
  );

  const value = useMemo(
    () => ({
      locale,
      getMessage,
      getFormattedDate,
      getFormattedNumber,
    }),
    [
      locale,
      getMessage,
      getFormattedDate,
      getFormattedNumber,
    ]
  );

  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
}

Note: When working with currency, it can be boring to always pass the right option so we could make a utility function name getFormattedCurrency which only take the currency name in second parameter:

const getFormattedCurrency = useCallback(
  (number, currency) =>
    Intl.NumberFormat(locale, {
      style: "currency",
      currency,
    }).format(number),
  [locale]
);

Conclusion

We have seen together how to manage simply manage internationalization in React by using React context. It consists to just pass the locale, message translations to the provider and then put utility methods in the context to get a message translated and formatted date, number or currency.

We also used the wonderful API Intl for formatted date and number which relays on the CLDR.

You can play in live with internationalization here.

Want to see more ? Follow me on Twitter or go to my Website. 🐼

17