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:
Cart
and ProductItem
components
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.
ProductCard
and Cart
componentsNow, 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.
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.
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.
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.
FormattedMessage
componentFirst, 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!
Cart
and ProductItem
componentsWe 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.
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 of count
is zero, this option is used, and the message will say "no items"
one {one item}:
: If the value of count
is one, this option is used, and the message will say "one item"
other {# items}:
: If the value of count
is anything other than zero or one, this option is used, and the message will say # items
, where #
is replaced with the value of count
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 the count
variable is equal to zero, which means the cart is empty. The message, in this case, is No tienes artĂculos
(you have no items)one {# articulo}
: This is when the count
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 the count
variableSo, 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.
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!
One final and important feature to add to our application is a language switch feature. Follow along below.
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!
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.
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:
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]