Building a News App Using React.js with i18n API in Strapi

Strapi - Sep 26 '22 - - Dev Community

In this tutorial, you will build a News app using Strapi and React.js. You will use the Strapi i18n plugin to translate the backend content from one language to another. Strapi will be used to build the backend and provide the API endpoints, and React.js we will use to build our frontend part.

What is Strapi?

Strapi is an open-source headless content management system based on Node.js. Strapi provides us with a nice UI to build our APIs. We can build APIs in the form of collections. A collection is a web resource that has endpoints — GET, DELETE, POST, and PUT. These endpoints perform a separate action to the web resource.

Strapi provides the web resource endpoints without us writing any server code. The web resources content can be added to Strapi via its admin panel, also using the API.

For example, if we create a movies collection from the admin panel. Strapi will provide us with the following endpoints:

  • /movies GET: This gets all the movies in the backend.
  • /movies/:id GET: This gets a movie from the backend.
  • /movies/:id PUT: This edits a movie.
  • /movies POST: This adds a new movie to the backend.
  • /movies/:id DELETE: This removes a movie from the backend.

So with just a collection, we get the above API endpoints. Strapi is self-hosted. It has an in-built server that serves the collections we create. Also, a Strapi project can be hosted in the cloud.

Strapi supports relational databases such as MySQL, SQLite, PostgreSQL, etc. With Strapi, we only have to build the frontend (mobile, web, or desktop) and use HTTP to fetch the data from the Strapi backend.

The i18n Strapi Plugin

Content internationalization is very popular these days. You can build an app with content displayed in English, but there are chances that a Chinese or Spanish user. You wouldn't want the language to be a barrier that stops users from accessing your app.

With content internationalization, you can translate the content of your application from one language to another. You can translate from English to Chinese, from English to Spanish, French to German, etc.

The Strapi i18n plugin allows users to create different localized/language versions of their API content. It helps developers build different language versions of their projects by fetching the content based on the language/locale of the user.

Prerequisites

Here are some things you should know, have, or do before continuing this tutorial.

Knowledge Requirements

To follow along, you need to have some basic knowledge of:

  • Javascript: A scripting language for web pages.
  • React: A Javascript library for building user interfaces.
  • Node.js: A Javascript runtime environment for the backend.
  • Shell (Bash): Command language interpreter.
  • Git: Distributed version control system.

Software Requirements

In this tutorial, we will need to install some tools if not already installed:

  • Operating System (any of the following are supported):

    • Ubuntu >= 18.04 (LTS-Only). I used Ubuntu 20.04 LTS for this tutorial.
    • Debian >= 9.x
    • CentOS/RHEL >= 8
    • macOS Mojave or newer (ARM not supported)
    • Windows 10 or 11
    • Docker - docker repo
  • Node: Both Strapi and Reactjs are Node-based tools and frameworks respectively. Supported versions are v14 or v16. Odd-number versions of Node are not supported (e.g. v13 or v15). To download the Nodejs binary, get it from the Download | Nodejs page. I used Node v16.14.2.

  • npm: This is a Node package manager. This comes bundled with the Nodejs binary. I used npm v8.5.0.

  • yarn (Optional): An alternative to npm. Check Installation | Yarn for installation instructions.

  • Postman: An API platform to test our app’s API endpoints. Download the desktop version or sign up for an account to use the web version.

  • Code Editor: Any editor of your choice. I used VSCode.

  • Web Browser: Any Chromium-based browser. I used Google Chrome.

  • Git: For version control. Download Git from the Git - Downloads page.

  • Git Bash: (for Windows users). The tutorial uses Unix shell commands. Git Bash can run UNIX shell commands in Windows. Git Bash ships with your Git download.

Hardware Requirements

  • At least 1 CPU core (Highly recommended, at least 2).
  • At least 2GB of RAM (Moderately recommended).
  • Minimum required storage space recommended by your OS or 32 GB of free space.

Set up Strapi Backend

The first thing we need to do is set up the Strapi backend.

NOTE: If you prefer testing the ready-made app, you can clone the Github repo.

1. Scaffold a Strapi Project

In your terminal, create a folder to contain the entire source code for this tutorial. Name it newsapp, move into it, and initialize git for source control.

    $ mkdir newsapp
    $ cd newsapp
    /newsapp $ git init
Enter fullscreen mode Exit fullscreen mode

Create a Strapi project named newsapp-api.

    /newsapp $ npx create-strapi-app newsapp-api --quickstart
    # or
    /newsapp $ yarn create strapi-app newsapp-api --quickstart
Enter fullscreen mode Exit fullscreen mode

This will create a Strapi project in the newsapp-api folder. The --quickstart flag sets up your Strapi app with an SQLite database. After executing the command, enter y if prompted to proceed with the installation. Wait for Strapi and all its dependencies to be installed in the newsapp-api folder.

