Building an Awesome Magazine App with i18n in React!

When building React single-page applications with i18n and l10n, a few concerns come into play: routing and links, locale switching, i18n-ized UI, and of course localized content. Thankfully, React's component modules, Redux's Flux architecture, and a handful of other libraries can help us quickly whip up i18n-ized prototypes—which we can turn into full on production apps. In this article we build two i18n-ized React / Redux SPAs: an admin back end and a front-facing app, with many of the react i18n and l10n bells and whistles that production apps would need. Coming along for the ride.

The movie magazine app

Let’s assume we’ve been commissioned to build a clean prototype for μveez, a new i18n-ized movie magazine app. Our client has asked that we build it as an SPA with the React view framework, since React came highly recommended by her colleagues for performant SPAs. We’ve been asked to build two prototype SPAs, actually: one for the admin panel and one for the front-facing website. The agreed feature list is as follows.

General

  • μveez will initially support Arabic, English, and French

Admin

  • Film director index with names in supported locales
  • Adding a director with translations in supported locales
  • Movie index with titles in supported locales
  • Adding a movie with translated titles and synopses in supported locales

Front

  • Language switching between our supported locales, covering RTL directionality for Arabic
  • Home page with featured directors, quote of the day, and featured movies
  • Movie index
  • Single (show) movie

Framework

Note » I’ll assume that you have a basic working knowledge of React and Redux.

Alright, we’ve worked with React before, so we know that we’ll likely want to adopt a Flux architecture. Flux’s uni-directional data flow makes it easy to reason about our app state, and places this state in one DRY store. Redux is a well-supported Flux implementation, so we’ll use that. We’ll also need to handle routing and basic i18n UI. To get going quickly, we can pull in Bootstrap for a CSS framework.

Our entire framework (with versions at time of writing) can, then, look like this.

A Little Organization: Directory Structure

We’ll adopt the common differentiation between React state-aware containers and presentational components. We’ll also want to place our Redux actions and reducers in logical locations. And we may well need a place for our apps’ services. Given that we bootstrap our apps with create-react-app, our directory structure can be this beauty:

We’ll mock our server back-end with JSON files that we place in the public/api directory, and we’ll explore these files in detail later. For now, let’s get to to building! We’ll start with the admin panel.

Note » If you’re a React / Redux guru and you just want to get to the juicy i18n config, UI, and routing bits, you may want to skip ahead to our building of the front-facing magazine app.

The Admin Panel

Note » You can see a live demo of the admin panel on heroku. You can also find all of the panel’s  source code on Github.

Scaffolding

Our Store

Let’s setup our Redux store.

/src/index.js

If you’ve used React and Redux before, this is pretty standard stuff. We’re simply wrapping our whole app in the Redux store Provider so that our store is available to any App subcomponent that needs it. To keep things clean, we’ve housed our store in its own file. Let’s take a quick look at it.

/src/services/store.js

We bring in Redux Thunk as middleware to handle asynchronous Flux actions. The handy Redux Devtools browser extension is tied in to help us debug our state in our development environment. Our reducers will be explored as we dive into each of our view models. Now let’s get to routing.

Routing

We’ll assume that our admin panel UI can be in English only, so we won’t worry about i18n-ized routes until we get to our front-facing app. For now, we can configure our admin’s routes as per our requirements.

/src/routes.js

We’ll have a home page, a directors index (which will include a simple Add Director form), and a movies index. The form for adding a movie will be relatively large, so it’s broken out into its component. Again, we’ll get to each of these components, as well as their respective reducers and actions, a bit later. For now, let’s round out our scaffolding by implementing our routing and creating our basic app layout.

/src/components/App.js

We spin over our configured routes and render a Route component for each one. We wrap the majority of our app in the requisite BrowserRouter component (aliased as Router), and use Bootstrap’s .container for layout.

Note » If you’re not familiar with React Router, check out its excellent documentation.

Our AppNavbar and AppFooter are pretty much presentational here, so we’ll skip their dissection for brevity. You can check them out in the Github repo if you’re curious about how they’re coded.

You may have noticed that our root component is Home, which means that we’ll render that component when we hit our / route. Home is a simple, stateless functional component. Let’s take a look at it.

/src/components/Home.js

We use reactstrap’s Bootstrap components to style, responsively size, and centre our list of Links. That’s it really.

Here’s a look at our app so far.

Not bad for a prototype. Thank you, Twitter Bootstrap 🙏🏽.

Alright, that’s our admin panel scaffolded. Let’s to get to our model CRUD. We’ll start with directors.

