Written by Emmanuel Odioko✏️
Making sure your React app is available in several languages is one of the most significant ways to ensure people worldwide can access it in today's globally integrated society. Localization is helpful in this situation. Format.js, an open source toolkit, offers a selection of tools for localizing your React application.
Format.js enables you to translate the UI elements, messages, and dates in your app — making it easier for users outside of your country to use it. This tutorial will show you how to translate your React application using Format.js.
To follow along with this article, you'll need a basic knowledge of React, a terminal, and a code editor (I recommend VS Code). Let's get started!
Jump ahead:
- Getting started with React
- Creating functions to add to our React app
- Setting up Format.js in React
- Translating the
Cart
andProductItem
components - Switching between languages
Getting started with React
First, we use Create React App to create a React application:
npx create-react-app translate-app
Then, we can navigate to the newly created project after the installation with the cd translate-app/
command. Let's quickly set up a very simple ecommerce application where we have a bunch of products displayed in cards. Our simple app will also have a cart component that shows products we've added to our cart and allows us to remove those products.
Building the ProductCard
and Cart
components
Now, create a new ./src/components/ProductItem.jsx
file and enter the following code:
// ./src/components/ProductItem.jsx
const ProductItem = ({ product, addToCart }) => {
return (
<li className="product">
<div className="product-image img-cont">
<img src={product.thumbnail} alt="" />
</div>
<div className="product-details">
<h3>{product.title}</h3>
<p>{product.description}</p>
<span className="product-price">${product.price}</span>
</div>
<div className="product-actions">
<button
disabled={product?.isInCart}
onClick={() => addToCart(product)}
className="cta"
>
Add to cart
</button>
</div>
</li>
);
};
export default ProductItem;
The code above shows that this is a simple component with two props — product
and addToCart
. These components are for the product information displayed on the component and the function to add a specified product to the cart.
Now, to create the Cart
component, create a new ./src/components/Cart.jsx
file and enter the following code:
// ./src/components/Cart.jsx
const Cart = ({ cart, removeItem }) => {
return (
<div className="dropdown">
<div className="trigger group">
<button className="cta">Cart</button>
<span className="badge"> {cart?.length}</span>
</div>
<div className="content">
<aside className="cart">
<header className="cart-header">
<h2>Your Cart</h2>
<p>
You have <span>{cart.length}</span> items in your cart
</p>
</header>
<ul className="items">
{cart.map((item) => {
return (
<li tabIndex={0} key={item.id} className="item">
<div className="item-image img-cont">
<img src={item.thumbnail} alt={item.name} />
</div>
<div className="item-details">
<h3>{item.title}</h3>
<p className="item-price">${item.price}</p>
<button onClick={() => removeItem(item)} className="cta">
Remove
</button>
</div>
</li>
);
})}
</ul>
</aside>
</div>
</div>
);
};
export default Cart;
In this component, we also accept two props. The cart
prop contains the list of products that have been added to the cart. The removeItem
prop will be used to pass the removed item to the parent component.
Creating functions to add to our React app
Next, in ./src/App.js
, we’ll import our components and create a few functions to fetch products from dummyjson.com
. These will be used for our dummy products, to add products to the cart, and to remove products from the cart. First, enter the following code into the ./src/App.js
file:
// ./src/App.js
import "./App.css";
import { useEffect, useState } from "react";
import ProductItem from "./components/ProductItem";
import Cart from "./components/Cart";
// function to fetch products from dummyjson.com
const getProducts = async () => {
try {
const res = await fetch("https://dummyjson.com/products");
const data = await res.json();
return data;
} catch (error) {
console.log({
error,
});
return [];
}
};
function App() {
// set up state for products and cart
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
// function to add product to cart
const handleAddToCart = (product) => {
console.log("product", product);
setCart((cart) => {
return [...cart, product];
});
setProducts((products) => {
return products.map((p) => {
if (p.id === product.id) {
return {
...p,
isInCart: true,
};
}
return p;
});
});
};
// function to remove product from cart
const handleRemoveFromCart = (product) => {
setCart((cart) => {
return cart.filter((p) => p.id !== product.id);
});
setProducts((products) => {
return products.map((p) => {
if (p.id === product.id) {
return {
...p,
isInCart: false,
};
}
return p;
});
});
};
// fetch products on component mount
useEffect(() => {
getProducts().then((data) => {
setProducts(data.products);
});
}, []);
return (
<div className="app">
<header className="app-header">
<div className="wrapper">
<div className="app-name">Simple store</div>
<div>
<Cart cart={cart} removeItem={handleRemoveFromCart} />
</div>
</div>
</header>
<main className="app-main">
<div className="wrapper">
<section className="products app-section">
<div className="wrapper">
<header className="section-header products-header">
<div className="wrapper">
<h2 className="caption">Browse our products</h2>
<p className="text">
We have a wide range of products to choose from. Browse our
products and add them to your cart.
</p>
</div>
</header>
<ul className="products-list">
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
addToCart={handleAddToCart}
/>
))}
</ul>
</div>
</section>
</div>
</main>
</div>
);
}
export default App;
Awesome! For purposes of this article, the styles used to create this example project will be placed in the ./src/App.css
file of the project and are available on GitHub. You can also copy the styles from Pastebin or use your own styles.
Now, if we start our app by running the npm start
command, we should see something like this:
Nice! Next, we'll install and set up Format.js to start translating our React application.
Setting up Format.js in React
To get started setting up Format.js in React, use the following commands:
Install react-intl, a Format.js package for React:
npm i -S react react-intl
Once installed, we can access different helper
functions, components, and Hooks from Format.js that we can use in our React app.
Adding the IntlProvider
This component helps us add i18n functionality to our application by providing configurations like the current locale and set of translated strings/messages to the root of the application. This makes these configurations available to the different <Formatted />
components used throughout the application.
In our ./src/App.js
file, we'll wrap our app with the <IntlProvider />
component:
// ./src/App.js
// ...
import { IntlProvider } from "react-intl";
function App() {
// ...
// set up state for locale and messages
const [locale, setLocale] = useState("es");
const [messages, setMessages] = useState({
"app.name": "Tienda sencilla",
})
// ...
return (
<IntlProvider messages={messages} key={locale} locale={locale}>
{/* ... */}
</IntlProvider>
);
export default App;
Based on the code above, we import the IntlProvider
component from the react-intl
library. We also set up the state for locale
and messages
variables with the initial values of "es"
and {"app.name": "Tienda sencilla"}
, respectively.
The IntlProvider
component is then used to wrap the content of the App
component. We also pass messages
and locale
variables as props to the IntlProvider
. The key
prop is set to the locale
state to force React to re-render the component when the locale
value changes.
The IntlProvider
component provides internationalization support to the wrapped component by supplying it with translations for the current locale. By using the messages
and locale
state variables, the content of the wrapped component can be translated based on the selected locale. In this example, the app.name
message key is translated to "Tienda sencilla"
for the Spanish locale. Next, we'll use the <FormattedMesage />
component to see the translation in action.
Using the FormattedMessage
component
First, we'll use the FormattedMessage
component to translate the app name that appears in the app-header
in ./src/App.js
using the "app.name"
property from our messages
, as shown below:
// ./src/App.js
// ...
import { IntlProvider } from "react-intl";
function App() {
// ...
// set up state for locale and messages
const [locale, setLocale] = useState("es");
const [messages, setMessages] = useState({
"app.name": "Tienda sencilla",
})
// ...
return (
<IntlProvider messages={messages} key={locale} locale={locale}>
<div className="app">
<header className="app-header">
<div className="wrapper">
<div className="app-name">
<FormattedMessage id="app.name" defaultMessage={"Simple Store"} />
</div>
{/* ... */}
</div>
</header>
{/* ... */}
</div>
</IntlProvider>
);
export default App;
Here, we use the FormattedMessage
component from the react-intl
library to translate the app name that appears in the app-header
. The FormattedMessage
component takes two props: ID
and defaultMessage
. The ID
prop is used to identify the translation message in the messages
object. In this case, it is set to "app.name"
.
The defaultMessage
prop is used as a fallback message in case the translation for the specified ID
is not found. In this case, it is set to "Simple Store"
.
By using FormattedMessage
, the app name is translated based on the currently selected locale. When the locale
state variable changes, the IntlProvider
component will re-render and provide FormattedMessage
with the translations for the new locale. With that, we should have something like this:
Similarly, we can translate other text in our .src/App.js
file by adding more properties to the messages
object and using <FormattedMessage />
to display the values like this:
// ./src/App.js
// ...
import { IntlProvider } from "react-intl";
function App() {
// ...
// set up state for locale and messages
const [locale, setLocale] = useState("es");
const [messages, setMessages] = useState({
"app.name": "Tienda sencilla",
"app.description": "Una tienda sencilla con React",
"app.products.caption": "Explora nuestros productos",
"app.products.text":
"Tenemos una amplia gama de productos para elegir. Explora nuestros productos y agrégalos a tu carrito.",
})
// ...
return (
<IntlProvider messages={messages} key={locale} locale={locale}>
<div className="app">
{/* ... */}
<main className="app-main">
<div className="wrapper">
<section className="products app-section">
<div className="wrapper">
<header className="section-header products-header">
<div className="wrapper">
<h2 className="caption">
<FormattedMessage
id="app.products.caption"
defaultMessage={"Browse our products"}
/>
</h2>
<p className="text">
<FormattedMessage
id="app.products.text"
defaultMessage={"We have a wide range of products to choose from. Browse our products and add them to your cart."}
/>
</p>
</div>
</header>
<ul className="products-list">
{products.map((product) => (
<ProductItem
key={product.id}
product={product}
addToCart={handleAddToCart}
/>
))}
</ul>
</div>
</section>
</div>
</main>
</div>
</IntlProvider>
);
export default App;
Here, we see how to use <FormattedMessage />
to translate other text in the ./src/App.js
file by adding more properties to the messages
object. In this example, messages
contains properties for the app name, description, product caption, and product text.
The component displays these values by passing the relevant ID
to the FormattedMessage
, along with a default message
to display if a translation is unavailable. In this example, the component displays the product caption and text using the ID
corresponding to the property keys in the messages
object with a fallback text passed to defaultMessage
.
With that, we should have something like this:
Nice!
Translating the Cart
and ProductItem
components
We can take it even further by translating our Cart
and ProductItem
components. First, we need to add the translations in the messages
object in ./src/App.js
with the code below:
const [messages, setMessages] = useState({
"app.name": "Tienda sencilla",
"app.description": "Una tienda sencilla con React",
"app.products.caption": "Explora nuestros productos",
"app.products.text":
"Tenemos una amplia gama de productos para elegir. Explora nuestros productos y agrégalos a tu carrito.",
"app.cart": "Carrito",
"app.cart.title": "Tu carrito",
"app.cart.empty": "El carrito está vacío",
"app.cart.items":
"{count, plural, =0 {No tienes artículos} one {# articulo} other {# artículos }} en tu carrito",
"app.cart.remove": "Eliminar",
"app.cart.add": "Añadir a la cesta",
"app.item.price": "{price, number, ::currency/EUR}",
});
Here, we update the messages
object in ./src/App.js
by adding translations for the new components. The translations include strings such as "app.cart.title"
, "app.cart.empty"
, "app.cart.items"
, and "app.item.price"
. These translations will display the correct text in the Cart
and ProductItem
components.
Translate the Cart
Moving forward, we'll translate the Cart
component. Go ahead and add the code below into ./src/components/Cart.jsx
:
// ./src/components/Cart.jsx
import { FormattedMessage } from "react-intl";
const Cart = ({ cart, removeItem }) => {
return (
<div className="dropdown">
<div className="trigger group">
<button className="cta">
<FormattedMessage id="app.cart" defaultMessage="Cart" />
</button>
<span className="badge"> {cart?.length}</span>
</div>
<div className="content">
<aside className="cart">
<header className="cart-header">
<h2>
<FormattedMessage
id="app.cart.title"
defaultMessage="Your Cart"
/>
</h2>
<p>
<FormattedMessage
id="app.cart.items"
defaultMessage={`You have {count, plural, =0 {no items} one {one item} other {# items}} in your cart`}
values={{ count: cart.length }}
/>
</p>
</header>
<ul className="items">
{cart.map((item) => {
return (
<li tabIndex={0} key={item.id} className="item">
<div className="item-image img-cont">
<img src={item.thumbnail} alt={item.name} />
</div>
<div className="item-details">
<h3>{item.title}</h3>
<p className="item-price">
<FormattedMessage
id="app.item.price"
defaultMessage={`{price, number, ::currency/USD}`}
values={{ price: item.price }}
/>
</p>
<button onClick={() => removeItem(item)} className="cta">
<FormattedMessage
id="app.cart.remove"
defaultMessage="Remove"
/>
</button>
</div>
</li>
);
})}
</ul>
</aside>
</div>
</div>
);
};
export default Cart;
In the code above, we use FormattedMessage
to display the translated strings. The FormattedMessage
component takes an ID
prop corresponding to the message
object's translation key. It also takes a defaultMessage
prop, which displays a default value if the translation is not found.
For example, FormattedMessage
with id="app.cart.title"
and defaultMessage="Your Cart"
displays the text "Tu carrito"
in Spanish when the locale
is set to "es"
.
Take a close look at the code block below:
<p>
<FormattedMessage
id="app.cart.items"
defaultMessage={`You have {count, plural, =0 {no items} one {one item} other {# items}} in your cart`}
values={{ count: cart.length }}
/>
</p>
Here, "app.cart.items" corresponds to:
const [messages, setMessages] = useState({
"app.cart.items":
"{count, plural, =0 {No tienes artículos} one {# articulo} other {# artículos }} en tu carrito",
});
Note that the message
template uses count
as a variable representing the cart's number of items. It has three possible options depending on the value of count
:
-
=0 {no items}:
: If the value ofcount
is zero, this option is used, and the message will say"no items"
-
one {one item}:
: If the value ofcount
is one, this option is used, and the message will say"one item"
-
other {# items}:
: If the value ofcount
is anything other than zero or one, this option is used, and the message will say# items
, where#
is replaced with the value ofcount
This is for the message specified in defaultMessage
and the one specified in the messages
state. It is a message
string that includes a plural form in Spanish. The message contains a count
variable used to determine the correct plural form of the message. The syntax for the plural form is {count, plural, ...}
.
In this syntax, the first argument is the variable name (count
in this case), and the second is the type of pluralization used. Inside the plural argument, there are three cases:
-
=0 {No tienes artículos}
: This is the case when thecount
variable is equal to zero, which means the cart is empty. The message, in this case, isNo tienes artículos
(you have no items) -
one {# articulo}
: This is when thecount
variable equals one. The message, in this case, is"# articulo"
(one item) -
other {# artículos}
: The default case for all other counts. The message, in this case, is"# artículos"
(X items), where X is the value of thecount
variable
So, the full message in Spanish would be "No tienes artículos"
(you have no items) for an empty cart, "1 artículo"
(1 item) for a cart with one item, and "# artículos"
for carts with two or more items. This follows the Intl MessageFormat
that you can learn more about in the docs.
Translate the ProductItem
For the ProductItem
component in ./src/components/ProductItem.jsx
, add the following code:
// ./src/components/ProductItem.jsx
import { FormattedMessage } from "react-intl";
const ProductItem = ({ product, addToCart }) => {
return (
<li className="product">
<div className="product-image img-cont">
<img src={product.thumbnail} alt="" />
</div>
<div className="product-details">
<h3>{product.title}</h3>
<p>{product.description}</p>
<span className="product-price">
<FormattedMessage
id="app.item.price"
defaultMessage={`{price, number, ::currency/USD}`}
values={{ price: product.price }}
/>
</span>
</div>
<div className="product-actions">
<button
disabled={product?.isInCart}
onClick={() => addToCart(product)}
className="cta"
>
<FormattedMessage id="app.cart.add" defaultMessage="Add to Cart" />
</button>
</div>
</li>
);
};
export default ProductItem;
One thing we should take note of is the product-price
, as shown below:
<span className="product-price">
<FormattedMessage
id="app.item.price"
defaultMessage={`{price, number, ::currency/USD}`}
values={{ price: product.price }}
/>
</span>
"app.item.price" corresponds to:
const [messages, setMessages] = useState({
"app.item.price": "{price, number, ::currency/EUR}",
});
In the code above, "{price, number, ::currency/EUR}"
is a message format pattern used in the react-intl
library. It specifies how to format a price
variable as a number with a currency symbol of EUR
.
The curly braces {}
indicate placeholders in the message pattern. Meanwhile, price
is the name of the variable that should be substituted into the pattern. The number
keyword specifies that the variable should be formatted as a number.
The ::currency/EUR
option indicates that the number should be formatted as a currency value using the EUR
currency symbol. With all that done, our app should be completely translated: Awesome!
Switching between languages
One final and important feature to add to our application is a language switch feature. Follow along below.
Create locale JSON files
First, we’ll create the JSON files for each locale. For the Spanish locale, we create a new ./src/locales/es.json
file, as shown below:
{
"app.name": "Tienda sencilla",
"app.description": "Una tienda sencilla con React",
"app.products.caption": "Explora nuestros productos",
"app.products.text": "Tenemos una amplia gama de productos para elegir. Explora nuestros productos y agrégalos a tu carrito.",
"app.cart": "Carrito",
"app.cart.title": "Tu carrito",
"app.cart.empty": "El carrito está vacío",
"app.cart.items": "{count, plural, =0 {No tienes artículos} one {# articulo} other {# artículos }} en tu carrito",
"app.cart.remove": "Eliminar",
"app.cart.add": "Añadir a la cesta",
"app.item.price": "{price, number, ::currency/EUR}"
}
For the English locale, we create a ./src/locales/en.json file:
{
"app.name": "Simple store",
"app.description": "A simple store with React",
"app.products.caption": "Explore our products",
"app.products.text": "We have a wide range of products to choose from. Explore our products and add them to your cart.",
"app.cart": "Cart",
"app.cart.title": "Your Cart",
"app.cart.empty": "Your cart is empty",
"app.cart.items": "{count, plural, =0 {You have no items} one {You have one item} other {You have # items}} in your cart",
"app.cart.remove": "Remove",
"app.cart.add": "Add to cart",
"app.item.price": "{price, number, ::currency/USD}"
}
Bravo!
Dynamically import locale messages
Now, we'll use the useEffect
Hook to asynchronously load the messages for the selected locale when the component is mounted or when the locale
state is changed. Here's the code:
// ./src/App.js
// ...
function App() {
// ....
const [locale, setLocale] = useState("es");
const [messages, setMessages] = useState({
// ...
});
// ...
// function to dynamically import messages depending on locale
useEffect(() => {
import(`./locales/${locale}.json`).then((messages) => {
console.log({
messages,
});
setMessages(messages);
});
}, [locale]);
return (
// ...
)
};
export default App;
The code above uses dynamic imports to load the JSON file containing the messages for the selected locale. Once the messages are loaded, it sets the messages
state with the loaded messages. Finally, add a select
input to switch between the locales, as shown below:
// ./src/App.js
// ...
function App() {
// ...
return (
<IntlProvider messages={messages} key={locale} locale={locale}>
<div className="app">
<header className="app-header">
<div className="wrapper">
<div className="app-name">
<FormattedMessage id="app.name" defaultMessage={"Simple Store"} />
</div>
<div style={{ display: "flex", gap: "1rem" }}>
<Cart cart={cart} removeItem={handleRemoveFromCart} />
<select
onChange={(e) => {
setLocale(e.target.value);
}}
value={locale}
name="language-select"
id="language-select"
className="select-input"
>
<option value="es">Español</option>
<option value="en">English</option>
</select>
</div>
</div>
</header>
{/* ... */}
</div>
</IntlProvider>
);
}
export default App;
With that, we should have something like this: Awesome.
Conclusion
In this article, we learned how to use Format.js to translate your React application. Ultimately, by following the instructions in this article, you can quickly translate your React application using Format.js, expanding your audience and improving the UX of your app.
Whether you're building a simple store or a complex web app, internationalization is an important consideration that can significantly enhance the UX for non-native speakers of your language. By using the power of Format.js, you can easily add support for multiple languages and cultures to your React applications. Here is a link to a deployed preview of the example project built in this tutorial.
If you want to learn more about Format.js or internationalization in general, several resources are available online. Here are a few recommended ones:
- Format.js documentation: This is the official documentation for Format.js. It provides a comprehensive guide to using Format.js and details the features and APIs
- React Intl documentation: If you're specifically interested in using Format.js with React, this documentation provides a guide to using the React Intl library, built on top of Format.js
- Unicode Common Locale Data Repository: The Unicode CLDR provides a comprehensive set of locale data, including information about date and time formats, currency symbols, and other language-specific details. This is a valuable resource if you need to support multiple languages and cultures
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Top comments (1)
To translate your React app using Format.js and software localization, you need to follow these steps:
In this example, we import translation messages for English (en.json), French (fr.json), Spanish (es.json), and other languages as needed. The locale state manages the selected language. We use the select element to allow the user to switch between languages.
Create translation files for each supported language:
Wrap the components you want to translate with the FormattedMessage component from react-intl. Use the translated message keys from your JSON files to display the translated content. For example:
To manage the translation files efficiently, you can use software localization tools such as Phrase, Transifex, or Lokalise.