After installation, your app should start automatically. Stop the server by pressing Ctrl plus C on your keyboard. Your project folder newsapp after installing Strapi should have a structure similar to this:

    /newsapp $ tree -L 2 -a
    .
    ├── newsapp-api
    │   ├── build
    │   ├── .cache
    │   ├── config
    │   ├── data
    │   ├── database
    │   ├── .editorconfig
    │   ├── .env
    │   ├── .env.example
    │   ├── .eslintignore
    │   ├── .eslintrc
    │   ├── favicon.ico
    │   ├── .gitignore
    │   ├── node_modules
    │   ├── package.json
    │   ├── public
    │   ├── README.md
    │   ├── src
    │   ├── .strapi-updater.json
    │   ├── .tmp
    │   └── yarn.lock
    ├── .git
    ├── LICENSE
    └── README.md
Enter fullscreen mode Exit fullscreen mode

Move inside the Strapi install folder.

    /newsapp $ cd newsapp-api
Enter fullscreen mode Exit fullscreen mode

2. Install Strapi i18n plugin

This Strapi i18n can be installed:

  1. From the Terminal
  2. From the Marketplace

According to the installation docs,, the Internationalization plugin is installed by default on all Strapi applications running on version 3.6.0 or higher. For lower versions, a migration is needed (see Update Strapi version), as well as a manual installation of the plugin.

This tutorial uses Strapi version 4.3.2 which ships with the i18n plugin by default.

3. Register an Admin User

Start the Strapi server

    /newsapp/newsapp-api $ npm run develop
    # OR
    /newsapp/newsapp-api $ yarn develop
Enter fullscreen mode Exit fullscreen mode

npm run develop starts your Strapi project development server in watch mode. In watch mode changes in your Strapi project will trigger a server restart.

Strapi will start its server at http://localhost:1337, and load its admin panel at http://localhost:1337/admin.

Strapi Admin Registration

NOTE:
If you visit http://localhost:1337/admin and see “Strapi: Warning: an error occurred while requesting the API”.

Stop the server, Ctrl + C. Change the host IP address in ./newsapp/newsapp-api/config/server.js from 0.0.0.0 to 127.0.0.1 . If you are stuck, check out this gist.

Fill in your details, and click on the LET’S START button. This makes Strapi register your details and shows the admin dashboard.

Strapi Admin Dashboard

Now, we will begin creating our collections.

4. Build the Strapi Collections

We are building a news website, a single news item will have this model:

    newspost {
        title
        body
        writtenBy
        imageUrl
    }
Enter fullscreen mode Exit fullscreen mode
  • The title is the title of the news.
  • The body is the content of the news.
  • The writtenBy is the author of the news.
  • The imageUrl is the header image of the news.

Strapi will provide us with these endpoints:

  • /newsposts GET: This endpoint will retrieve all the news content.
  • /newsposts/:id GET: This endpoint will retrieve a single news post based on the :id.
  • /newsposts POST: This endpoint will create a new news item.
  • /newsposts/:id DELETE: This endpoint will delete a particular news item.
  • /newsposts/:id PUT: This endpoint will edit a particular news item based on the :id

On the admin page, click on the Create your first Content type button. This will take you to the Content-Type Builder plugin page. Click on + Create a collection type to display the Create a collection type modal. Enter newspost for the Display name and leave the API ID (Singular) and API ID (Plural) as is.

Create a collection type - Basic Settings

We need to enable localization for this collection, so move to the top-right of the modal and click on the Advanced Settings tab. Scroll down and tick the Enable localization for this Content-Type checkbox. This will allow you to have content in different locales.

Create a collection type - Enable localization

Click Continue and a modal appears with field options for your collection type. Select Text.

Select Text field for Newspost collection

In the Add new Text field modal, enter title for Name, leave Type as Short text and click on + Add another field.

Add new Text field named title

Add Text fields named writtenBy and imageUrl. The type should be Short text. Next, add a Rich text field named body and click Finish. This loads a view of the Newspost collection type with the four fields you just created. Click Save and when the server restarts Newspost will be listed under COLLECTION TYPES in the left sidebar.

Newspost collection type

When the server restarted, the database for your Strapi app was updated with the newspost collection type. New files in ./newsapp/newsapp-api/src/api are created to provide the business logic for the newspost API.

    /newsapp/newsapp-api $ cd src
    /newsapp/newsapp-api/src $ tree
    .
    ├── admin
    │   ├── app.example.js
    │   └── webpack.config.example.js
    ├── api
    │   └── newspost
    │       ├── content-types
    │       │   └── newspost
    │       │       └── schema.json
    │       ├── controllers
    │       │   └── newspost.js
    │       ├── routes
    │       │   └── newspost.js
    │       └── services
    │           └── newspost.js
    ├── extensions
    └── index.js
Enter fullscreen mode Exit fullscreen mode

5. Adding Languages

With localization enabled, the content will be seeded in the database for each language or locale. English is by default the selected language.

Click Settings on the sidebar. Under GLOBAL SETTINGS, click on Internationalization.

Settings: Internationalization Plugin Menu

English (en) is the default locale/language. Let’s add the French locale. Click on the + Add a locale button on the top-right. A modal shows up. Click on the Locales dropdown. All locales supported by Strapi will cascade down. Select French (France) (fr-FR). Click on ✔️ Save.

Add French (France) (fr-FR) locale

See that the French (France) (fr-FR) locale is added to your list of locales for your Newsposts collection.

List of locales

Our app now supports both English and French languages.

6. Seeding our database

Now, we will add content to our database. Click on Content Manager on the sidebar. Under COLLECTION TYPES select newspost.