Director CRUD

We can start with a Directors component that will contain our AddDirectors form and DirectorList index.

Note » As per as our client’s requirements, we’re skipping updating and deleting director functionality. This is a proof of concept prototype after all.

/src/components/Directors.js

Our DirectorList will need to load data from our mock API. Let’s take a look at some of this JSON.

/src/public/api/directors.json (excerpt)

This is how we would expect a request like GET /admin/api/directors to respond. We can consume this “API” and present it in our views. Let’s take a look at how our director list would look like.

A simple <table> should be good to get us started. We just need to pull in the data from our JSON file and load it into this table, minding our separation of concerns. If you know the ways of Reacty Reduxy Fluxy kung-fu, you know what’s next: a reducer, young grasshopper.

/src/reducers/directors.js 

Our ADD_DIRECTORS action will reduce our state to the given list of directors, merging it in with whatever directors are currently loaded. We use the popular utility library, Lodash, and its handy unionBy function, to help us with the merge.

Next, we’ll need to write a couple of actions that fetch our existing directors and add them to our app state.

/src/actions/index.js

The fetchDirectors action is asynchronous. The Redux Thunk middleware will notice that we’re returning a function from fetchDirectors and step in to handle the action. It will also provide the returned function with a dispatcher to allow us to call other actions.

We use the standard fetch API to make an async request that asks for our mock JSON. Once we get that JSON, we call our addDirectors action with the directors we’ve received. Of course, our directors reducer is already setup to handle this action and update our app state.

Note » fetch is widely supported, but not 100%. Some older browsers do not support the relatively new API. If you want something closer to complete browser coverage, you may want to add a polyfill or use a library like axios for your XHR calls.

We can now build out our view.

/src/containers/DirectorList.js

We simply map state.directors.directors to a directors prop in our React Component, fetch the existing directors when our component has mounted, and render out our directors as table rows. Bada boom, bada bing.

Let’s get to adding a director in our demo admin app. First, we’ll need some new bits of state.

/src/reducers/directors.js (excerpt)

Since we don’t have a real back-end, we’ll track the lastId of an added director in browser memory. We’ll also keep track of the entered translations of a newDirector as the user enters them.  We can use this new state to add the new director at the appropriate time.

Let’s actually track our lastId when we add directors.

/src/reducers/directors.js (excerpt)

Whenever we add directors in bulk, we get the lastId added to the “back-end” by getting the largest id in our current set of our directors.

Alright, let’s get to our view for adding directors. While we’re in the directors reducer, let’s add a new action handler for tracking translation user input.

/src/reducers/directors.js (excerpt)

To avoid undefined and null values—which will cause React to throw an error when our AddDirector component is populating its text fields—we default our translations values to current state using the defaultOnUndefinedOrNull utility function. This function checks if its first parameter is undefined or null, and if it is returns the second parameter. Otherwise it simply returns the first parameter.

When the user starts typing her Arabic translation, for example, the English and French translations will cycle through our app state, remaining as '' (empty strings). When she moves on to writing her English translation, the Arabic translation will be maintained as she entered it.

A setNewDirector action will be dispatched to track our new director translations state.

/src/actions/index.js (excerpt)

Of course, our AddDirector view will be the sheer epitome of UX design.

Alan Cooper would be proud. Ok, ok. Let’s get to the code.

/src/containers/AddDirector.js

Reactstrap’s presentational components are pulled in for styling. We also wire up our setNewDirector action to be dispatched whenever our AddDirectorTranslations are changed ie. whenever the user enters text. And, since AddDirectorTranslation’s internal text input is controlled, we make sure to pass it the relevant part of our newDirector state. This way we ensure uni-directional data flow. Our state is always the single source of truth about the newDirector’s translations. This keeps things nice and easy to reason about.

Let’s dive into the AddDirectorTranslation component just to see what it’s composed of.

/src/components/AddDirectorTranslation.js

We default our input’s directionality to left-to-right if none is provided by the developer. We also connect the synthetic onChange input event to the parent component, calling its provided onChange, delegating upwards. AddDirectorTranslation is effectively a presentational component that offers connections into its text input.

Ok, let’s go back to our directors reducer. We’ll update it to include the action handling logic that will add a new director to our state from user input.

/src/reducers/directors.js (excerpt)

ADD_DIRECTOR is handled by first incrementing our lastId, since we’re going to be upping the count of the directors collection in our state. We use this incremented value as the id of the director we’re adding, and bring in the user-entered name translations of the director while we’re at it. To clear out the text inputs, we make sure to reset the user-entered translation state when we reduce.

