Multi-language routing in React
Tags: react, routing, webdev, tutorial
Published: 2019-10-30 12:55:46

# Multi-language routing in React

One of the great things around routing in React is that its ecosystem has allowed for great and very declarative routing syntax. You can define your routing as a set of components, write an object structure from which you will render the routing logic, etc.

And it’s constantly improving and getting better and better:

- [react-router v5.1 is out](https://reacttraining.com/blog/react-router-v5-1/)
- [reach](https://reach.tech/router) router is also getting big traction, check it out.

But, what if you want to support a true multi-language routing, which will support route names in multiple languages and redirect your users to correct routes when you change languages?

### Is that really a big deal?

Well, it is definitively possible to go on without such a feature and have a fully usable website. There are a lot of websites that have English-only routing, but multi-language content.

From a _development_ perspective, the reasons for this vary:

- A framework that doesn’t support it is used.
- It’s a big effort to implement.
- It’s not always easy to maintain.

However, having a multi-language route localization can give you and your _end-users_ the following benefits:

- multi-language SEO
- users get the additional context of the page hierarchy in their own language

A solution written in React is relatively simple to implement and maintain, and this article will outline the packages and methods that will guide you to a solution.

The example is written with _TypeScript_, latest _react-router-dom_, and _react-hooks_.

## Add a Router package

In case you aren’t using a router package, try [react-router-dom](https://www.npmjs.com/package/react-router-dom) out.

```bash
yarn add react-router-dom
yarn add @types/react-router-dom --dev
```


After adding a router, we should define a few routes and components that will be used on our website.

```jsx
export const App: React.FC = () => (
  <BrowserRouter>
    <AppLayout>
      <Switch>
        <Route exact path={AppRoute.Home}>
          <views.Home />
        </Route>
        <Route exact path={AppRoute.Summary}>
          <views.Summary />
        </Route>
        <Route path="*">
          <views.GeneralError />
        </Route>
      </Switch>
    </AppLayout>
  </BrowserRouter>
);
```

In the latest react-router-dom version, component and render props were scrapped for children prop which is much more flexible. The only downside is that the v4 version was more concise and readable in most scenarios. Please note that the old way of doing things through component/render props is still available at this moment, but it will become deprecated soon.

We also added an **_AppLayout_** component which allows us to have a global header, navigation, and footer, and renders the routes inside the main tag as partial views.

There is also a fallback route here that renders the error component so our users know that they ended up on the error page in case they try to access a route that doesn’t exist.

## Add an i18n package

First, we need to add a package that will allow us to internationalize things in our app. There are a lot of good examples, but one of the best packages around is [react-intl](https://github.com/formatjs/react-intl).

It is a project by [FormatJS](https://formatjs.io/) (Yahoo! Inc) which has impressive support for localizing almost everything, including currencies, dates, etc.

```bash
    yarn add react-intl
```

This package was written in Typescript so it has its own types included.

### Adding a base locale

It’s always easiest to start with a language that will be the primary language for the website as a baseline. You can always add more languages easily later on.

Let’s first add an enum which will be a collection of all languages used inside our app. For start, we will only add the base language.

```ts
export enum AppLanguage {
  English = 'en',
}
```

The value of each enum property should match a two-letter country code ISO locale.

After adding a language, we should also add some language strings for that language, which we will use to localize our routes and other content.

Create an intl folder somewhere in the app, and a file for your base language.

```ts
const baseStrings = {
  /** Routes */
  'routes.home': '/',
  'routes.summary': '/summary',

  ...
};

export type LanguageStrings = typeof baseStrings;
export const en = baseStrings;
```

The exported type will be used as an equality enforcer which all other languages need to support, meaning that any localization added to _baseStrings_ will need to be added to other files in order to enforce some safety. It also works vice-versa.

If you try to add a string to a specific language which doesn’t exist in the base strings file, you will get a compilation error. This will enforce that all of the used languages have all strings at least set, if not translated, and save you from runtime errors.

We are also exporting the _baseStrings_ as a matching iso variable for the language at hand.

Now let’s add a matching enum (or frozen object in plain JS) which we can use to reference the routes to avoid any typos.

```ts
export enum AppRoute {
  Home = 'routes.home',
  Summary = 'routes.summary'
}
```

### Localized Switch component

In order to simplify the process of translating the route paths, we will create a custom **LocalizedSwitch** component that handles this logic.

It is also possible to do this on the route component level, however, swapping out the Switch component allows you to support this with the least amount of changes, as it is easier to update the parent then every route to a LocalizedRoute variant. Changing the route component is probably a more flexible solution though.

The intended suggestion for the LocalisedSwitch component is imagined as a drop-in replacement for the normal Switch one, and it is designed to work with Route components from the **react-router-dom** package.

```jsx
export const LocalizedSwitch: React.FC = ({ children }) => {
  /**
   * inject params and formatMessage through hooks, so we can localize the route
   */
  const { formatMessage, locale } = useIntl();

  /**
   * Apply localization to all routes
   * Also checks if all children elements are <Route /> components
   */
  return (
    <Switch>
      {React.Children.map(children, child =>
        React.isValidElement<RouteProps>(child)
          ? React.cloneElement(child, {
              ...child.props,
              path: localizeRoutePath(child.props.path)
            })
          : child
      )}
    </Switch>
  );

  /**
   *
   * @param path can be string, undefined or string array
   * @returns Localized string path or path array
   */
  function localizeRoutePath(path?: string | string[]) {
    switch (typeof path) {
      case 'undefined':
        return undefined;
      case 'object':
        return path.map(key => `/${locale}` + formatMessage({ id: key }));
      default:
        const isFallbackRoute = path === '*';
        return isFallbackRoute
          ? path
          : `/${locale}` + formatMessage({ id: path });
    }
  }
};
```


### Wiring it all up

To wire it all together, we need to add the **IntlProvider** component from the [react-intl](https://github.com/formatjs/react-intl) package, connect it to the data we defined, and add our own LocalizedSwitch component.

```jsx
export const App: React.FC = () => (
  <LocalizedRouter
    RouterComponent={BrowserRouter}
    languages={AppLanguage}
    appStrings={appStrings}
  >
    <AppLayout>
      <LocalizedSwitch>
        <Route exact path={AppRoute.Home}>
          <views.Home />
        </Route>
        <Route exact path={AppRoute.Summary}>
          <views.Summary />
        </Route>
        <Route path="*">
          <views.GeneralError />
        </Route>
      </LocalizedSwitch>
    </AppLayout>
  </LocalizedRouter>
);
```


## Supporting multiple languages

Now that we have covered the basics of setting up the logic that allows us to internationalize our application and localize the application routes, we need to add support for other languages and add their route definitions.

For the purpose of this example, let’s add support for _Deutch_, _French_ and _Croatian_ languages, all inside the intl folder that we already have.

> **Disclaimer:** I don’t personally speak Deutch or French, so if the translations are not perfect, I do apologize for mangling your language, but please blame Google Translate :)

### Adding translations for a new language

Just add a new language file inside the intl folder:

```ts
export const de: LanguageStrings = {
  /** Routes */
  'routes.home': '/',
  'routes.summary': '/zusammenfassung',

  ...
};
```

If you are wondering why this was done in **.ts** file in this scenario, and not another format like JSON, the sole purpose is to enforce safety that comes with using TypeScript.

You can, of course, write these in JSON, JS or another preferred format in case you don’t want or need the type-safety.

For every language file you add, extend the **AppLanguage** enum.

### Updating the router

We first need to update the router to support redirecting to other languages, reading the current language from the pathname, and setting the locale accordingly.

Expected behaviour:

    /summary -> Redirect to base language
    /en/summary -> English language summary page
    /de/zusammenfassung -> German language summary page

We will swap out the default router component with the one that supports pathname detection and returns react-intl provider.

```jsx
interface Props {
  RouterComponent: React.ComponentClass<any>;
  languages: { [k: number]: string };
  appStrings: { [prop: string]: LanguageStrings };
  defaultLanguage?: AppLanguage;
}

export const LocalizedRouter: React.FC<Props> = ({
  children,
  RouterComponent,
  appStrings,
  defaultLanguage
}) => (
  <RouterComponent>
    <Route path="/:lang([a-z]{2})">
      {({ match, location }) => {
        /**
         * Get current language
         * Set default locale to en if base path is used without a language
         */
        const params = match ? match.params : {};
        const { lang = defaultLanguage || AppLanguage.English } = params;

        /**
         * If language is not in route path, redirect to language root
         */
        const { pathname } = location;
        if (!pathname.includes(`/${lang}/`)) {
          return <Redirect to={`/${lang}/`} />;
        }

        /**
         * Return Intl provider with default language set
         */
        return (
          <IntlProvider locale={lang} messages={appStrings[lang]}>
            {children}
          </IntlProvider>
        );
      }}
    </Route>
  </RouterComponent>
);
```

Wrapping everything in a route, allows us to use regex to determine the language from the pathname, and use that match to inject the current language into the provider.

Also, our new router component will enforce that a language is always a part of the pathname.

The regex used in this example will only support lowercase language, but you can modify it to **[a-zA-z]{2}** and use **String.toLowercase()** method when pathname matching if you want to support uppercase routes as well.

### Language switcher

We also need to add a language switcher component that will allow us to change the active language and show the currently activated language based on the pathname.

Apart from the styling, we need a helper function that checks for matching route inside the strings object for other languages if we want to support navigating to the same page in another language directly.

```jsx
export const LanguageSwitcher: React.FC = () => {
  const { pathname } = useLocation();
  const { locale, messages } = useIntl();

  return (
    <ul className={css(list.container)}>
      {Object.keys(AppLanguage).map(lang => (
        <li key={lang} className={css(list.item)}>
          <NavLink
            className={css(link.primary)}
            activeClassName={css(link.active)}
            to={getMatchingRoute(AppLanguage[lang])}
          >
            {AppLanguage[lang]}
          </NavLink>
        </li>
      ))}
    </ul>
  );

  function getMatchingRoute(language: string) {
    /**
     * Get the key of the route the user is currently on
     */
    const [, route] = pathname.split(locale);
    const routeKey = Object.keys(messages).find(key => messages[key] === route);

    /**
     * Find the matching route for the new language
     */
    const matchingRoute = appStrings[language][routeKey];

    /**
     * Return localized route
     */
    return `/${language}` + matchingRoute;
  }
};
```

**Navigation**

The last thing to do is updating the **Navigation** component itself, to also support switching to other routes in all languages.

We simply use the **formatMessage** function from the react-intl hook for this purpose.

```jsx
export const Navigation: React.FC = () => {
  const { formatMessage, locale } = useIntl();

  return (
    <ul className={css(list.container)}>
      {Object.keys(AppRoute).map(elem => (
        <li key={elem} className={css(list.item)}>
          <NavLink
            exact
            className={css(link.primary)}
            activeClassName={css(link.active)}
            to={localizeRouteKey(AppRoute[elem])}
          >
            {formatMessage({ id: AppRouteTitles.get(AppRoute[elem]) || '' })}
          </NavLink>
        </li>
      ))}
    </ul>
  );

  function localizeRouteKey(path: string) {
    return `/${locale}` + formatMessage({ id: path });
  }
};
```


In order to allow for easier route name resolution, since TS enums don’t allow for reverse mapping on string enums, you can create an ES6 map.

```ts
export const AppRouteTitles = new Map([
  [AppRoute.Home, 'home.title'],
  [AppRoute.Summary, 'summary.title']
]);
```

## Summary

As you can see, localizing the routes of the website is not a hard task in React. It requires a few components and little thinking on the side of the project architecture, so you don’t overcomplicate things. The result is easy to understand the solution that will easily scale regardless of the language count that you might add later on.

A fully working example can be found on:
[**vlaja/multilanguage-routing-react**](https://github.com/vlaja/multilanguage-routing-react)