Notice that any newspost entry you make will be English (en) locale by default. Since you have added the French (France) (fr-FR) locale, clicking on the dropdown box will display the French (France) (fr-FR) locale listed below the English (en) locale.

Locale options in Content Manager for newspost

Locales added in the Internationalization section will appear in the dropdown. You can select them to add content for that language. So, if you select the French (France) (fr-FR), the content locale will then be for French (France) (fr-FR).

But for now, let’s add news content for the English (en) locale. Click on + Create new entry in the top right corner.

Enter the following data for your entry:

    title -> Nepal's Minister sworn into office
    imageUrl -> https://thehimalayantimes.com/uploads/imported_images/wp-content/uploads/2020/10/Ministers-Sworn-in.jpg
    writtenBy -> John Doe
    body -> Type anything here
Enter fullscreen mode Exit fullscreen mode

Click on Save, then Publish to publish your article. You can add many more news items as you deem fit.

Let’s add two more:

    title -> Angular 12 released.
    imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1280px-Angular_full_color_logo.svg.png
    writtenBy -> M. Hry
    body -> Type anything here
Enter fullscreen mode Exit fullscreen mode

Click on Save then Publish. Add this:

    title -> React v17 released
    imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1920px-React-icon.svg.png
    writtenBy -> Dan Tierry
    body -> Type anything here
Enter fullscreen mode Exit fullscreen mode

Click on Save, then Publish. We are done adding news posts for the English (en) locale. The list of your news posts should be similar to this:

List of News posts

7. Add News Content for the French Locale

Click on the English (en) dropdown box and select French (France) (fr-FR).
Select French locale

The page will reload:
News posts for French locale

There is no data for French (France) (fr-FR) yet, but we can add them. Here, we will add the French translation of our English version.

For the first data on the French version, we will add this:

    title -> Le ministre du Népal a prêté serment
    imageUrl -> https://thehimalayantimes.com/uploads/imported_images/wp-content/uploads/2020/10/Ministers-Sworn-in.jpg
    writtenBy -> John Doe
    body -> Type_any_thing_here.
Enter fullscreen mode Exit fullscreen mode

Click on + Create new entry and add the above content. Click on Save and then on Publish. Now, do this for the second and third posts:

    title -> Angular 12 est sorti.
    imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1280px-Angular_full_color_logo.svg.png
    writtenBy -> M. Hry
    body -> Type_any_thing_here.
Enter fullscreen mode Exit fullscreen mode
    title -> Sortie de React v17
    imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1920px-React-icon.svg.png
    writtenBy -> Dan Tierry
    body -> Type_any_thing_here.
Enter fullscreen mode Exit fullscreen mode

News posts for the French (France) fr-FR locale are ready.

List of French news posts

8. Open Access for the Newsposts

Now, only authenticated users can access our endpoints. Let’s make the endpoints to be accessible to the public.

To do that:

  • Click on Settings on the sidebar.
  • In the USERS & PERMISSIONS PLUGIN section, click on Roles.
  • On the Roles page, click on Public. Select Public User Permission Roles
  • Scroll down to the Permissions section.
  • Select Newspost and check the Select all checkbox. This makes all the endpoints accessible to the public and performs any Create, Read, Update, Delete (CRUD) actions on them. Open access for News posts
  • Now, scroll up to the top, and click on Save.

Testing Our Strapi Backend Endpoints

Let's test the endpoints. Open Postman. If you don’t have it already, you can download it from here.

1. Get All Newsposts

Create a new collection so we can organize our requests. Add a request to the collection. Set the Request type to GET in the input box type http://localhost:1337/api/newsposts and click on Send.

Newsposts API endpoint view in Postman

See that only the Newsposts for the English (en) locale are returned because the default locale is set to English (en). To get the French translation, we add the query, ?locale=fr-FR to the URL and send. The fr-FR is the standard language code for the French language.

Type http://localhost:1337/api/newsposts?locale=fr-FR in the input box and click Send. The result will give the French locale news posts as seen below.

French News posts API Endpoint

Notice that both newsposts in both English and French have unique ids. So if we fetch a News with its id, we don’t have to add the locale= param. It will fetch the content of the news with no effect on the locale.

2. Adding a News Item

To add a news item to a particular locale, we will have to pass the language code of the locale in the body payload. Change the request method to POST. Select Body, choose raw then add the following JSON data for the new news post.

    {
        "data": {
            "title": "Nouvelles fonctionnalités de Vue js et changements de rupture - Présentation de Vue 3",
            "body": "Avec la sortie d'une nouvelle version majeure du noyau, toutes les autres parties du cadre devaient progresser ensemble. Nous devions également fournir une voie de migration pour les utilisateurs de Vue 2. Il s'agissait d'une entreprise massive pour une équipe dirigée par la communauté comme Vue. Lorsque le noyau de Vue 3 était prêt, la plupart des autres parties du framework étaient soit en version bêta, soit en attente de mise à jour. Nous avons décidé d'aller de l'avant et de publier le noyau afin que les premiers utilisateurs, les auteurs de bibliothèques et les frameworks de niveau supérieur puissent commencer à construire avec lui pendant que nous travaillions sur le reste du framework.\n\n Dans le même temps, nous avons conservé Vue 2 comme valeur par défaut pour la documentation et les installations npm. En effet, nous savions que pour de nombreux utilisateurs, Vue 2 offrait toujours une expérience plus cohérente et plus complète jusqu'à ce que d'autres parties de Vue 3 soient affinées.",
            "writtenBy": "CJ",
            "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1920px-Vue.js_Logo_2.svg.png",
            "locale": "fr-FR"
        }
    }
