This article is a guest post by Chidume Nnamdi. He wrote this blog post through the Write for the Community program. If you are passionate about everything jamstack, open-source or javascript and want to share, join the writer's guild!
In this tutorial, we will build a News app using Strapi and React.js. We will use the new Strapi i18n plugin to translate the backend content from a language to another. The Strapi will be used to build the backend and provide the API endpoints, and React.js we will use to build our frontend part.
Read on.
What is Strapi? Strapi is an open-source headless content management system based on Node.js. Strapi provides us 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 the 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 get a movie from the backend./movies/:id
PUT: This edit a movie./movies
POST: This add 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 allows you to use your favorite database such as MongoDB, 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 build an app with content displayed in English, but there are chances that a Chinese user might use your app or a Spanish user. You wouldn't want a language to be a barrier that stops users from accessing your app. With Content Internationalization, you can translate the content of your application from a language to another. You can translate from English to Chinese, from English to Spanish, French to German, etc.
Strapi has an old way of add internationalization to Strapi, but recently they released a new plugin that simplifies the process. It is the i18n. This Strapi i18n plugin will allow users to create different localized/language versions of their API content. It will also help developers build different language versions of their projects by fetching the content based on the language/locale of the user.
Requirements In this tutorial, we will need to install some tools if not already installed.
npm --version
to confirm.npm i yarn -g
. The -g
sub-command makes it to be installed globally in your machine so it can be accessed from any directory.Scaffold a Strapi project Follow the below instructions to create a Strapi project:
mkdir newsapp
cd newsapp
1npx create-strapi-app newsapp-api --quickstart
2# or
3yarn create strapi-app newsapp-api --quickstart
This will create a Strapi project in newsapp-api
folder . Strapi will install the dependencies.
NB: The Strapi project will be using an SQLite database (.tmp/data.db).
cd newsapp-api
Install Strapi i18n plugin This Strapi i18n via two ways:
Here, we will describe the plugin installation via the Terminal.
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, an update of the Strapi app is needed, as well as a manual installation of the plugin.
Note: For users with Strapi version 3.6.0 and above, you should skip this section to Register section, but I will recommend you read this section in case you will ever need to upgrade your Strapi app.
To upgrade your Strapi app, go to the package.json
and set the Strapi packages to the latest version you want to upgrade to.
Close the Strapi server before doing this.
For example, to upgrade the Strapi packages below from their 3.2.4
to 3.6.0
, you do this:
1{
2 //...
3 "dependencies": {
4 "strapi": "3.2.4",
5 "strapi-admin": "3.2.4",
6 "strapi-connector-bookshelf": "3.2.4",
7 "strapi-plugin-content-manager": "3.2.4",
8 "strapi-plugin-content-type-builder": "3.2.4",
9 "strapi-plugin-email": "3.2.4",
10 "strapi-plugin-graphql": "3.2.4",
11 "strapi-plugin-upload": "3.2.4",
12 "strapi-plugin-users-permissions": "3.2.4",
13 "strapi-utils": "3.2.4"
14 //...
15 }
16}
upgraded:
1{
2 //...
3 "dependencies": {
4 "strapi": "3.6.0",
5 "strapi-admin": "3.6.0",
6 "strapi-connector-bookshelf": "3.6.0",
7 "strapi-plugin-content-manager": "3.6.0",
8 "strapi-plugin-content-type-builder": "3.6.0",
9 "strapi-plugin-email": "3.6.0",
10 "strapi-plugin-graphql": "3.6.0",
11 "strapi-plugin-upload": "3.6.0",
12 "strapi-plugin-users-permissions": "3.6.0",
13 "strapi-utils": "3.6.0"
14 //...
15 }
16}
We changed the version numbers to 3.6.0
.
Re-install the packages by running the command:
1yarn install
2# or
3npm install
This should install the packages with the latest versions it was upgraded to.
If there is an issue, do any or all of these:
yarn.lock
or package-lock.json
, and re-run the command.node_modules
and try again.Now, we will install the i18n plugin. To do that, run the below command:
1yarn strapi install i18n
2# or
3npm run strapi install i18n
Now, we run the build
command and then start the server by running the develop
command:
1npm run build --clean && npm run develop
2# or
3yarn build --clean && yarn develop
Strapi will start its server at http://localhost:1337
, and load its admin panel to register. Fill in your details, and click on “LET’S START” button. This makes Strapi register your details and shows the admin panel.
Now we will begin creating our collections.
Build the Strapi collections We are building a news website, a single news item will have this model:
1newspost {
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 head image of the news.Now, we create a news collection. Strapi will provide us with the endpoints:
/``newsposts
GET: This endpoint will retrieve all the news content./newsposts/:id
GET: This endpoint will retrieve a piece of particular news from 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. The news item is gotten from the :id
in the URL.On the admin page, click on the “> CREATE YOUR FIRST CONTENT-TYPE” button. A modal will show up. On the “Create a collection type” modal that shows up type in “newspost”.
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 enable the “Enable localization for this Content-Type” checkbox. This will allow you to have content in different locales.
Repeat this process for body
, writtenBy
, imageUrl
fields on the news model. At the end of imageUrl
addition, click on the "Finish" button. We will see the “Newspost” page displaying the fields we just added. Now, click on the “Save” button.
Now, a “Newsposts” link will appear on the sidebar.
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.
See English is the default locale/language. Let’s add 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.
French (France) (fr-FR)
.Our app now supports both English and French languages.
Seeding our database Now, we will add content to our database.
See that we are currently adding content for “English”. Since we have added the fr-Fr
French locale, clicking on the dropbox we will see the fr-Fr
we added.
Languages added in the Internationalization section will appear in the dropdown, and we can select them to add content for that language. So, if we select the “French (France) (fr-FR)”, the content locale will then be for fr-Fr
.
But for now, let’s add news content for “English” locale.
Enter the data:
1title -> Nepal's Minister sworn into office
2imageUrl -> https://thehimalayantimes.com/uploads/imported_images/wp-content/uploads/2020/10/Ministers-Sworn-in.jpg
3writtenBy -> Chidume Nnamdi
4body -> Type_any_thing_here.
Let’s add two more:
1title -> Angular 12 released.
2imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1280px-Angular_full_color_logo.svg.png
3writtenBy -> M. Hry
4body -> Type_any_thing_here.
1title -> React v13 released
2imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1920px-React-icon.svg.png
3writtenBy -> Dan Tierry
4body -> Type_any_thing_here.
Let’s add news content for French locale
The page will reload:
There is no data for French (France) (fr-FR)
yet. So we 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:
1title -> Le ministre du Népal a prêté serment
2imageUrl -> https://thehimalayantimes.com/uploads/imported_images/wp-content/uploads/2020/10/Ministers-Sworn-in.jpg
3writtenBy -> Chidume Nnamdi
4body -> Type_any_thing_here.
Now, do this for data 2 and 3:
1title -> Angular 12 est sorti.
2imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Angular_full_color_logo.svg/1280px-Angular_full_color_logo.svg.png
3writtenBy -> M. Hry
4body -> Type_any_thing_here.
5
6
7title -> Sortie de React v13
8imageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1920px-React-icon.svg.png
9writtenBy -> Dan Tierry
10body -> Type_any_thing_here.
Our data content for the French fr-Fr
locale is ready.
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:
On the “Roles” page, click on “Public”
Scroll down to the “Permissions” section:
There check the “Select all” checkbox. This makes all the endpoints to be accessible to the public and the CRUD actions can be performed on them.
Now, scroll up to the top, and click on the “Save” button.
This saves our changes.
Testing our Strapi endpoints We test the endpoints. Open Postman, if you don’t have it already, you can download it from here.
Get all Newsposts
Create a new collection so we can organize our requests. Add a request to the collection.
In the input box type http://localhost:1337/newsposts
and click on "Send".
See that only the English Newsposts
locale is returned because the default is set to be "English". To get the French translation we add _locale=fr-Fr
to the URL and send. The fr-FR
is the standard language code for the French language.
Type http://localhost:1337/newsposts?_locale=fr-FR
in the input box and press "Send". The result will be in French:
Notice that both the News posts in both English and French has 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.
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, like this:
The payload:
1title: Nouvelles fonctionnalités de Vue js et changements de rupture - Présentation de Vue 3
2body: __add anything french here___
3writtenBy:CJ
4imageUrl: https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1920px-Vue.js_Logo_2.svg.png
5locale: fr-FR
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.
Scaffold React app Now, we will build the frontend part of our app.
create-react-app
tool installed in your machine. If not, you can install globally by running this command:1npm i create-react-app -g
1create-react-app --version
newsapp-api
folder, so we move out:1cd ../
newsapp-strapi
React project by running the following command:1create-react-strapi
1cd newsapp-strapi
axios
module, we will use for querying our Strapi endpoints:
npm i axiosreact-router-dom
:
npm i react-router-domlocalhost:3000
.We are set to go.
Build the components Our 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 of the news item.Our final app will look like this:
This displays a news.
Reading the full news.
Let’s break our app into components.
/news
route. It will display the list of news. It is a smart componentNewsList
component.Our page components will be in pages
folder while the presentational components will be in components
folder.
We will create two folders pages
and components
inside the src
folder. Follow the below instructions to create the folders:
Now, we will create Header
, AddNewsDialog
, NewsCard
components inside the components
folder. Follow the below instructions:
components
folder:
cd src/components1mkdir AddNewsDialog
2touch AddNewsDialog/index.js
3
4mkdir Header
5touch Header/index.js
6touch Header/Header.css
7
8mkdir NewsCard
9touch NewsCard/index.js
10touch NewsCard/NewsCard.css
Let’s create the page components.
components
folder:
cd ../pages
folder:
cd pages1mkdir NewsList
2touch NewsList/index.js
3touch NewsList/NewsList.css
4
5mkdir NewsView
6touch NewsView/index.js
7touch NewsView/NewsView.css
App.js
, clear the content and paste the below code:1import "./App.css";
2import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
3import Header from "./components/Header";
4import NewsList from "./pages/NewsList";
5import NewsView from "./pages/NewsView";
6
7function App() {
8 return (
9 <>
10 <Header />
11 <div className="container">
12 <main className="main">
13 <BrowserRouter>
14 <Switch>
15 <Route path="/news">
16 <NewsList />
17 </Route>
18 <Route path="/newsview/:id">
19 <NewsView />
20 </Route>
21 <Route exact path="/">
22 <Redirect to="/news" />
23 </Route>
24 <Route path="*">
25 <NewsList />
26 </Route>{" "}
27 </Switch>
28 </BrowserRouter>
29 </main>
30 </div>
31 </>
32 );
33}
34export default App;
See we set up our 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 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.
App.css
and save the file.Header component
Header.js
file.1import "./Header.css";
2
3export default function Header() {
4 return (
5 <section className="header">
6 <div className="headerName">News24</div>
7 </section>
8 );
9}
Just a simple UI that displays News24
.
Header.css
file:CSS:
1.header {
2 height: 54px;
3 background-color: black;
4 color: white;
5 display: flex;
6 align-items: center;
7 padding: 10px;
8 font-family: sans-serif;
9 /*width: 100%;*/
10 padding-left: 27%;
11}
12.headerName {
13 font-size: 1.8em;
14}
NewsList page
NewsList.js
file:
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() {1const [newsList, setNewsList] = useState([]);
2const [locale, setLocale] = useState("en");
3const [showModal, setShowModal] = useState(false);
4
5useEffect(() => {
6 async function fetchNews() {
7 const data = await axios.get(
8 "http://localhost:1337/newsposts?_locale=" + locale
9 );
10 setNewsList([...data?.data]);
11 }
12 fetchNews();
13}, [locale]);
14
15function setLang() {
16 setLocale(window.locales.value);
17}
18
19function showAddNewsDialog() {
20 setShowModal(!showModal);
21}
22
23return (
24 <div className="newslist">
25 <div className="newslistbreadcrumb">
26 <div className="newslisttitle">
27 <h3>World News</h3>
28 </div>
29 <div style={{ display: "flex", alignItems: "center" }}>
30 <div style={{ marginRight: "4px" }}>
31 <button onClick={showAddNewsDialog}>Add News</button>
32 </div>
33 <div>
34 <select name="locales" id="locales" onChange={setLang}>
35 <option value="en">English</option>
36 <option value="fr-FR">French</option>
37 </select>
38 </div>
39 </div>
40 </div>
41 <div>
42 {newsList?.map((newsItem, i) => (
43 <NewsCard newsItem={newsItem} key={i} />
44 ))}
45 </div>
46 {showModal ? <AddNewsDialog closeModal={showAddNewsDialog} /> : null}
47 </div>
48);
}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
, and the last holds the modal visibility state.
The useEffect
loads the news whenever the component mounts from the "http://localhost:1337/newsposts?_locale="
endpoint, the current locale is added to it based on the selected locale
. This makes our news app to be internationalized, we can view and read news in both English and French. The result is set to 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 look at 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.
NewsList.css
and paste the below code:1.newslistbreadcrumb {
2 display: flex;
3 align-items: center;
4 justify-content: space-between;
5 border-bottom: 1px solid darkgray;
6}
7.newslisttitle {
8 margin-left: 12px;
9}
10.newslisttitle h3 {
11 color: rgb(107 107 107);
12 font-weight: 500;
13}
NewsView page
This page component renders the news selected from the NewsList
component.
NewsView.js
file:1import "./NewsView.css";
2import { useParams } from "react-router-dom";
3import axios from "axios";
4import { useEffect, useState } from "react";
5
6export default function NewsView() {
7 let { id } = useParams();
8 const [news, setNews] = useState();
9
10 useEffect(() => {
11 async function getNews() {
12 const data = await axios.get("http://localhost:1337/newsposts/" + id);
13 setNews(data?.data);
14 }
15 getNews();
16 }, [id]);
17
18 async function deleteNews() {
19 if (window.confirm("Do you want to delete this news?")) {
20 await axios.delete("http://localhost:1337/newsposts/" + id);
21 window.history.pushState(null, "", "/news");
22 window.location.reload();
23 }
24 }
25 return (
26 <div className="newsview">
27 <div
28 className="newsviewimg"
29 style={{ backgroundImage: `url(${news?.imageUrl})` }}
30 ></div>
31 <div>
32 <div className="newsviewtitlesection">
33 <div className="newsviewtitle">
34 <h1>{news?.title}</h1>
35 </div>
36 <div className="newsviewdetails">
37 <span style={{ flex: "1", color: "rgb(99 98 98)" }}>
38 Written By: <span>{news?.writtenBy}</span>
39 </span>
40 <span style={{ flex: "1", color: "rgb(99 98 98)" }}>
41 Date: <span>{news?.created_at}</span>
42 </span>
43 <span>
44 <button className="btn-danger" onClick={deleteNews}>
45 Delete
46 </button>
47 </span>
48 </div>
49 </div>
50 <div className="newsviewbody">{news?.body}</div>
51 </div>
52 </div>
53 );
54}
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/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/newsposts/" + id
. Then, navigate back to the /news
page. This component’s UI displays the news in its entirety.
NewsView.css
and paste the below code:1.newsview {
2 margin-top: 7px;
3}
4.newsviewimg {
5 background-color: darkgray;
6 background-repeat: no-repeat;
7 background-size: cover;
8 background-position: center;
9 height: 200px;
10}
11.newsviewdetails {
12 display: flex;
13 justify-content: space-between;
14 align-items: center;
15}
16.newsviewtitlesection {
17 margin-bottom: 20px;
18}
19.newsviewtitle h1 {
20 margin-bottom: 6px;
21}
22.newsviewbody {
23 font-size: large;
24}
25.newsviewbody::first-letter {
26 font-weight: 700;
27 font-size: 4em;
28 line-height: 0.83;
29 float: left;
30 margin-right: 7px;
31 margin-bottom: 4px;
32}
33.newsviewbody {
34 clear: left;
35 font-size: 21px;
36 line-height: 1.58;
37 letter-spacing: -0.003em;
38}
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 list of news in the app.
NewsCard.js
file:1import { Link } from "react-router-dom";
2import "./NewsCard.css";
3export default function NewsCard({ newsItem }) {
4 const { title, body, imageUrl, id } = newsItem;
5 const synopsis = body.slice(0, 150);
6 return (
7 <Link to={"/newsview/" + id}>
8 <div className="newscard">
9 <div
10 className="newscardimg"
11 style={{ backgroundImage: `url(${imageUrl})` }}
12 ></div>
13 <div>
14 <div className="newscardtitle">
15 <h1>{title}</h1>
16 </div>
17 <div>
18 <span>{synopsis}</span>
19 </div>
20 <div></div>
21 </div>
22 </div>
23 </Link>
24 );
25}
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 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 display it.
See that we set a link to the full news by wrapping the div#newscard
in 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. So paste the above code in NewsCard/index.js
.
NewsCard.css
and paste the below code:1.newscard {
2 /*background-color: white;*/
3 padding: 8px;
4 /*box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
5 transition: 0.3s;*/
6 border-radius: 4px;
7 margin: 8px;
8 cursor: pointer;
9 display: flex;
10}
11.newscardimg {
12 width: 146px;
13 height: 146px;
14 background-color: darkgray;
15 background-repeat: no-repeat;
16 background-size: cover;
17 background-position: center;
18 margin-right: 9px;
19 flex: 1 100%;
20}
21.newscardtitle {
22 flex: 1 100%;
23}
24.newscardtitle h1 {
25 margin-top: 0;
26 margin-bottom: 1px;
27}
AddNewsDialog component
AddNewsDialog.js
file:1import { useState } from "react";
2import axios from "axios";
3
4export default function AddNewsDialog({ closeModal }) {
5 const [disable, setDisable] = useState(false);
6 async function saveNews() {
7 const title = window.newsTitle.value;
8 const imageUrl = window.newsImageUrl.value;
9 const writtenBy = window.newsWrittenBy.value;
10 const body = window.newsBody.value;
11 const lang = window.addNewsLocales.value;
12
13 setDisable(true);
14 await axios.post("http://localhost:1337/newsposts", {
15 title,
16 imageUrl,
17 writtenBy,
18 body,
19 locale: lang,
20 });
21 window.location.reload();
22 setDisable(false);
23 }
24
25 return (
26 <div className="modal">
27 <div className="modal-backdrop" onClick={closeModal}></div>
28 <div className="modal-content">
29 <div className="modal-header">
30 <h3>Add News</h3>
31 <span
32 style={{ padding: "10px", cursor: "pointer" }}
33 onClick={closeModal}
34 >
35 X
36 </span>
37 </div>
38 <div className="modal-body content">
39 <div style={{ display: "flex", flexWrap: "wrap" }}>
40 <div className="inputField">
41 <div className="label">
42 <label>Title</label>
43 </div>
44 <div>
45 <input id="newsTitle" type="text" />
46 </div>
47 </div>
48 <div className="inputField">
49 <div className="label">
50 <label>Lang</label>
51 </div>
52 <div>
53 <select name="addNewsLocales" id="addNewsLocales">
54 <option value="en">English</option>
55 <option value="fr-FR">French</option>
56 </select>
57 </div>
58 </div>
59 <div className="inputField">
60 <div className="label">
61 <label>ImageUrl</label>
62 </div>
63 <div>
64 <input id="newsImageUrl" type="text" />
65 </div>
66 </div>
67 <div className="inputField">
68 <div className="label">
69 <label>Written By</label>
70 </div>
71 <div>
72 <input id="newsWrittenBy" type="text" />
73 </div>
74 </div>
75 <div className="inputField" style={{ flex: "2 1 100%" }}>
76 <div className="label">
77 <label>Body</label>
78 </div>
79 <div>
80 <textarea
81 id="newsBody"
82 style={{ width: "100%", height: "200px" }}
83 ></textarea>
84 </div>
85 </div>
86 </div>
87 </div>
88 <div className="modal-footer">
89 <button
90 disabled={disable}
91 className="btn-danger"
92 onClick={closeModal}
93 >
94 Cancel
95 </button>
96 <button disabled={disable} className="btn" onClick={saveNews}>
97 Save
98 </button>
99 </div>
100 </div>
101 </div>
102 );
103}
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 we are adding. 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 make it not 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
, lang
values from the input boxes, text area and select element, and disables the buttons. It then calls the http://localhost:1337/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.
index.css
:1body {
2 margin: 0;
3 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
4 "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
5 "Helvetica Neue", sans-serif;
6 -webkit-font-smoothing: antialiased;
7 -moz-osx-font-smoothing: grayscale;
8 background-color: rgba(234, 238, 243, 1);
9}
10code {
11 font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 monospace;
13}
14button {
15 height: 30px;
16 padding: 0px 15px 2px;
17 font-weight: 400;
18 font-size: 1rem;
19 line-height: normal;
20 border-radius: 2px;
21 cursor: pointer;
22 outline: 0px;
23 background-color: rgb(0, 126, 255);
24 border: 1px solid rgb(0, 126, 255);
25 color: rgb(255, 255, 255);
26 text-align: center;
27}
28.btn-danger {
29 background-color: rgb(195 18 18);
30 border: 1px solid rgb(195 18 18);
31}
32.container {
33 min-height: 100vh;
34 /*padding: 0 0.5rem; */
35 display: flex;
36 flex-direction: column;
37 justify-content: center;
38 align-items: center;
39 background-color: rgba(234, 238, 243, 1);
40}
41.main {
42 /*padding: 5rem 0;*/
43 flex: 1;
44 display: flex;
45 flex-direction: column;
46 width: 46%;
47 /*justify-content: center;
48 align-items: center;*/
49}
50.modal {
51 position: fixed;
52 top: 0;
53 left: 0;
54 width: 100%;
55 height: 100%;
56 display: flex;
57 flex-direction: column;
58 align-items: center;
59 z-index: 1000;
60 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
61}
62.modal-backdrop {
63 opacity: 0.5;
64 width: inherit;
65 height: inherit;
66 background-color: grey;
67 position: fixed;
68}
69.modal-body {
70 padding: 5px;
71 padding-top: 15px;
72 padding-bottom: 15px;
73}
74.modal-footer {
75 padding: 15px 5px;
76 display: flex;
77 justify-content: space-between;
78}
79.modal-header {
80 display: flex;
81 justify-content: space-between;
82 align-items: center;
83}
84.modal-header h3 {
85 margin: 0;
86}
87.modal-content {
88 background-color: white;
89 z-index: 1;
90 padding: 10px;
91 margin-top: 10px;
92 width: 520px;
93 box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
94 0px 9px 46px 8px rgba(0, 0, 0, 0.12);
95 border-radius: 4px;
96}
97input[type="text"] {
98 width: 100%;
99 /*height: 3.4rem;*/
100 padding: 9px;
101 font-weight: 400;
102 /*font-size: 1.3rem;*/
103 cursor: text;
104 outline: 0px;
105 border: 1px solid rgb(227, 233, 243);
106 border-radius: 2px;
107 color: rgb(51, 55, 64);
108 background-color: transparent;
109 box-sizing: border-box;
110}
111.label {
112 padding: 4px 0;
113 font-size: small;
114 color: rgb(51, 55, 64);
115}
116.content {
117 display: flex;
118 flex-wrap: wrap;
119 flex-direction: column;
120}
121.inputField {
122 margin: 3px 7px;
123 flex: 1 40%;
124}
125.disable {
126 opacity: 0.5;
127 cursor: not-allowed;
128}
129a[href] {
130 text-decoration: none;
131 color: black;
132}
133a:visited {
134 color: black;
135}
Test the app Let’s test our app. We will start by adding a news item.
/news
page and add click on the Add News
button.1title -> Minnesota National Guard deployed after protests over the police killing of a man during a traffic stop
2imageUrl -> https://logos-world.net/wp-content/uploads/2020/11/CNN-Logo-700x394.png
3Body -> (CNN) -- Hundreds of people __TRUNC__t," Walz tweeted.
4writtenBy -> CNN
/news
page and click on "Add News", this time select the lang to French.Add the data, I translated it to French using Google Translate:
1title -> 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
2imageUrl -> https://logos-world.net/wp-content/uploads/2020/11/CNN-Logo-700x394.png
3Body -> (CNN) Des __TRUNC__es forces de l'ordre", a tweeté Walz.
4writtenBy -> CNN
Click on “Save”. Now, select the “French” lang:
See the news in French, click on it.
Now, we delete it.
Source code
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.
Author of "Understanding JavaScript", Chidume is also an awesome writer about JavaScript, Angular, React and other web technologies.