Ok, now we’ll need a quick action that we can dispatch to add the new director.

/src/actions/index.js (excerpt)

We can now call addDirector from our view to finish up our add director functionality.

/src/containers/AddDirector.js (excerpt)

We call _addDirector() when our Add button is clicked. The function does some rudimentary input validation, making sure all the translations have values, and then dispatches the addDirector action.

 

Note » It’s “Michel” Gondry, not “Michael”. The guy’s French for God’s sake.

Of course, in a production app, we would be making an API call when we add a director: something like POST /admin/api/movies with the translated name params. We’re just demoing here though, so we’ll omit the server call for brevity.

Et voilà! Our add director demo is working 🚀

The movie index and add movie functionality are essentially more complex versions of the DirectorList and AddDirector components, respectively. From an i18n / l10n perspective they shed no new light on administrating models, so I won’t go over movie admin here. You can play with movie admin in the demo app, and peruse all of the admin movie code in the Github repo.

The Front-facing Magazine App

Alright, we show the client our admin panel prototype, and she wonders why there’s no user authentication. We justify that this is just a proof of concept, and that the live app will of course have enforced SSL and best-practice auth. She squints at us, and then asks to see the front-facing, public app. We talk about PM, that we’re showing her what we have as soon as we build it, and that we’ll get to the public app next. She squints harder at us.

Let’s roll up our sleeves and get to cooking our second dish.

Note » You can see a live demo of the front-facing app on heroku. You can also find all of the app’s  source code on Github.

Scaffolding

The scaffolding for our front-facing app is largely the same as our admin panel; it will have a very similar directory structure and a Redux store. There are, however, some differences regarding i18n and routing. Remember that unlike our admin panel, our front-facing app needs to be be i18n-ized and localized. In fact, a lot of the scaffolding unique to our public app deals with just this i18n and l10n. Let’s take a look.

Configuration

/src/config/i18n.js

Configuring our supported locales in one place keeps things DRY and facilitates reuse. We’ll want locale names in their respective languages to use in a language switcher. Since we are supporting Arabic, we’ll also want to know a locale’s directionality when we switch to it.

Routing

/src/routes.js

Our locale determination will be based on the current URI. So /fr/movies will respond with a French version of the movies index, for example. To make sure we always have a locale explicitly selected, we redirect the / route to our default locale. In this case it’s English, so / will redirect to /en. React Router makes this quite easy with its Redirect component.

Notice the localizeRoutes(routes) call above. We provide the localizeRoutes function so we don’t have to include our locale parameter when we specify each of our routes. In actuality, however, we want all our routes prefixed by a segment corresponding to the current locale. So /movies/:id should actually be /:locale/movies/:id. We can then use this :locale parameter to determine our app’s current locale. Our localizeRoutes achieves this parameter prefixing, making use of our special localize option on our configured routes. Let’s see how this simple mapper works.

/src/services/i18n/util.js (excerpt)

We’re just prefixing every route passed to us with the /:locale/ route parameter and returning the prefixed routes. A dedicated l10n component will consume this parameter and use it to set our current locale. We’ll see this in action a bit later.

Note » We use a simple prefixPath function above to separate our concerns. Check out the function in the Github repo if you want.

Ok, let’s see how this all comes together. First let’s take a look at our App container.

/src/containers/App.js

Ok, most of what’s up there looks quite similar to our admin panel.  We do have a Switch, however, which may be new to you, and a custom Localizer.

The Switch component makes routing more akin to what we’re used to in server-side frameworks, meaning that it will render the first route it matches. If you remember, we had our /movies/:id route come before our /movies route in our config. Our Switch will make sure that /movies/:id route catches, and that we don’t fall through to the /movies route, when we hit /movies/1/.

Note » Read more about the Switch component in the React Router documentation.

 The Localizer Higher Order Component

The Localizer container is our own special sauce for setting the current locale based on the active URI. Notice that our Localizer sits inside the Router component. This is important, since Localizer will need the /:locale route parameter we defined in our routes to do its work.

/src/containers/Localizer.js

Ok, this may be a lot to take in. So let’s break it up.

Setting the Locale

When we construct our Localizer, we call setLocale to do some locale setup. By default and to be efficient, setLocale will check to see if our locale has actually changed before doing its work. Since there will be no change on app initialization, we force setLocale to do its setup via the second, boolean parameter. We then listen for URI changes and call setLocale whenever we get a newly requested URI.