Enter fullscreen mode Exit fullscreen mode

Click Send and wait for a response from the server. If successful, you should get the following response:

    {
        "data": {
            "id": 8,
            "attributes": {
                "title": "Nouvelles fonctionnalités de Vue js et changements de rupture - Présentation de Vue 3",
                "body": "Avec la sortie d'une nouvelle version majeure du noyau, toutes les autres parties du cadre devaient progresser ensemble. Nous devions également fournir une voie de migration pour les utilisateurs de Vue 2. Il s'agissait d'une entreprise massive pour une équipe dirigée par la communauté comme Vue. Lorsque le noyau de Vue 3 était prêt, la plupart des autres parties du framework étaient soit en version bêta, soit en attente de mise à jour. Nous avons décidé d'aller de l'avant et de publier le noyau afin que les premiers utilisateurs, les auteurs de bibliothèques et les frameworks de niveau supérieur puissent commencer à construire avec lui pendant que nous travaillions sur le reste du framework.\n\n Dans le même temps, nous avons conservé Vue 2 comme valeur par défaut pour la documentation et les installations npm. En effet, nous savions que pour de nombreux utilisateurs, Vue 2 offrait toujours une expérience plus cohérente et plus complète jusqu'à ce que d'autres parties de Vue 3 soient affinées.",
                "writtenBy": "CJ",
                "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1920px-Vue.js_Logo_2.svg.png",
                "createdAt": "2022-08-30T19:39:30.199Z",
                "updatedAt": "2022-08-30T19:39:30.199Z",
                "publishedAt": "2022-08-30T19:39:30.194Z",
                "locale": "fr-FR"
            }
        },
        "meta": {}
    }
Enter fullscreen mode Exit fullscreen mode

POST Request Payload

The final confirmation for the successful news post addition is it is visible in the list of French locale news posts. Navigate to the French (France) fr-FR locale list of news posts in the Content Manager and you will see it. See we set fr-FR to the locale prop so Strapi knows this goes into the French locale.

Newly added news post in French list

We are done with our Strapi backend; let’s build the frontend to see how the two meet.

Set up React App Frontend

Let's move to the next stage, which is setting up the React app frontend.

1. Scaffold React App

Now, you will build the frontend of your app.
Change the directory to the root of your project folder newsapp:

    /newsapp/newsapp-api $ cd ..
Enter fullscreen mode Exit fullscreen mode

First, make sure you have the create-react-app tool installed on your machine. If not, you can install it globally by running this command:

    /newsapp $ npm i create-react-app -g
Enter fullscreen mode Exit fullscreen mode

Test the installation by running the command:

    /newsapp $ create-react-app --version
Enter fullscreen mode Exit fullscreen mode

From here, scaffold the React project. Create a newsapp-strapi React project by running the following command:

    /newsapp $ create-react-app newsapp-strapi
Enter fullscreen mode Exit fullscreen mode

Move into the folder:

    /newsapp $ cd newsapp-strapi
Enter fullscreen mode Exit fullscreen mode

Install the axios module, which you will use for querying Strapi endpoints:

    /newsapp/newsapp-strapi $ npm i axios
Enter fullscreen mode Exit fullscreen mode

Next, you will need routes in our app. Install the react-router-dom:

    /newsapp/newsapp-strapi $ npm i react-router-dom
Enter fullscreen mode Exit fullscreen mode

Start the server:

    /newsapp/newsapp-strapi $ npm run start
Enter fullscreen mode Exit fullscreen mode

Go to your browser and navigate to http://localhost:3000 to see the React frontend for your app.

2. Build the Components

LET'S BEGIN WITH THE SETUP.
Your news app will have two routes:

  • /news: This route will render all the news in our app.
  • /newsview/:id: This route will render a particular news item. The :id will be the id attribute of the news item.

Your final app will look like this:

News app Home page

Add News post modal

This displays a single news post.

Full view of Single News post

Let’s break our app into components.

  • Header: This will hold the header section of your app.
  • NewsList: This component will be a page component. It will be rendered when you navigate to the /news route. It will display the list of news. It is a smart component.
  • NewsCard: This component will display an overview of a news item. It will be rendered by the NewsList component.
  • NewsView: This component is a page component, it displays the full details of a news item. It is the news page, where users will read a piece of particular news.
  • AddNewsDialog: This is a dialog component, it is where news is added to your app. Header, NewsList and NewsCard Component

AddNewsDialog Component

NewsView Component

The page components will be in the pages folder while the presentational components will be in the components folder.

Create two folders pages and components inside the src folder.

    /newsapp/newsapp-strapi $ mkdir src/pages && mkdir src/components
Enter fullscreen mode Exit fullscreen mode

Create Header, AddNewsDialog, and NewsCard components inside the components folder. Move inside the components folder:

    /newsapp/newsapp-strapi $ cd src/components
