When building web applications, it is easy to focus solely on your immediate audience. However, the internet has no borders, and your app may reach users from different cultures, languages, and regions. This is where internationalization comes in.
What is internationalization?
Internationalization, often shortened to I18n (which is related to the number of letters between “i” and “n”), is a technical approach to the translation of your app into different languages. Adding internationalization means preparing your app to receive different languages while maintaining the structure: the codebase will support language changes without major interference.
This process is essential for apps with a global user base. Without internationalization, you might end up hardcoding strings and formats, creating an unnecessary extra level of work, and likely resulting in poor user experiences.
Internationalization versus localization
These terms might come across as similar, and you might even have mistaken them for each other once or twice. So, let’s take a minute to separate and explain them.
Internalization will do the technical work, turning your app translatable. You will prepare translations, and the work will be done. However, localization will be the adaptation and formatting you will need for different audiences. While internalization is technical, localization is the adjustment of content to a particular culture.
Locale
A locale is responsible for telling us the linguistic and cultural expectations for a region and is represented by a “locale code”. The code is divided into the representation of the language first, followed by the representation of the region, and separated by a hyphen. For example:
enEnglishen-USEnglish – United Statesen-GBEnglish – United Kingdom
This will be important to guarantee the correct adaptation to the region and its culture.
Internationalization in React
There are different libraries that will help with internationalization in React. Most of them will follow a similar approach, so we will use a specific one for this post. However, feel free to explore others!
Format.JS and React-intl
Today, we will use React-intl to talk more about internationalization. It is a dependency of Format.JS, which is a set of JavaScript libraries for internationalization. It is responsible for adding your translations and formatting whatever is necessary according to the language provided (numbers, dates, and strings).
It will format text with variables (more on the next topic), and be responsible for formatting numbers (decimals, currencies, unities, percentages), as well as dates.
Let’s review some basic concepts to get started.
Translating text
Before translations are displayed on the screen, we must provide our text in the chosen languages. There is a message syntax we must follow when writing our text. Our library will handle strings; however, depending on the context, we might need to replace some of them.
Any replacement will be called “arguments” and will be enclosed in curly braces ({ and }). Depending on the translation, each region and culture will require specific adaptations, such as those related to dates. In this case, we need formatted arguments. We will check out some examples later.
The translated text will be added to an object according to each locale:
const messages = {
"en-US": {
greeting: 'Hello {name}',
},
"pt-BR": {
greeting: 'Olá {name}',
},
}
However, when considering the app’s scalability, we can create multiple files, each for a different locale, containing all the necessary translations.
Structuring our code
First, we need to identify the user’s locale or allow the user to select their preferred locale. Based on the chosen option, we will configure our library to provide the appropriate information by setting the locale, supplying the correct strings for that locale, and including any optional formats if we decide to add them. Don’t worry; we will clarify this further as we proceed.
React-intl in Action
Assuming you already have a React app you’re planning on internationalizing, let’s start by adding the format.js lib:
npm i -S react react-intlNow we must add our IntlProvier to wrap our App. And, in doing so, there are two ways we can approach this: by using the declarative or imperative API (they are not mutually exclusive).
First, let’s start by writing some code to understand the setup, and then we will focus on specific languages.
First steps
The declarative API uses components. Let’s do it the declarative way first:
import { IntlProvider, FormattedMessage } from 'react-intl';
const messages = {
greeting: "Olá, {name}"
};
function App() {
return (
<IntlProvider
locale={"pt-BR"}
messages={messages}
defaultLocale="en"
>
<FormattedMessage
id="greeting"
defaultMessage="Hello {name}"
values={{ name: 'Miner' }}
/>
</IntlProvider>
);
}
export default App;Okay, so what do we have here? Our provider will receive the locale, the messages, and the default locale. And the component FormattedMessage?
id: the name of the key intended for translation (in this casegreeting)defaultMessage: ensures a message in cases of missing translations (e.g., languages not supported by the app’s scope).values: the values to each variable used in the original message
Other types of components are available, depending on what you must translate and format.
On the other hand, the imperative API uses a hook, which returns an object responsible for making the necessary changes. Let’s check it out:
import { IntlProvider, useIntl } from 'react-intl';
import messagesEn from './locales/en.json';
import messagesPt from './locales/pt.json';
const messages = {
greeting: "Olá, {name}"
};
function App() {
return (
<IntlProvider
locale={"pt-BR"}
messages={messages}
defaultLocale="en"
>
<Content />
</IntlProvider>
);
}
function Content() {
const intl = useIntl()
return (
<div>
{intl.formatMessage(
{
id: "greeting",
defaultMessage: "Hello {name}"
},
{ name: "Miner" }
)}
</div>
);
}
export default App;Now, by using the hook useIntl, some methods become available to us. Above, we can see the formatMessage.: a method responsible for working on text. There are other specific methods available that you can check in the documentation and use according to the type of data you must translate and format, as mentioned above for the declarative API.
There are other differences in usage. As you can see, we need to use the hook in a different component. While using the declarative API, we added the FormattedMessagecomponent directly on App. This happens so we can call the hook inside the provider.
Adding and supporting multiple languages
Let’s say your app supports 10 different languages. Should all those languages be available to each and every user? I think we can agree that this is not necessary. We could start by showing the defaultLocale we first set or even set the language according to the user’s browser.
But first things first: let’s add our translations. If you’re thinking about scalability, the best practice here would be individual JSON files for each language:
// src/lang/en-US.json
{
"greeting": "Hello, {name}!",
"date": "Today is {date, date, long}",
"price": "Total: {value, number, ::currency/USD}",
}// src/lang/pt-BR.json
{
"greeting": "Olá, {name}!",
"date": "Hoje é {date, date, long}",
"price": "Total: {value, number, ::currency/BRL}",
}Now let’s change our App to support these files:
import { IntlProvider } from 'react-intl';
import messagesEnUS from './lang/en-US.json';
import messagesPtBR from './lang/pt-BR.json';
const messages = {
'en-US': messagesEnUS,
'pt-BR': messagesPtBR
};
const userLocale = navigator.language.split('-')[0];
function App() {
return (
<IntlProvider
locale={userLocale}
messages={messages[userLocale] || messages.en}
defaultLocale="en"
>
<Content />
</IntlProvider>
);
}We now have messages tailored to the user’s locale based on the browser’s language. This way, we select the necessary languages for the user and make this process lighter for the app. Imagine downloading each translation file for an app with 50+ language options!
In a specific component inside Content, we will use the translations. It can be done both with the declarative API:
import { FormattedMessage } from 'react-intl';
function Welcome({ name }) {
return (
<div>
<h2>
<FormattedMessage id="greeting" values={{ name }} />
</h2>
</div>
);
}Or the imperative API:
import { useIntl } from 'react-intl';
function Welcome({ name }) {
const intl = useIntl();
return (
<h1>
{intl.formatMessage({ id: 'greeting' }, { name })}
</h1>
);
}That’s it! Your app now supports multiple languages. To enhance the user experience, how about adding a language selector so that users can choose from additional options?
Adding a language selector
export function LanguageSelector({ currentLocale }) {
const [locale, setLocale] = useState('en');
return (
<div >
<label htmlFor="language-select">
Choose language:
</label>
<select onChange={(e) => setLocale(e.target.value)} defaultValue='en'>
<option value='en'>EN</option>
<option value='pt'>PT</option>
</select>
</div>
);
}However, to make the chosen language available throughout the app, we need to make a few changes.
As you know by now, the state we set in this LanguageSelector is only available inside itself. This locale set here will not have repercussions in your app, unless we make use of the Context API, for example. Check it out:
import { createContext, useContext, useState } from 'react';
export const LocaleContext = createContext();
export function LocaleProvider({ children }) {
const userLocale = navigator.language.split('-')[0];
const [locale, setLocale] = useState(userLocale);
return (
<LocaleContext.Provider value={{ locale, setLocale }}>
{children}
</LocaleContext.Provider>
);
}And then we can wrap our App with the LocaleProvider as well, so the whole app will know what your user chose as a language option:
import { IntlProvider } from 'react-intl';
import messagesEnUS from './lang/en-US.json';
import messagesPtBR from './lang/pt-BR.json';
const messages = {
'en-US': messagesEnUS,
'pt-BR': messagesPtBR
};
function App() {
return (
<LocaleProvider>
<IntlProvider
locale={userLocale}
messages={messages[userLocale] || messages.en}
defaultLocale="en"
>
<Content />
</IntlProvider>
</LocaleProvider>
);
}The LanguageSelector component can now be added to the app’s header, for example, and your users would have free will to choose among the options.
⚠️ Interesting to see how the logic related to language selection can be separated into a custom hook responsible for managing the available languages. Give it a try! (Check our blog post on the topic)
This was just a bit of what React-intl can do for your app. To know more about other declarative API components or the hook useIntl options and other, more complex usages of Format.js and React-intl, feel free to check the documentation.
Ready for the world, one locale at a time
Internationalization enhances your app by making it accessible to different audiences worldwide. In React, libraries like React Intl and FormatJS make this process accessible, powerful, and aligned with native web standards.
Whether you’re building a landing page or a complex product with global reach, investing in internationalization early can save time, reduce bugs, and create a better experience for all users, regardless of where they are or what language they speak.
References
We want to work with you. Check out our Services page!