Note » We’re using a simple utility function called getLocaleFromPath to extract the locale URI segment from the current locale. Check it out in the Github repo.

Alright, let’s take a look at what setLocale actually does.

/src/containers/Localizer.js (excerpt)

The function is responsible for a couple of things. It makes sure that our <html> element is synced correctly with our current locale. setLocale also lets our UI i18n library know what l10n the library should load, again based on the current locale. The state of UI translation file loading is tracked as our UI i18n library initializes in setUiLocale. We’ll dive into switchHtmlLocale and setUiLocale a bit later. For now, let’s continue working through our Localizer.

/src/containers/Localizer (excerpt)

Since our Localizer exists solely for setting the current locale, it doesn’t need to render anything. It’s a higher order React component that will wrap other components, and we achieve this wrapping by rendering out Localizer’s children. Now we export our module.

/src/containers/Localizer (excerpt)

At the bottom of our file, where we normally export a connected component or a plain old React component, we’re doing something a bit different. After we connect a bit of state that tracks our current locale and some locale actions, we wrap everything up in withRouter. We’ll get to withRouter in a minute.

First, let’s take a brief look at our locale state. It’s really simple stuff. We have two bits of locale state that we track.

/src/reducer/l10n.js (excerpt)

locale is just the current locale code e.g. "ar" for Arabic. The uiTranslationsLoaded boolean is used to track whether the UI translation files for the current locale have been loaded successfully. I’ll spare you the rest of the l10n reducer and its associated actions. They really just set the locale string and flip the uiTranslationsLoaded boolean. Nothing fancy at all happening there.

Note » You can check out the l10n reducer and actions in the Github repo.

Let’s get back to our Localizer.

/src/containers/Localizer (excerpt)

We wrap our normal React Redux connect call in withRouter. withRouter is a higher order component that provides routing information to its children.

If you remember, our Localizer’s constructor made use of some seemingly magical props.

/src/containers/Localizer (excerpt)

It’s the withRouter call that gives our Localizer access to the history prop, which we use to listen for URI changes in our app. It also gives us access to a handy location prop, which we can use to retrieve the current URI from.

Switching the Document’s Locale

When we set our current locale in the Localizer, we made a call to switchHtmlLocale.

/src/containers/Localizer.js (excerpt)

Let’s step into this function.

/src/services/i18n/util.js (excerpt)

We first make sure that our <html lang="ar" dir="rtl"> reflects the current locale and directionality. Any special stylesheets that are needed when our directionality is right-to-left are loaded, and removed when our directionality is left-to-right. We use this option in our calling code to load Bootstrap RTL styles.

Note » The loadAsset and removeAsset functions do pretty much what you think they do. You can check them out in the Github repo.

UI i18n

Our Localizer is responsible for initializing our UI i18n library. It does this via a call to setUilocale.

/src/containers/Localizer.js (excerpt)

For UI i18n, we’re using i18next and providing some simple wrappers around it. Let’s peek in.

Note » We’ll be covering the basics of i18next here. We have an article that goes into i18next in much more detail.

/src/services/i18n/index.js

Loading Translation Files

The first thing we do is pull in the UI translation file for our given locale. We’re assuming that we’re placing our translation files in /public/translations/. The JSON for these is pretty straightforward.

/public/translations/fr.json (excerpt)

i18next namespaces its translations under a translation key by default, so we adhere to that convention. Our translations are just key / value pairs. Done like dinner.

Once our translation file is loaded, we initialize i18next with the file’s JSON. From that point on we can use our  t() wrapper—which you may have noticed above—to return translation values by key from the currently loaded locale file.

In our views…

We can also interpolate values using i18next. Notice that we’re passing in a map with a director key in our second call to t above. Our translation copy can have a placeholder that corresponds to this key.

/public/translations/fr.json (excerpt)

The {{director}} placeholder will be replaced by "Michel Gondry" before t outputs the value of directed_by. i18next really simplifies our UI i18n and l10n.

Formatting Dates

i18next doesn’t support formatting dates itself. It does, however, provide a way for us to inject a date formatting interpolator when we initialize it. Notice that our interpolation.format function checks to see if the given value is a date, and delegates to formatDate if it is.

/src/services/i18n/index.js (excerpt)

We’ll jump into our date formatter in a minute. First let’s see how we want to use it.

/public/translations/ar.json (excerpt)

i18next allows to control the parameters we pass to our format interpolator. Given the above, if we were to call t('published_on', new Date('2018-02'))interpolation.format would receive "year:numeric;month:long" as its second parameter.