Enter fullscreen mode Exit fullscreen mode

Create a folder named AddNewsDialog and create an index.js file inside it.

    /newsapp/newsapp-strapi/src/components $ mkdir AddNewsDialog && touch AddNewsDialog/index.js
Enter fullscreen mode Exit fullscreen mode

Create a folder named Header. Add index.js and Header.css to the Header folder.

    /newsapp/newsapp-strapi/src/components $ mkdir Header && touch Header/index.js && touch Header/Header.css
Enter fullscreen mode Exit fullscreen mode

Do the same for the NewsCard component.

    /newsapp/newsapp-strapi/src/components $ mkdir NewsCard && touch NewsCard/index.js && touch NewsCard/NewsCard.css
Enter fullscreen mode Exit fullscreen mode

Create the page components. Switch to the pages folder.

    /newsapp/newsapp-strapi/src/components $ cd /newsapp/newsapp-strapi/src/pages
Enter fullscreen mode Exit fullscreen mode

Add the NewsList component.

    /newsapp/newsapp-strapi/src/pages $ mkdir NewsList && touch NewsList/index.js && touch NewsList/NewsList.css
Enter fullscreen mode Exit fullscreen mode

Add the NewsView component.

    /newsapp/newsapp-strapi/src/pages $ mkdir NewsView && touch NewsView/index.js && touch NewsView/NewsView.css
Enter fullscreen mode Exit fullscreen mode

Open App.js in the src folder. Clear the content and paste the below code:

    /* /newsapp/newsapp-strapi/src/App.js */

    import "./App.css";
    import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
    import Header from "./components/Header";
    import NewsList from "./pages/NewsList";
    import NewsView from "./pages/NewsView";

    function App() {
      return (
        <>
          <Header />
          <div className="container">
            <main className="main">
              <BrowserRouter>
                <Switch>
                  <Route path="/news">
                    <NewsList />
                  </Route>
                  <Route path="/newsview/:id">
                    <NewsView />
                  </Route>
                  <Route exact path="/">
                    <Redirect to="/news" />
                  </Route>
                  <Route path="*">
                    <NewsList />
                  </Route>{" "}
                </Switch>
              </BrowserRouter>
            </main>
          </div>
        </>
      );
    }
    export default App;
Enter fullscreen mode Exit fullscreen mode

We set up two routes: /news and newsview/:id. The first route will render the NewsList component while the last route will render the NewsView component.

We used React Router to set up routing here. The BrowserRouter component initializes the routing system, the Switch component wraps the dynamic routes, and the Route configures the specific routes and wraps the component the route that will render. Anything else outside the BrowserRouter component will render on every page. See that we render the Header component outside it, so the header appears on all our pages.

Next, clear the code in App.css and save the file.

HEADER COMPONENT

