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.
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.
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.
Before continuing this tutorial, you should know, have, or do some things.
To follow along, you need to have some basic knowledge of:
In this tutorial, we will need to install some tools if not already installed:
Operating System (any of the following are supported):
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.
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.
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.
1 $ mkdir newsapp
2 $ cd newsapp
3 /newsapp $ git init
Create a Strapi project named newsapp-api
.
/newsapp $ npx create-strapi-app newsapp-api --quickstart
# or
/newsapp $ yarn create strapi-app newsapp-api --quickstart
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:
1 /newsapp $ tree -L 2 -a
2 .
3 ├── newsapp-api
4 │ ├── build
5 │ ├── .cache
6 │ ├── config
7 │ ├── data
8 │ ├── database
9 │ ├── .editorconfig
10 │ ├── .env
11 │ ├── .env.example
12 │ ├── .eslintignore
13 │ ├── .eslintrc
14 │ ├── favicon.ico
15 │ ├── .gitignore
16 │ ├── node_modules
17 │ ├── package.json
18 │ ├── public
19 │ ├── README.md
20 │ ├── src
21 │ ├── .strapi-updater.json
22 │ ├── .tmp
23 │ └── yarn.lock
24 ├── .git
25 ├── LICENSE
26 └── README.md
Move inside the Strapi install folder.
/newsapp $ cd newsapp-api
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.
Start the Strapi server
/newsapp/newsapp-api $ npm run develop
# OR
/newsapp/newsapp-api $ yarn develop
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.
️ 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.
Now, we will begin creating our collections.
We are building a news website, a single news item will have this model:
1 newspost {
2 title
3 body
4 writtenBy
5 imageUrl
6 }
title
is the title of the news.body
is the content of the news.writtenBy
is the author of the news.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.
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.
Click Continue and a modal appears with field options for your collection type. Select Text.
In the Add new Text field modal, enter title
for Name, leave Type as Short text
and click on + Add another field.
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.
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.
1 /newsapp/newsapp-api $ cd src
2 /newsapp/newsapp-api/src $ tree
3 .
4 ├── admin
5 │ ├── app.example.js
6 │ └── webpack.config.example.js
7 ├── api
8 │ └── newspost
9 │ ├── content-types
10 │ │ └── newspost
11 │ │ └── schema.json
12 │ ├── controllers
13 │ │ └── newspost.js
14 │ ├── routes
15 │ │ └── newspost.js
16 │ └── services
17 │ └── newspost.js
18 ├── extensions
19 └── index.js
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.
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.
See that the French (France) (fr-FR) locale is added to your list of locales for your Newsposts collection.
Our app now supports both English and French languages.
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.
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:
1 title -> Nepal's Minister sworn into office
2 imageUrl -> https://thehimalayantimes.com/uploads/imported_images/wp-content/uploads/2020/10/Ministers-Sworn-in.jpg
3 writtenBy -> John Doe
4 body -> Type anything here
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:
1 title -> Angular 12 released.
2 imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1280px-Angular_full_color_logo.svg.png
3 writtenBy -> M. Hry
4 body -> Type anything here
Click on Save then Publish. Add this:
1 title -> React v17 released
2 imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1920px-React-icon.svg.png
3 writtenBy -> Dan Tierry
4 body -> Type anything here
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:
Click on the English (en) dropdown box and select French (France) (fr-FR).
The page will reload:
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:
1 title -> Le ministre du Népal a prêté serment
2 imageUrl -> https://thehimalayantimes.com/uploads/imported_images/wp-content/uploads/2020/10/Ministers-Sworn-in.jpg
3 writtenBy -> John Doe
4 body -> Type_any_thing_here.
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:
1 title -> Angular 12 est sorti.
2 imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1280px-Angular_full_color_logo.svg.png
3 writtenBy -> M. Hry
4 body -> Type_any_thing_here.
1 title -> Sortie de React v17
2 imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1920px-React-icon.svg.png
3 writtenBy -> Dan Tierry
4 body -> Type_any_thing_here.
News posts for the French (France) fr-FR locale are ready.
Now, only authenticated users can access our endpoints. Let’s make the endpoints to be accessible to the public.
To do that:
Let's test the endpoints. Open Postman. If you don’t have it already, you can download it from here.
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.
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.
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.
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.
1 {
2 "data": {
3 "title": "Nouvelles fonctionnalités de Vue js et changements de rupture - Présentation de Vue 3",
4 "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.",
5 "writtenBy": "CJ",
6 "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1920px-Vue.js_Logo_2.svg.png",
7 "locale": "fr-FR"
8 }
9 }
Click Send and wait for a response from the server. If successful, you should get the following response:
1 {
2 "data": {
3 "id": 8,
4 "attributes": {
5 "title": "Nouvelles fonctionnalités de Vue js et changements de rupture - Présentation de Vue 3",
6 "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.",
7 "writtenBy": "CJ",
8 "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1920px-Vue.js_Logo_2.svg.png",
9 "createdAt": "2022-08-30T19:39:30.199Z",
10 "updatedAt": "2022-08-30T19:39:30.199Z",
11 "publishedAt": "2022-08-30T19:39:30.194Z",
12 "locale": "fr-FR"
13 }
14 },
15 "meta": {}
16 }
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.
We are done with our Strapi backend; let’s build the frontend to see how the two meet.
Let's move to the next stage, which is setting up the React app frontend.
Now, you will build the frontend of your app.
Change the directory to the root of your project folder newsapp
:
/newsapp/newsapp-api $ cd ..
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
Test the installation by running the command:
/newsapp $ create-react-app --version
From here, scaffold the React project. Create a newsapp-strapi
React project by running the following command:
/newsapp $ create-react-app newsapp-strapi
Move into the folder:
/newsapp $ cd newsapp-strapi
Install the axios
module, which you will use for querying Strapi endpoints:
/newsapp/newsapp-strapi $ npm i axios
Next, you will need routes in our app. Install the react-router-dom
:
/newsapp/newsapp-strapi $ npm i react-router-dom
Start the server:
/newsapp/newsapp-strapi $ npm run start
Go to your browser and navigate to http://localhost:3000 to see the React frontend for your app.
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:
This displays a single news post.
Let’s break our app into components.
/news
route. It will display the list of news. It is a smart component.NewsList
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
Create Header
, AddNewsDialog
, and NewsCard
components inside the components
folder. Move inside the components
folder:
/newsapp/newsapp-strapi $ cd src/components
Create a folder named AddNewsDialog
and create an index.js
file inside it.
/newsapp/newsapp-strapi/src/components $ mkdir AddNewsDialog && touch AddNewsDialog/index.js
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
Do the same for the NewsCard
component.
/newsapp/newsapp-strapi/src/components $ mkdir NewsCard && touch NewsCard/index.js && touch NewsCard/NewsCard.css
Create the page components. Switch to the pages
folder.
/newsapp/newsapp-strapi/src/components $ cd /newsapp/newsapp-strapi/src/pages
Add the NewsList component.
/newsapp/newsapp-strapi/src/pages $ mkdir NewsList && touch NewsList/index.js && touch NewsList/NewsList.css
Add the NewsView component.
/newsapp/newsapp-strapi/src/pages $ mkdir NewsView && touch NewsView/index.js && touch NewsView/NewsView.css
Open App.js
in the src
folder. Clear the content and paste the below code:
1 /* /newsapp/newsapp-strapi/src/App.js */
2
3 import "./App.css";
4 import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
5 import Header from "./components/Header";
6 import NewsList from "./pages/NewsList";
7 import NewsView from "./pages/NewsView";
8
9 function App() {
10 return (
11 <>
12 <Header />
13 <div className="container">
14 <main className="main">
15 <BrowserRouter>
16 <Switch>
17 <Route path="/news">
18 <NewsList />
19 </Route>
20 <Route path="/newsview/:id">
21 <NewsView />
22 </Route>
23 <Route exact path="/">
24 <Redirect to="/news" />
25 </Route>
26 <Route path="*">
27 <NewsList />
28 </Route>{" "}
29 </Switch>
30 </BrowserRouter>
31 </main>
32 </div>
33 </>
34 );
35 }
36 export default App;
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.
1 // /newsapp/newsapp-strapi/src/components/Header/index.js
2
3 import "./Header.css";
4
5 export default function Header() {
6 return (
7 <section className="header">
8 <div className="headerName">News24</div>
9 </section>
10 );
11 }
Just a simple UI that displays the text “News24”. Add some styling to your header, using the Header.css
file:
1 /* /newsapp/newsapp-strapi/src/components/Header/Header.css */
2
3 .header {
4 height: 54px;
5 background-color: black;
6 color: white;
7 display: flex;
8 align-items: center;
9 padding: 10px;
10 font-family: sans-serif;
11 /*width: 100%;*/
12 padding-left: 27%;
13 }
14 .headerName {
15 font-size: 1.8em;
16 }
NEWSLIST COMPONENT
Insert the following code in the index.js
file for the NewsList
component:
1 // /newsapp/newsapp-strapi/src/pages/NewsList/index.js
2
3 import "./NewsList.css";
4 import NewsCard from "./../../components/NewsCard";
5 import { useEffect, useState } from "react";
6 import axios from "axios";
7 import AddNewsDialog from "../../components/AddNewsDialog";
8
9 export default function NewsList() {
10 const [newsList, setNewsList] = useState([]);
11 const [locale, setLocale] = useState("en");
12 const [showModal, setShowModal] = useState(false);
13
14 useEffect(() => {
15 async function fetchNews() {
16 const data = await axios.get(
17 "http://localhost:1337/api/newsposts?_locale=" + locale
18 );
19 setNewsList([...data?.data]);
20 }
21 fetchNews();
22 }, [locale]);
23
24 function setLang() {
25 setLocale(window.locales.value);
26 }
27
28 function showAddNewsDialog() {
29 setShowModal(!showModal);
30 }
31
32 return (
33 <div className="newslist">
34 <div className="newslistbreadcrumb">
35 <div className="newslisttitle">
36 <h3>World News</h3>
37 </div>
38 <div style={{ display: "flex", alignItems: "center" }}>
39 <div style={{ marginRight: "4px" }}>
40 <button onClick={showAddNewsDialog}>Add News</button>
41 </div>
42 <div>
43 <select name="locales" id="locales" onChange={setLang}>
44 <option value="en">English</option>
45 <option value="fr-FR">French</option>
46 </select>
47 </div>
48 </div>
49 </div>
50 <div>
51 {newsList?.map((newsItem, i) => (
52 <NewsCard newsItem={newsItem} key={i} />
53 ))}
54 </div>
55 {showModal ? <AddNewsDialog closeModal={showAddNewsDialog} /> : null}
56 </div>
57 );
58 }
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:
1 /* /newsapp/newsapp-strapi/src/pages/NewsList/NewsList.css */
2
3 .newslistbreadcrumb {
4 display: flex;
5 align-items: center;
6 justify-content: space-between;
7 border-bottom: 1px solid darkgray;
8 }
9 .newslisttitle {
10 margin-left: 12px;
11 }
12 .newslisttitle h3 {
13 color: rgb(107 107 107);
14 font-weight: 500;
15 }
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:
1 /* /newsapp/newsapp-strapi/src/pages/NewsView/index.js */
2
3 import "./NewsView.css";
4 import { useParams } from "react-router-dom";
5 import axios from "axios";
6 import { useEffect, useState } from "react";
7
8 export default function NewsView() {
9 let { id } = useParams();
10 const [news, setNews] = useState();
11
12 useEffect(() => {
13 async function getNews() {
14 const data = await axios.get("http://localhost:1337/api/newsposts/" + id);
15 setNews(data?.data);
16 }
17 getNews();
18 }, [id]);
19
20 async function deleteNews() {
21 if (window.confirm("Do you want to delete this news?")) {
22 await axios.delete("http://localhost:1337/api/newsposts/" + id);
23 window.history.pushState(null, "", "/news");
24 window.location.reload();
25 }
26 }
27 return (
28 <div className="newsview">
29 <div
30 className="newsviewimg"
31 style={{ backgroundImage: `url(${news?.imageUrl})` }}
32 ></div>
33 <div>
34 <div className="newsviewtitlesection">
35 <div className="newsviewtitle">
36 <h1>{news?.title}</h1>
37 </div>
38 <div className="newsviewdetails">
39 <span style={{ flex: "1", color: "rgb(99 98 98)" }}>
40 Written By: <span>{news?.writtenBy}</span>
41 </span>
42 <span style={{ flex: "1", color: "rgb(99 98 98)" }}>
43 Date: <span>{news?.created_at}</span>
44 </span>
45 <span>
46 <button className="btn-danger" onClick={deleteNews}>
47 Delete
48 </button>
49 </span>
50 </div>
51 </div>
52 <div className="newsviewbody">{news?.body}</div>
53 </div>
54 </div>
55 );
56 }
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:
1 /* /newsapp/newsapp-strapi/src/pages/NewsView/NewsView.css */
2
3 .newsview {
4 margin-top: 7px;
5 }
6 .newsviewimg {
7 background-color: darkgray;
8 background-repeat: no-repeat;
9 background-size: cover;
10 background-position: center;
11 height: 200px;
12 }
13 .newsviewdetails {
14 display: flex;
15 justify-content: space-between;
16 align-items: center;
17 }
18 .newsviewtitlesection {
19 margin-bottom: 20px;
20 }
21 .newsviewtitle h1 {
22 margin-bottom: 6px;
23 }
24 .newsviewbody {
25 font-size: large;
26 }
27 .newsviewbody::first-letter {
28 font-weight: 700;
29 font-size: 4em;
30 line-height: 0.83;
31 float: left;
32 margin-right: 7px;
33 margin-bottom: 4px;
34 }
35 .newsviewbody {
36 clear: left;
37 font-size: 21px;
38 line-height: 1.58;
39 letter-spacing: -0.003em;
40 }
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:
1 /* /newsapp/newsapp-strapi/src/components/NewsCard/index.js */
2
3 import { Link } from "react-router-dom";
4 import "./NewsCard.css";
5
6 export default function NewsCard({ newsItem }) {
7 const { title, body, imageUrl, id } = newsItem;
8 const synopsis = body.slice(0, 150);
9 return (
10 <Link to={"/newsview/" + id}>
11 <div className="newscard">
12 <div
13 className="newscardimg"
14 style={{ backgroundImage: `url(${imageUrl})` }}
15 ></div>
16 <div>
17 <div className="newscardtitle">
18 <h1>{title}</h1>
19 </div>
20 <div>
21 <span>{synopsis}</span>
22 </div>
23 <div></div>
24 </div>
25 </div>
26 </Link>
27 );
28 }
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:
1 /* /newsapp/newsapp-strapi/src/components/NewsCard/NewsCard.css */
2
3 .newscard {
4 /*background-color: white;*/
5 padding: 8px;
6 /*box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
7 transition: 0.3s;*/
8 border-radius: 4px;
9 margin: 8px;
10 cursor: pointer;
11 display: flex;
12 }
13 .newscardimg {
14 width: 146px;
15 height: 146px;
16 background-color: darkgray;
17 background-repeat: no-repeat;
18 background-size: cover;
19 background-position: center;
20 margin-right: 9px;
21 flex: 1 100%;
22 }
23 .newscardtitle {
24 flex: 1 100%;
25 }
26 .newscardtitle h1 {
27 margin-top: 0;
28 margin-bottom: 1px;
29 }
AddNewsDialog COMPONENT
Add the below code to index.js
for the AddNewsDialog
component:
1 /* /newsapp/newsapp-strapi/src/components/AddNewsDialog/index.js */
2
3 import { useState } from "react";
4 import axios from "axios";
5
6 export default function AddNewsDialog({ closeModal }) {
7 const [disable, setDisable] = useState(false);
8 async function saveNews() {
9 const title = window.newsTitle.value;
10 const imageUrl = window.newsImageUrl.value;
11 const writtenBy = window.newsWrittenBy.value;
12 const body = window.newsBody.value;
13 const lang = window.addNewsLocales.value;
14
15 setDisable(true);
16 await axios.post("http://localhost:1337/api/newsposts", {
17 title,
18 imageUrl,
19 writtenBy,
20 body,
21 locale: lang,
22 });
23 window.location.reload();
24 setDisable(false);
25 }
26
27 return (
28 <div className="modal">
29 <div className="modal-backdrop" onClick={closeModal}></div>
30 <div className="modal-content">
31 <div className="modal-header">
32 <h3>Add News</h3>
33 <span
34 style={{ padding: "10px", cursor: "pointer" }}
35 onClick={closeModal}
36 >
37 X
38 </span>
39 </div>
40 <div className="modal-body content">
41 <div style={{ display: "flex", flexWrap: "wrap" }}>
42 <div className="inputField">
43 <div className="label">
44 <label>Title</label>
45 </div>
46 <div>
47 <input id="newsTitle" type="text" />
48 </div>
49 </div>
50 <div className="inputField">
51 <div className="label">
52 <label>Lang</label>
53 </div>
54 <div>
55 <select name="addNewsLocales" id="addNewsLocales">
56 <option value="en">English</option>
57 <option value="fr-FR">French</option>
58 </select>
59 </div>
60 </div>
61 <div className="inputField">
62 <div className="label">
63 <label>ImageUrl</label>
64 </div>
65 <div>
66 <input id="newsImageUrl" type="text" />
67 </div>
68 </div>
69 <div className="inputField">
70 <div className="label">
71 <label>Written By</label>
72 </div>
73 <div>
74 <input id="newsWrittenBy" type="text" />
75 </div>
76 </div>
77 <div className="inputField" style={{ flex: "2 1 100%" }}>
78 <div className="label">
79 <label>Body</label>
80 </div>
81 <div>
82 <textarea
83 id="newsBody"
84 style={{ width: "100%", height: "200px" }}
85 ></textarea>
86 </div>
87 </div>
88 </div>
89 </div>
90 <div className="modal-footer">
91 <button
92 disabled={disable}
93 className="btn-danger"
94 onClick={closeModal}
95 >
96 Cancel
97 </button>
98 <button disabled={disable} className="btn" onClick={saveNews}>
99 Save
100 </button>
101 </div>
102 </div>
103 </div>
104 );
105 }
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.
Add global styles in index.css
:
1 /* /newsapp/newsapp-strapi/src/index.css */
2
3 body {
4 margin: 0;
5 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
6 "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
7 "Helvetica Neue", sans-serif;
8 -webkit-font-smoothing: antialiased;
9 -moz-osx-font-smoothing: grayscale;
10 background-color: rgba(234, 238, 243, 1);
11 }
12 code {
13 font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
14 monospace;
15 }
16 button {
17 height: 30px;
18 padding: 0px 15px 2px;
19 font-weight: 400;
20 font-size: 1rem;
21 line-height: normal;
22 border-radius: 2px;
23 cursor: pointer;
24 outline: 0px;
25 background-color: rgb(0, 126, 255);
26 border: 1px solid rgb(0, 126, 255);
27 color: rgb(255, 255, 255);
28 text-align: center;
29 }
30 .btn-danger {
31 background-color: rgb(195 18 18);
32 border: 1px solid rgb(195 18 18);
33 }
34 .container {
35 min-height: 100vh;
36 /*padding: 0 0.5rem; */
37 display: flex;
38 flex-direction: column;
39 justify-content: center;
40 align-items: center;
41 background-color: rgba(234, 238, 243, 1);
42 }
43 .main {
44 /*padding: 5rem 0;*/
45 flex: 1;
46 display: flex;
47 flex-direction: column;
48 width: 46%;
49 /*justify-content: center;
50 align-items: center;*/
51 }
52 .modal {
53 position: fixed;
54 top: 0;
55 left: 0;
56 width: 100%;
57 height: 100%;
58 display: flex;
59 flex-direction: column;
60 align-items: center;
61 z-index: 1000;
62 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
63 }
64 .modal-backdrop {
65 opacity: 0.5;
66 width: inherit;
67 height: inherit;
68 background-color: grey;
69 position: fixed;
70 }
71 .modal-body {
72 padding: 5px;
73 padding-top: 15px;
74 padding-bottom: 15px;
75 }
76 .modal-footer {
77 padding: 15px 5px;
78 display: flex;
79 justify-content: space-between;
80 }
81 .modal-header {
82 display: flex;
83 justify-content: space-between;
84 align-items: center;
85 }
86 .modal-header h3 {
87 margin: 0;
88 }
89 .modal-content {
90 background-color: white;
91 z-index: 1;
92 padding: 10px;
93 margin-top: 10px;
94 width: 520px;
95 box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
96 0px 9px 46px 8px rgba(0, 0, 0, 0.12);
97 border-radius: 4px;
98 }
99 input[type="text"] {
100 width: 100%;
101 /*height: 3.4rem;*/
102 padding: 9px;
103 font-weight: 400;
104 /*font-size: 1.3rem;*/
105 cursor: text;
106 outline: 0px;
107 border: 1px solid rgb(227, 233, 243);
108 border-radius: 2px;
109 color: rgb(51, 55, 64);
110 background-color: transparent;
111 box-sizing: border-box;
112 }
113 .label {
114 padding: 4px 0;
115 font-size: small;
116 color: rgb(51, 55, 64);
117 }
118 .content {
119 display: flex;
120 flex-wrap: wrap;
121 flex-direction: column;
122 }
123 .inputField {
124 margin: 3px 7px;
125 flex: 1 40%;
126 }
127 .disable {
128 opacity: 0.5;
129 cursor: not-allowed;
130 }
131 a[href] {
132 text-decoration: none;
133 color: black;
134 }
135 a:visited {
136 color: black;
137 }
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
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:
1 title -> Minnesota National Guard deployed after protests over the police killing of a man during a traffic stop
2 imageUrl -> https://logos-world.net/wp-content/uploads/2020/11/CNN-Logo-700x394.png
3 Body -> (CNN) -- Hundreds of people __TRUNC__t," Walz tweeted.
4 writtenBy -> CNN
Click on Save.
The news post is added. Click on it to confirm if it is readable.
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:
1 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
2 imageUrl -> https://logos-world.net/wp-content/uploads/2020/11/CNN-Logo-700x394.png
3 Body -> (CNN) Des __TRUNC__es forces de l'ordre", a tweeté Walz.
4 writtenBy -> CNN
Click on Save. Select the French language:
See the news in French, click on it.
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.
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.
Mark Munyaka is a freelance web developer and writer who loves problem-solving and testing out web technologies. You can follow him on Twitter @McMunyaka