It’s up to us to handle this format. We could pull in a library like Moment.js for date formatting, but for a proof of concept like this Moment is overkill. Instead, we’ll use the Intl API  built into most modern browsers.

The Intl.DateTimeFormat constructor accepts a variety of formatting options which are well-documented. We can simply pass these along in our date formats when we write our translation files.

/public/translations/fr.json (excerpt)

All we have to do now is take these format strings and convert them to objects that Intl.DateTimeFormat understands. That’s exactly what our custom date interpolater, formatDate, does.

/src/services/i18n/util.js (excerpt)

We break up the format options along ; then we break each segment up further into its key and value, and we use those to build our options object. After that, we do our Intl.DateTimeFormat thing, gracefully handling any errors that could be caused by invalid user options.

Ok, that’s it for our scaffolding. Let’s get to our views.

UI: Our React Views

Navigation and The Language Switcher

We’ll start with our main app nav.

/src/components/AppNavbar.js

Most of the components we’re using here are Bootstrap presentation that Reactstrap provides for us. You’ll notice that we’re using our t() function instead of hard-coding any UI text. This ensures that the text is i18n-ized and pulled in from the current locale’s translation file.

We’re also pulling in a custom LocalizedLink along with React Router’s usual Link component. Take a gander with me.

/src/containers/LocalizedLink.js

Remember that prefixPath function that we used to prefix our routes with the locale param? Well now we’re using it to prefix the given URI, to, with the actual current locale. We’re pulling in the current locale from our single source of truth o the subject: our handy dandy Redux state.

The Language Switcher

Back to AppNavbar. This piece of JSX is of particular interest.

/src/container/AppNav.js (excerpt)

Since our supported locales are stored in one central config, we pull them in with import { locales } from '../config/i18n near the top of our file. All we have to do then is spin over them and output links to /ar, /en, and /fr. Our routing and Localizer take care of the rest. Disco.

Now we can build out our Home component.

Home Sweet Home

/src/components/Home.js

Like good React developers, we componentize our Home sections and pull them in. Once all is built out, we get this glorious rendering.

Banging prototype, dude 👾. Instead of boring you with building out all of the Home containers, we’ll deep-dive into one of them so that we can get an idea of a whole vertical.

Note » You can see the rest of the Home containers, along with the rest of the app code, in the Github repo.

Featured Movies

We’ll focus on the FeaturedMovies container. Let’s take a look at our mock API first; we represent it with JSON files tucked away in /public/api/.

/public/api/en/movies.json (excerpt)

We’d expect our app’s API to return something like this if we made a GET /en/movies request. To round out our mock API, we have one of these JSON files for each of our supported locales. Now to our movie reducer.

Our movie state is nice and terse.

/src/reducers/movies.js (excerpt)

We make sure to keep a featured subset of our movie collection each time we add new movies. Now, of course, we need something to act on this state.

/src/actions/index.js (excerpt)

Pretty standard stuff here. Notice, however, that we’re pulling in our current state by using Redux Thunk’s getState parameter. This allows to figure out the current locale without requiring it from our calling code, so we can pull in the right movie localization. Ok, let’s use this funky fluxy flow in our views.

/src/containers/FeaturedMovies.js

A CardDeck is a presentational Bootstrap component that helps lay out a set of Cards. Luckily, our FeatureMovie component is wrapped in just such a Card.

/src/components/FeaturedMovie.js

The synposis helper function truncates a movie synopsis that’s too long for our index view, and returns an array of paragraphs. Other than that, we’re just using our good old LocalizedLink to render links to the individual movie in the index, and t()ing up all our text, with interpolation where needed.

The rest of the views are, for our purposes, largely more of the same. So, I’ll let you peer into the Github repo yourself to check them out.

When all is said and done, we get something that works a little something like this.

We quickly run to our client to show her our finished front-facing proof of concept, with routing, language switching, i18n-ized UI, and localized content. We think we glean the beginnings of a smile on her face.

Writing code to localize your app is one task, but working with translations is a completely different story. Many translations for multiple languages may quickly overwhelm you which will lead to the user’s confusion. Fortunately, PhraseApp can make your life as a developer easier! Feel free to learn more about PhraseApp, referring to the Getting Started guide.

I hope this gets you started on the right foot when building your React SPAs with i18n and l10n, and I hope it was as fun for you to read as it was for me to write. Be sure to check out the code and the live demo of the admin and public apps. Til next time 😊 👍🏽

 

Building an Awesome Magazine App with i18n in React!
5 (100%) 4 votes
Related Posts
Comments