React Admin v3: Zoom in the i18n Layer

François Zaninotto
François ZaninottoOctober 10, 2019
#react#react-admin#tutorial

React-admin v3 is nearing a stable state, and the 3.0 release is around the corner. Should you upgrade? If you're developing an internationalized application, the answer is: definitely! Because we've overhauled the i18n subsystem, and the new one offers extreme flexibility and an improved developer experience.

This tutorial shows how it affects common usage of the i18n layer: translating labels, showing the current locale, and offering a language switch menu.

Translating Labels

If you need to translate messages in your own components, React-admin v2 provides a translate HOC that injects a translate callback:

// in src/MyHelloButton.js
import React from "react";
import { translate } from "react-admin";

const MyHelloButton = ({ translate }) => (
  <button>{translate("myroot.hello.world")}</button>
);

export default translate(MyHelloButton);

The fact that the HOC and the injected callback share the same name was confusing. Besides, HOCs are so 2018. In react-admin v3, prefer the useTranslate hook, which returns the translate function:

// in src/MyHelloButton.js
import React from "react";
import { useTranslate } from "react-admin";

const MyHelloButton = () => {
  const translate = useTranslate();
  return <button>{translate("myroot.hello.world")}</button>;
};

export default MyHelloButton;

Don't use useTranslate for Field and Input labels, or for page titles, as they are already translated:

// don't do this
<TextField source="first_name" label={translate('myroot.first_name')} />

// do this instead
<TextField source="first_name" label="myroot.first_name" />

// or even better, use the default translation key
<TextField source="first_name" />
// and translate the `resources.customers.fields.first_name` key

The translate function returned by the translate HOC and the useTranslate hook are the same. It relies on Polyglot.js, an i18n library open-sourced by Airbnb, which supports replacement and pluralization:

const DeleteItemsButton = ({ items, type }) => {
    const translate = useTranslate();
    return (
        <button>{translate('myroot.action.delete_items', { smart_count: items.length, type })}</button>
    );
}

// uses this translation messages
const englishMessages = {
    // ...
    myroot: {
        action: {
            delete_items: "Delete one %{type} |||| Delete %{smart_count} %{type}s",
        }
    }
}

// later in the code
<DeleteItemsButton type="post" items={[{}, {}]} /> // "Delete 2 posts"

Showing The Current Locale And Allowing to Change It

Let's imagine that you want to display a languages menu, showing the available languages and allowing the user to choose another one.

In react-admin v2, you had to grab the current locale from the Redux store, and dispatch a Redux action to change it:

import React from "react";
import Button from "@material-ui/core/Button";
import { connect } from "react-redux";
import compose from "recompose/compose";
import { translate, changeLocale } from "react-admin";

const LocaleSwitcher = ({ locale, changeLocale, translate }) => {
  return (
    <div>
      <div>{translate("myroot.language.label")}</div>
      <Button disabled={locale === "fr"} onClick={() => changeLocale("fr")}>
        {translate("myroot.language.french")}
      </Button>
      <Button disabled={locale === "en"} onClick={() => changeLocale("en")}>
        {translate("myroot.language.english")}
      </Button>
    </div>
  );
};

const mapStateToProps = state => ({
  locale: state.i18n.locale,
});

const enhance = compose(
  connect(
    mapStateToProps,
    { changeLocale }
  ),
  translate
);

export default enhance(LocaleSwitcher);

In v3, no more Redux: use the useLocale and useSetLocale hooks in addition to the useTranslate hook:

import React from "react";
import Button from "@material-ui/core/Button";
import { useTranslate, useLocale, useSetLocale } from "react-admin";

const LocaleSwitcher = () => {
  const translate = useTranslate();
  const locale = useLocale();
  const setLocale = useSetLocale();
  return (
    <div>
      <div>{translate("myroot.language.label")}</div>
      <Button disabled={locale === "fr"} onClick={() => setLocale("fr")}>
        {translate("myroot.language.french")}
      </Button>
      <Button disabled={locale === "en"} onClick={() => setLocale("en")}>
        {translate("myroot.language.english")}
      </Button>
    </div>
  );
};

export default LocaleSwitcher;

This component uses zero HOC, is more idiomatic to modern react, it's easier to read and test.

Injecting A Different i18nProvider

Under the hood, all the i18n tasks are handled by the i18nProvider. React-admin grabs it from the <Admin> component, and puts it in a React context.

import i18nProvider from './i18n/i18nProvider';

const App = () => (
    <Admin
        dataProvider={dataProvider}
        i18nProvider={i18nProvider}
    >
        <Resource name="posts" list={...}>
        // ...

The i18n hooks are mostly proxies for the i18nProvider, which they get from the context. The i18nProvider signature is:

const i18nProvider = {
  translate: (key, options) => string,
  changeLocale: locale => Promise,
  getLocale: () => string,
};

And that's all. By default, react-admin uses an i18nProvider based on Polyglot.js, but you can use an alternative provider, e.g. to handle translations in GETTEXT files rather than JSON.

Upgrade Path

As the i18nProvider signature changed, you'll need a small change in your react-admin v2 application to be able to use v3. To ease the transition, react-admin v3 contains a module called ra-i18n-polyglot, that is a wrapper around your old i18nProvider to make it compatible with the new provider signature:

Besides, the Admin component does not accept a locale prop anymore as it is the i18nProvider provider responsibility:

import React from 'react';
import { Admin, Resource } from 'react-admin';
+import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

const messages = {
    fr: frenchMessages,
    en: englishMessages,
};
-const i18nProvider = locale => messages[locale];
+const i18nProvider = polyglotI18nProvider(locale => messages[locale], 'fr');

const App = () => (
-    <Admin locale="fr" i18nProvider={i18nProvider}>
+    <Admin i18nProvider={i18nProvider}>
        ...
    </Admin>
);

export default App;

Also, if you used to dispatch the changeLocale action, you'll have to call the useSetLocale hook instead.

Conclusion

With react-admin v3, you shouldn't have to look for a path in the Redux store, or mess with action creators. You shouldn't be blocked by a dependency either: you can replace any provider (i18nProvider, but also authProvider and dataProvider) by your own. In short, react-admin v3 is more straightforward to learn and use.

We bet you'll love it!

Did you like this article? Share it!