Add the below code to index.js in the Header component folder.

    // /newsapp/newsapp-strapi/src/components/Header/index.js

    import "./Header.css";

    export default function Header() {
      return (
        <section className="header">
          <div className="headerName">News24</div>
        </section>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Just a simple UI that displays the text “News24”. Add some styling to your header, using the Header.css file:

    /* /newsapp/newsapp-strapi/src/components/Header/Header.css */

    .header {
      height: 54px;
      background-color: black;
      color: white;
      display: flex;
      align-items: center;
      padding: 10px;
      font-family: sans-serif;
      /*width: 100%;*/
      padding-left: 27%;
    }
    .headerName {
      font-size: 1.8em;
    }
Enter fullscreen mode Exit fullscreen mode

NEWSLIST COMPONENT

Insert the following code in the index.js file for the NewsList component:

    // /newsapp/newsapp-strapi/src/pages/NewsList/index.js

    import "./NewsList.css";
    import NewsCard from "./../../components/NewsCard";
    import { useEffect, useState } from "react";
    import axios from "axios";
    import AddNewsDialog from "../../components/AddNewsDialog";

    export default function NewsList() {
        const [newsList, setNewsList] = useState([]);
        const [locale, setLocale] = useState("en");
        const [showModal, setShowModal] = useState(false);

        useEffect(() => {
            async function fetchNews() {
                const data = await axios.get(
                    "http://localhost:1337/api/newsposts?_locale=" + locale
                );
                setNewsList([...data?.data]);
            }
            fetchNews();
        }, [locale]);

        function setLang() {
            setLocale(window.locales.value);
        }

        function showAddNewsDialog() {
            setShowModal(!showModal);
        }

        return (
            <div className="newslist">
                <div className="newslistbreadcrumb">
                    <div className="newslisttitle">
                        <h3>World News</h3>
                    </div>
                    <div style={{ display: "flex", alignItems: "center" }}>
                        <div style={{ marginRight: "4px" }}>
                            <button onClick={showAddNewsDialog}>Add News</button>
                        </div>
                        <div>
                            <select name="locales" id="locales" onChange={setLang}>
                                <option value="en">English</option>
                                <option value="fr-FR">French</option>
                            </select>
                        </div>
                    </div>
                </div>
                <div>
                    {newsList?.map((newsItem, i) => (
                        <NewsCard newsItem={newsItem} key={i} />
                    ))}
                </div>
                {showModal ? <AddNewsDialog closeModal={showAddNewsDialog} /> : null}
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

Here we set up three states. The first holds the list of news. The second holds the current locale/language. It is by default set to English en. The last holds the modal visibility state.

The useEffect() function loads the news whenever the component mounts from the http://localhost:1337/api/newsposts?_locale=" endpoint. The current locale is added to it based on the selected locale. This enables of internationalization of the news app. You read news in both English and French.

The result sets the newsList state. This causes the component to render and the UI displays the news in a long list. We added a locale dependency to the useEffect so the callback runs when the locale state changes.

The functions setLang and showAddNewsDialog sets the selected locale to the locale state and toggles the showModal state respectively.

Let’s analyze the UI code.

See that we added an Add News button that when clicked the AddNewsDialog modal shows up. We also added a language select element, this is where we can select the language the news will be rendered in. We have "English" and "French", this is so because we have internationalization on our backend for "English" and "French". When any of the options is clicked, the language version of the app is loaded.

Add the styling. Open NewsList.css and paste the below code:

    /* /newsapp/newsapp-strapi/src/pages/NewsList/NewsList.css */

    .newslistbreadcrumb {
      display: flex;
      align-items: center;
      justify-content: space-between;
      border-bottom: 1px solid darkgray;
    }
    .newslisttitle {
      margin-left: 12px;
    }
    .newslisttitle h3 {
      color: rgb(107 107 107);
      font-weight: 500;
    }
Enter fullscreen mode Exit fullscreen mode

NEWWSVIEW COMPONENT
This page component renders the news selected from the NewsList component. Insert the following code to the index.js file for the NewsView page:

    /* /newsapp/newsapp-strapi/src/pages/NewsView/index.js */

    import "./NewsView.css";
    import { useParams } from "react-router-dom";
    import axios from "axios";
    import { useEffect, useState } from "react";

    export default function NewsView() {
        let { id } = useParams();
        const [news, setNews] = useState();

        useEffect(() => {
            async function getNews() {
                const data = await axios.get("http://localhost:1337/api/newsposts/" + id);
                setNews(data?.data);
            }
            getNews();
        }, [id]);

        async function deleteNews() {
            if (window.confirm("Do you want to delete this news?")) {
                await axios.delete("http://localhost:1337/api/newsposts/" + id);
                window.history.pushState(null, "", "/news");
                window.location.reload();
            }
        }
        return (
            <div className="newsview">
                <div
                    className="newsviewimg"
                    style={{ backgroundImage: `url(${news?.imageUrl})` }}
                ></div>
                <div>
                    <div className="newsviewtitlesection">
                        <div className="newsviewtitle">
                            <h1>{news?.title}</h1>
                        </div>
                        <div className="newsviewdetails">
                            <span style={{ flex: "1", color: "rgb(99 98 98)" }}>
                                Written By: <span>{news?.writtenBy}</span>
                            </span>
                            <span style={{ flex: "1", color: "rgb(99 98 98)" }}>
                                Date: <span>{news?.created_at}</span>
                            </span>
                            <span>
                                <button className="btn-danger" onClick={deleteNews}>
                                    Delete
                                </button>
                            </span>
                        </div>
                    </div>
                    <div className="newsviewbody">{news?.body}</div>
                </div>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

We start by using the useParams hook to get the :id param and the value is stored in the id variable. We set a state to hold the current news being viewed. The useEffect loads the news details from the endpoint http://localhost:1337/api/newsposts/ + id and set the result to the news state.

The deleteNews function deletes this news by performing a DELETE HTTP request to the endpoint http://localhost:1337/api/newsposts/" + id. Then, navigate back to the /news page. This component’s UI displays the news in its entirety.

Add styling. Open NewsView.css and paste the below code:

    /* /newsapp/newsapp-strapi/src/pages/NewsView/NewsView.css */

    .newsview {
      margin-top: 7px;
    }
    .newsviewimg {
      background-color: darkgray;
      background-repeat: no-repeat;
      background-size: cover;
      background-position: center;
      height: 200px;
    }
    .newsviewdetails {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .newsviewtitlesection {
      margin-bottom: 20px;
    }
    .newsviewtitle h1 {
      margin-bottom: 6px;
    }
    .newsviewbody {
      font-size: large;
    }
    .newsviewbody::first-letter {
      font-weight: 700;
      font-size: 4em;
      line-height: 0.83;
      float: left;
      margin-right: 7px;
      margin-bottom: 4px;
    }
    .newsviewbody {
      clear: left;
      font-size: 21px;
      line-height: 1.58;
      letter-spacing: -0.003em;
    }
Enter fullscreen mode Exit fullscreen mode

We used the ::first-select pseudoselector here .newsviewbody::first-letter to make the first letter in the news body appear larger 😁. That's a great UI appeal just like what Medium.com does.

NEWSCARD COMPONENT

This component is rendered by the NewsList to display a list of news posts in the app. Insert the following code to the index.js file for the NewsCard component:

    /* /newsapp/newsapp-strapi/src/components/NewsCard/index.js */

    import { Link } from "react-router-dom";
    import "./NewsCard.css";

    export default function NewsCard({ newsItem }) {
      const { title, body, imageUrl, id } = newsItem;
      const synopsis = body.slice(0, 150);
      return (
        <Link to={"/newsview/" + id}>
          <div className="newscard">
            <div
              className="newscardimg"
              style={{ backgroundImage: `url(${imageUrl})` }}
            ></div>
            <div>
              <div className="newscardtitle">
                <h1>{title}</h1>
              </div>
              <div>
                <span>{synopsis}</span>
              </div>
              <div></div>
            </div>
          </div>
        </Link>
      );
    }
Enter fullscreen mode Exit fullscreen mode

The newsItem is destructured from the component props, and then the news properties body, imageUrl, title, and id are destructured from it. Then, we generated a synopsis of the news from the body, this is just a snippet of the whole news. We did this by slicing off 150 characters from the whole news in the body and then displaying it.

See that we set a link to the full news by wrapping the div#newscard in the Link component with a link to newsview/ + id. This will load the NewsView component with the id param set to the param in the id variable.

Add styling to the NewsCard. Open the NewsCard.css and paste the below code:

    /* /newsapp/newsapp-strapi/src/components/NewsCard/NewsCard.css */

    .newscard {
      /*background-color: white;*/
      padding: 8px;
      /*box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
      transition: 0.3s;*/
      border-radius: 4px;
      margin: 8px;
      cursor: pointer;
      display: flex;
    }
    .newscardimg {
      width: 146px;
      height: 146px;
      background-color: darkgray;
      background-repeat: no-repeat;
      background-size: cover;
      background-position: center;
      margin-right: 9px;
      flex: 1 100%;
    }
    .newscardtitle {
      flex: 1 100%;
    }
    .newscardtitle h1 {
      margin-top: 0;
      margin-bottom: 1px;
    }
Enter fullscreen mode Exit fullscreen mode

AddNewsDialog COMPONENT

Add the below code to index.js for the AddNewsDialog component:

    /* /newsapp/newsapp-strapi/src/components/AddNewsDialog/index.js */

    import { useState } from "react";
    import axios from "axios";

    export default function AddNewsDialog({ closeModal }) {
        const [disable, setDisable] = useState(false);
        async function saveNews() {
            const title = window.newsTitle.value;
            const imageUrl = window.newsImageUrl.value;
            const writtenBy = window.newsWrittenBy.value;
            const body = window.newsBody.value;
            const lang = window.addNewsLocales.value;

            setDisable(true);
            await axios.post("http://localhost:1337/api/newsposts", {
                title,
                imageUrl,
                writtenBy,
                body,
                locale: lang,
            });
            window.location.reload();
            setDisable(false);
        }

        return (
            <div className="modal">
                <div className="modal-backdrop" onClick={closeModal}></div>
                <div className="modal-content">
                    <div className="modal-header">
                        <h3>Add News</h3>
                        <span
                            style={{ padding: "10px", cursor: "pointer" }}
                            onClick={closeModal}
                        >
                            X
                        </span>
                    </div>
                    <div className="modal-body content">
                        <div style={{ display: "flex", flexWrap: "wrap" }}>
                            <div className="inputField">
                                <div className="label">
                                    <label>Title</label>
                                </div>
                                <div>
                                    <input id="newsTitle" type="text" />
                                </div>
                            </div>
                            <div className="inputField">
                                <div className="label">
                                    <label>Lang</label>
                                </div>
                                <div>
                                    <select name="addNewsLocales" id="addNewsLocales">
                                        <option value="en">English</option>
                                        <option value="fr-FR">French</option>
                                    </select>
                                </div>
                            </div>
                            <div className="inputField">
                                <div className="label">
                                    <label>ImageUrl</label>
                                </div>
                                <div>
                                    <input id="newsImageUrl" type="text" />
                                </div>
                            </div>
                            <div className="inputField">
                                <div className="label">
                                    <label>Written By</label>
                                </div>
                                <div>
                                    <input id="newsWrittenBy" type="text" />
                                </div>
                            </div>
                            <div className="inputField" style={{ flex: "2 1 100%" }}>
                                <div className="label">
                                    <label>Body</label>
                                </div>
                                <div>
                                    <textarea
                                        id="newsBody"
                                        style={{ width: "100%", height: "200px" }}
                                    ></textarea>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div className="modal-footer">
                        <button
                            disabled={disable}
                            className="btn-danger"
                            onClick={closeModal}
                        >
                            Cancel
                        </button>
                        <button disabled={disable} className="btn" onClick={saveNews}>
                            Save
                        </button>
                    </div>
                </div>
            </div>
        );
    }
Enter fullscreen mode Exit fullscreen mode

We have a state that holds the disabling state of buttons. The UI displays input boxes and a text area where we enter our news info. Then, there is a Lang select element where we can select the language. Here, we hardcoded our two locales “English” and “French”, so we are bound with two languages here. It was done this way just to show you how you can toggle between languages from your frontend. A better solution will be to fetch the locales for this collection and dynamically list them in the select element.

Now, when adding your locales to the option element, the locale code is very important. Omitting any part of it will make it fetch the wrong language version or not work at all. For example, the French locale code is fr-FR, adding something like fr will not make it work. So we should be very careful when hardcoding locale code, or better still fetching the locales is the best option.

The “Save” button calls the saveNews function. The saveNews function picks the title, imageUrl, writtenBy, body, and lang values from the input boxes, text area, and select element, and disables the buttons. It then calls the http://localhost:1337/api/newsposts endpoint, passing the picked values as payload in the POST body. See that the language type for the news is set in the payload’s locale property. Finally, the page is reloaded and the newly added news post will be displayed on the news page.

3. Add Styling

Add global styles in index.css:

    /* /newsapp/newsapp-strapi/src/index.css */

    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
        "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
        "Helvetica Neue", sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      background-color: rgba(234, 238, 243, 1);
    }
    code {
      font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
        monospace;
    }
    button {
      height: 30px;
      padding: 0px 15px 2px;
      font-weight: 400;
      font-size: 1rem;
      line-height: normal;
      border-radius: 2px;
      cursor: pointer;
      outline: 0px;
      background-color: rgb(0, 126, 255);
      border: 1px solid rgb(0, 126, 255);
      color: rgb(255, 255, 255);
      text-align: center;
    }
    .btn-danger {
      background-color: rgb(195 18 18);
      border: 1px solid rgb(195 18 18);
    }
    .container {
      min-height: 100vh;
      /*padding: 0 0.5rem; */
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background-color: rgba(234, 238, 243, 1);
    }
    .main {
      /*padding: 5rem 0;*/
      flex: 1;
      display: flex;
      flex-direction: column;
      width: 46%;
      /*justify-content: center;
      align-items: center;*/
    }
    .modal {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      z-index: 1000;
      font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
    }
    .modal-backdrop {
      opacity: 0.5;
      width: inherit;
      height: inherit;
      background-color: grey;
      position: fixed;
    }
    .modal-body {
      padding: 5px;
      padding-top: 15px;
      padding-bottom: 15px;
    }
    .modal-footer {
      padding: 15px 5px;
      display: flex;
      justify-content: space-between;
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .modal-header h3 {
      margin: 0;
    }
    .modal-content {
      background-color: white;
      z-index: 1;
      padding: 10px;
      margin-top: 10px;
      width: 520px;
      box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
        0px 9px 46px 8px rgba(0, 0, 0, 0.12);
      border-radius: 4px;
    }
    input[type="text"] {
      width: 100%;
      /*height: 3.4rem;*/
      padding: 9px;
      font-weight: 400;
      /*font-size: 1.3rem;*/
      cursor: text;
      outline: 0px;
      border: 1px solid rgb(227, 233, 243);
      border-radius: 2px;
      color: rgb(51, 55, 64);
      background-color: transparent;
      box-sizing: border-box;
    }
    .label {
      padding: 4px 0;
      font-size: small;
      color: rgb(51, 55, 64);
    }
    .content {
      display: flex;
      flex-wrap: wrap;
      flex-direction: column;
    }
    .inputField {
      margin: 3px 7px;
      flex: 1 40%;
    }
    .disable {
      opacity: 0.5;
      cursor: not-allowed;
    }
    a[href] {
      text-decoration: none;
      color: black;
    }
    a:visited {
      color: black;
    }
Enter fullscreen mode Exit fullscreen mode

Test the App

We begin testing the app by making sure the backend server and the frontend are running.

    /newsapp/newsapp-strapi $ cd /newsapp/newsapp-api && yarn develop
    /newsapp/newsapp-api $ cd /newsapp/newsapp-strapi && npm run start
Enter fullscreen mode Exit fullscreen mode

You will start by adding a news item. In your browser go to http://localhost:3000/news page and click on the Add News button. On the modal that shows up type in the data:

    title -> Minnesota National Guard deployed after protests over the police killing of a man during a traffic stop
    imageUrl -> https://logos-world.net/wp-content/uploads/2020/11/CNN-Logo-700x394.png
    Body -> (CNN) -- Hundreds of people __TRUNC__t," Walz tweeted.
    writtenBy -> CNN
Enter fullscreen mode Exit fullscreen mode

Add News post in test mode

Click on Save.

List of News posts

The news post is added. Click on it to confirm if it is readable.

Newly added News post

Add the French (fr-FR) version. Go back to http://localhost:3000/news page. Click on Add News. This time change the language to French.

Add the French version of the news post:

    title -> La Garde nationale du Minnesota déployée après des manifestations contre le meurtre par la police d'un homme lors d'un arrêt de la circulation
    imageUrl -> https://logos-world.net/wp-content/uploads/2020/11/CNN-Logo-700x394.png
    Body -> (CNN) Des __TRUNC__es forces de l'ordre", a tweeté Walz.
    writtenBy -> CNN
Enter fullscreen mode Exit fullscreen mode

Click on Save. Select the French language:

Sample

See the news in French, click on it.
Sample

Finally, test deleting the news posts.

Click on the Delete button and on the OK button on the confirm dialog that shows up. See the news is gone.

Conclusion

We learned how to use the Strapi i18n plugin to add internationalization to our Strapi API endpoints, and it is quite simple to use.

We started by learning what Strapi does and how it makes building APIs highly efficient. Next, we introduced the i18n plugin, and from there, we created a Strapi project to demonstrate how to use the i18n plugin in a Strapi project.

Next, we built a news app in React.js and showed how we could support multiple languages in a Reactjs app using Strapi as a backend with the help of the i18n plugin.

View the source code here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .