React is a great framework for building interactive web apps. It comes with a bare bones set of features. It can render your page when you update your data and provides a convenient syntax for you to write your code easily. We can easily use it to build apps that uses public APIs, like the New York Times API.
In this story, we will build an app that uses the New York Times API with multilingual capability. You can view the static text in the app in English or French. The app can get the news from different sections and also do searches. It supports cross domain HTTP requests so that we can write client side apps that uses the API.
Before building the app, you need to register for an API key at https://developer.nytimes.com/
To start building the app, we use the Create React App command line utility to generate the scaffolding code. To use it, we run npx create-react-app nyt-app
to create the code in the nyt-app
folder. After that we need to install some libraries. We need the Axios HTTP client, a library to convert objects into query strings, Bootstrap library to make everything look better, React Router for routing and create forms easily with Formik and Yup. For translation and localization, we use the React-i18next library to allow us to translate our text into English and French. To install the libraries, we run npm i axios bootstrap formik i18next i18next-browser-languagedetector i18next-xhr-backend querystring react-bootstrap react-i18next react-router-dom yup
.
Now that we have all the libraries installed, we can start writing code. For simplicity’s sake, we put everything in the src
folder. We start by modifying App.js
. We replace the existing code with:
import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import SearchPage from "./SearchPage";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
const history = createHistory();
function App() {
const { t, i18n } = useTranslation();
const [initialized, setInitialized] = useState(false);
const changeLanguage = lng => {
i18n.changeLanguage(lng);
};
useEffect(() => {
if (!initialized) {
changeLanguage(localStorage.getItem("language") || "en");
setInitialized(true);
}
});
return (
<div className="App">
<Router history={history}>
<TopBar />
<Route path="/" exact component={HomePage} />
<Route path="/search" exact component={SearchPage} />
</Router>
</div>
);
}
export default App;
This is the root component of our app and is the component that is loaded when the app first loads. We use the useTranslation
function from the react-i18next
library returns an object with the t
property and the i18n
property,. Here, we destructured the returned object’s properties into its own variables. we will use the t
, which takes a translation key, to get the English or French text depending on the language set. In this file, we use the i18n
function to set the language with the provided i18n.changeLanguage
function. We also set the language from local storage when provided so that the chosen language will be persisted after refresh.
We also add the routes for our pages here used by the React router.
In App.css
,we put:
.center {
text-align: center;
}
to center some text.
Next we make the home page. we createHomePage.js
and in the file, we put:
import React from "react";
import { useState, useEffect } from "react";
import Form from "react-bootstrap/Form";
import ListGroup from "react-bootstrap/ListGroup";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import { getArticles } from "./requests";
import { useTranslation } from "react-i18next";
import "./HomePage.css";
const sections = `arts, automobiles, books, business, fashion, food, health,
home, insider, magazine, movies, national, nyregion, obituaries,
opinion, politics, realestate, science, sports, sundayreview,
technology, theater, tmagazine, travel, upshot, world`
.replace(/ /g, "")
.split(",");
function HomePage() {
const [selectedSection, setSelectedSection] = useState("arts");
const [articles, setArticles] = useState([]);
const [initialized, setInitialized] = useState(false);
const { t, i18n } = useTranslation();
const load = async section => {
setSelectedSection(section);
const response = await getArticles(section);
setArticles(response.data.results || []);
};
const loadArticles = async e => {
if (!e || !e.target) {
return;
}
setSelectedSection(e.target.value);
load(e.target.value);
};
const initializeArticles = () => {
load(selectedSection);
setInitialized(true);
};
useEffect(() => {
if (!initialized) {
initializeArticles();
}
});
return (
<div className="HomePage">
<div className="col-12">
<div className="row">
<div className="col-md-3 d-none d-md-block d-lg-block d-xl-block">
<ListGroup className="sections">
{sections.map(s => (
<ListGroup.Item
key={s}
className="list-group-item"
active={s == selectedSection}
>
<a
className="link"
onClick={() => {
load(s);
}}
>
{t(s)}
</a>
</ListGroup.Item>
))}
</ListGroup>
</div>
<div className="col right">
<Form className="d-sm-block d-md-none d-lg-none d-xl-none">
<Form.Group controlId="section">
<Form.Label>{t("Section")}</Form.Label>
<Form.Control
as="select"
onChange={loadArticles}
value={selectedSection}
>
{sections.map(s => (
<option key={s} value={s}>{t(s)}</option>
))}
</Form.Control>
</Form.Group>
</Form>
<h1>{t(selectedSection)}</h1>
{articles.map((a, i) => (
<Card key={i}>
<Card.Body>
<Card.Title>{a.title}</Card.Title>
<Card.Img
variant="top"
className="image"
src={
Array.isArray(a.multimedia) &&
a.multimedia[a.multimedia.length - 1]
? a.multimedia[a.multimedia.length - 1].url
: null
}
/>
<Card.Text>{a.abstract}</Card.Text>
<Button
variant="primary"
onClick={() => (window.location.href = a.url)}
>
{t("Go")}
</Button>
</Card.Body>
</Card>
))}
</div>
</div>
</div>
</div>
);
}
export default HomePage;
In this file, we display a responsive layout where there is a left bar is the screen is wide and the a drop down on the right pane if it’s not. We display the items in the chosen section we choose from the left pane or the drop down. To display the items, we use the Card widget from React Bootstrap. We also use the t
function provided by react-i18next
to load our text from our translation file, which we will create. To load the initial article entries, we run a function in the callback of the useEffect
function to show load the items once from the New York Times API. We need the initialized
flag so that the function in the callback won’t load on every re-render. In the drop down, we added code to load articles whenever the selection changes.
The we createHomePage.css
, and add:
.link {
cursor: pointer;
}
.right {
padding: 20px;
}
.image {
max-width: 400px;
text-align: center;
}
.sections {
margin-top: 20px;
}
We change the cursor style for the Go button and add some padding to the right pane.
Next we create a file for loading the translations and setting the default language. Create a file called i18n.js
and add:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { resources } from "./translations";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
lng: "en",
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false,
},
});
export default i18n;
In this file, we load the translations from a file, and set the default language to English. Since react-i18next
escapes everything, we can set escapeValue
to false
for interpolation
since it’s redundant.
We need a file to put the code for making HTTP requests. To do so, we create a file called requests.js
and add:
const APIURL = "https://api.nytimes.com/svc";
const axios = require("axios");
const querystring = require("querystring");
export const search = data => {
Object.keys(data).forEach(key => {
data["api-key"] = process.env.REACT_APP_APIKEY;
if (!data[key]) {
delete data[key];
}
});
return axios.get(
`${APIURL}/search/v2/articlesearch.json?${querystring.encode(data)}`
);
};
export const getArticles = section =>
axios.get(
`${APIURL}/topstories/v2/${section}.json?api-key=${process.env.REACT_APP_APIKEY}`
);
We load the API key from the process.env.REACT_APP_APIKEY
variable, which is provided by an environment variable in the .env
file located at the root folder. You have to create it yourself, and in there, put:
REACT_APP_APIKEY='you New York Times API key'
Replace the value on the right side with the API key you got after registering in the New York Times API website.
Next we create the search page. Create a file called SearchPage.js
and add:
import React from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import "./SearchPage.css";
import * as yup from "yup";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Trans } from "react-i18next";
import { search } from "./requests";
import Card from "react-bootstrap/Card";
const schema = yup.object({
keyword: yup.string().required("Keyword is required"),
});
function SearchPage() {
const { t } = useTranslation();
const [articles, setArticles] = useState([]);
const [count, setCount] = useState(0);
const handleSubmit = async e => {
const response = await search({ q: e.keyword });
setArticles(response.data.response.docs || []);
};
return (
<div className="SearchPage">
<h1 className="center">{t("Search")}</h1>
<Formik validationSchema={schema} onSubmit={handleSubmit}>
{({
handleSubmit,
handleChange,
handleBlur,
values,
touched,
isInvalid,
errors,
}) => (
<Form noValidate onSubmit={handleSubmit} className="form">
<Form.Row>
<Form.Group as={Col} md="12" controlId="keyword">
<Form.Label>{t("Keyword")}</Form.Label>
<Form.Control
type="text"
name="keyword"
placeholder={t("Keyword")}
value={values.keyword || ""}
onChange={handleChange}
isInvalid={touched.keyword && errors.keyword}
/>
<Form.Control.Feedback type="invalid">
{errors.keyword}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit" style={{ marginRight: "10px" }}>
{t("Search")}
</Button>
</Form>
)}
</Formik>
<h3 className="form">
<Trans i18nKey="numResults" count={articles.length}>
There are <strong>{{ count }}</strong> results.
</Trans>
</h3>
{articles.map((a, i) => (
<Card key={i}>
<Card.Body>
<Card.Title>{a.headline.main}</Card.Title>
<Card.Text>{a.abstract}</Card.Text>
<Button
variant="primary"
onClick={() => (window.location.href = a.web_url)}
>
{t("Go")}
</Button>
</Card.Body>
</Card>
))}
</div>
);
}
export default SearchPage;
This is where we create a search form with the keyword field used for searching the API. When the use clicks Search, then it will search the New York Times API for articles using the keyword. We use Formik to handle the form value changes and make the values available in the e
object in the handleSubmit
parameter for us to use. We use React Bootstrap for the buttons, form elements, and the cards. After clicking Search, the articles
variable get set and load the cards for the articles.
We use the Trans
component provided by react-i18next
for translating text that have some dynamic components, like in the example above. We have a variable in the text for the number of results. Whenever you have something like, you wrap it in the Trans
component and then pass in the variables like in the example above by passing in the variables as props. Then you will show the variable in the text between the Trans
tags. We we will also make interpolation available in the translations by putting “There are <1>{{count}}</1> results.”
in the English translation and “Il y a <1>{{count}}</1> résultats.”
in French translation. The 1
tag corresponds to the strong
tag. The number in this case is arbitrary. As long as the pattern consistent with the component’s pattern, it will work, so strong
tag in this case should always be 1
in the translation string.
To add the translations mentioned above along with the rest of the translations, create a file called translations.js
and add:
const resources = {
en: {
translation: {
"New York Times App": "New York Times App",
arts: "Arts",
automobiles: "Automobiles",
books: "Books",
business: "Business",
fashion: "Fashion",
food: "Food",
health: "Health",
home: "Home",
insider: "Inside",
magazine: "Magazine",
movies: "Movies",
national: "National",
nyregion: "New York Region",
obituaries: "Obituaries",
opinion: "Opinion",
politics: "Politics",
realestate: "Real Estate",
science: "Science",
sports: "Sports",
sundayreview: "Sunday Review",
technology: "Technology",
theater: "Theater",
tmagazine: "T Magazine",
travel: "Travel",
upshot: "Upshot",
world: "World",
Search: "Search",
numResults: "There are <1>{{count}}</1> results.",
Home: "Home",
Search: "Search",
Language: "Language",
English: "English",
French: "French",
Keyword: "Keyword",
Go: "Go",
Section: "Section",
},
},
fr: {
translation: {
"New York Times App": "App New York Times",
arts: "Arts",
automobiles: "Les automobiles",
books: "Livres",
business: "Entreprise",
fashion: "Mode",
food: "Aliments",
health: "Santé",
home: "Maison",
insider: "Initiée",
magazine: "Magazine",
movies: "Films",
national: "Nationale",
nyregion: "La région de new york",
obituaries: "Notices nécrologiques",
opinion: "Opinion",
politics: "Politique",
realestate: "Immobilier",
science: "Science",
sports: "Des sports",
sundayreview: "Avis dimanche",
technology: "La technologie",
theater: "Théâtre",
tmagazine: "Magazine T",
travel: "Voyage",
upshot: "Résultat",
world: "Monde",
Search: "Search",
numResults: "Il y a <1>{{count}}</1> résultats.",
Home: "Page d'accueil",
Search: "Chercher",
Language: "La langue",
English: "Anglais",
French: "Français",
Keyword: "Mot-clé",
Go: "Aller",
Section: "Section",
},
},
};
export { resources };
We have the static text translations, and the interpolated text we mentioned above in this file.
Finally, we create the top bar by creating TopBar.js
and add:
import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import NavDropdown from "react-bootstrap/NavDropdown";
import "./TopBar.css";
import { withRouter } from "react-router-dom";
import { useTranslation } from "react-i18next";
function TopBar({ location }) {
const { pathname } = location;
const { t, i18n } = useTranslation();
const changeLanguage = lng => {
localStorage.setItem("language", lng);
i18n.changeLanguage(lng);
};
return (
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">{t("New York Times App")}</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/" active={pathname == "/"}>
{t("Home")}
</Nav.Link>
<Nav.Link href="/search" active={pathname.includes("/search")}>
{t("Search")}
</Nav.Link>
<NavDropdown title={t("Language")} id="basic-nav-dropdown">
<NavDropdown.Item onClick={() => changeLanguage("en")}>
{t("English")}
</NavDropdown.Item>
<NavDropdown.Item onClick={() => changeLanguage("fr")}>
{t("French")}
</NavDropdown.Item>
</NavDropdown>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
export default withRouter(TopBar);
We use the NavBar
component provided by React Boostrap and we add a drop down for users to select a language and when they click those items, they can set the language. Notice that we wrapped the TopBar
component with the withRouter
function so that we get the current route’s value with the location
prop, and use it to set which link is active by setting the active
prop in the Nav.Link
components.
Finally, we replace the existing code in index.html
with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React New York Times App</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
to add the Bootstrap CSS and change the title of the app.