Localisation is very important for every web application to bring a better user experience to all the users from around the globe and not lose your potential customers.
Today, we will look into how to integrate localisation into your React web app including how to handle language changes, formatting, as well as how to load your localisation file from CDN and etc.
First, let's start with a simple React app with language selections and a simple text display. You can clone or download the starter project to start with, or if you are impatient, you can get the completed code here.
Once you have the starter project, install the
react-i18next
dependency:
yarn add i18next react-i18next
Next, we need to setup react-i18next
in our app:
1. Create an i18n.config.js
inside the src
folder.
(Actually, you can name anything you want).
2. Add the following codes into the file.
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import en from './resources/en.json'; import zh from './resources/zh.json'; i18n .use(initReactI18next) // passes i18n down to react-i18next .init({ resources: { en: { translation: en, // Add translation for English }, zh: { translation: zh, // Add translation for Chinese }, }, fallbackLng: "en", interpolation: { escapeValue: false // No need to escape for react } });
3. Import the config in App.js
to initialize
// Other import import i18next from 'i18next'; import './i18n.config'; // Rest of the code
4. Create 2 new files in src/resources/en.json
and
src/resources/zh.json
and insert the content as below.
// src/resources/en.json { "welcome": "Welcome to the world of wonder" } // src/resources/zh.json { "welcome": "欢迎来到奇幻的世界" }
useTranslation
hook
To start translating, you can use useTranslation
hook provided by
react-i18next
. Now let's add our first translated text inside
section
tag in App.js
:
const App = () => { ++const { t, i18n } = useTranslation(); // Other code const handleLanguageSelect = (event) => { setSelectedLanguage(event.target.value); ++ i18n.changeLanguage(event.target.value); }; // Other code return ( <div className="h-screen flex justify-center items-center"> <div className="mx-auto bg-white p-4 rounded space-y-2"> // Other code <section> ++ <p>{t('welcome')}</p> </section> </div> </div> ); };
To change the language, you can always use the
changeLanguage
method from i18n
.
Now, you should be able to see the welcome
text is translated
based on the language you have selected.
Interpolation is a very useful and common feature that we will be used in translation, it allows you to add dynamic values to your translated text.
Let's say we want to display what language has been selected to the users.
const App = () => { // Other code return ( <div className="h-screen flex justify-center items-center"> <div className="mx-auto bg-white p-4 rounded space-y-2"> // Other code <section> <p>{t('welcome')}</p> ++ <p>{t('selectedLanguage', { language: selectedLanguage })}</p> </section> </div> </div> ); };
Then add the new translation text in src/resources/en.json
and
src/resources/zh.json
.
// src/resources/en.json { "selectedLanguage": "Your selected language is: {{language}}" } // src/resources/zh.json { "selectedLanguage": "您选择的语言是: {{language}}" }
The differences between the welcome
text and the
selectedLanguage
text is that we pass the
language
as the second parameter for t
method to
replace the placeholder in the translation text.
You should see something like this now.
For more details about interpolation, please check the official i18next interpolation documentation.
Another powerful feature that i18n
provides is you can format the
interpolation value. You can either use built-in formatting functions based on
Intl API
or build your own format function.
To have a better understanding of formatting, we will build a simple
uppercase
format function. Now, add a simple format method in
src/i18n.config.js
:
// Other codes i18n .use(initReactI18next) .init({ // Other codes interpolation: { escapeValue: false, ++ format: (value, format, lng) => { ++ if (format === 'uppercase') { ++ return value.toUpperCase(); ++ } ++ ++ return value; } } });
The function is really self-explanatory: if the format is uppercase, it will convert the value to uppercase, otherwise, it just returns the value.
Next, we can apply the uppercase format in our
selectedLanguage
text, let's update the text in
src/resources/en.json
and src/resources/zh.json
:
// src/resources/en.json { --"selectedLanguage": "Your selected language is: {{language}}" ++"selectedLanguage": "Your selected language is: {{language, uppercase}}" } // src/resources/zh.json { --"selectedLanguage": "您选择的语言是: {{language, uppercase}}", ++"selectedLanguage": "Your selected language is: {{language, uppercase}}" }
If you refresh the page now, you could see the language text has been converted to uppercase.
For more information about formatting, you can refer back to i18next formatting document.
Another common case that we definitely need when doing the translation is
pluralisation, and of course, i18next
got it for you too. We will
simply count how many times have we changed the language to demonstrate the
pluralisation in i18next
.
First, let's add the necessary text in src/resources/en.json
and
src/resources/zh.json
.
// src/resources/en.json { ++"numOfTimesSwitchingLanguage": "Your have switch language for {{count}} time", ++"numOfTimesSwitchingLanguage_zero": "Your have switch language for {{count}} time", ++"numOfTimesSwitchingLanguage_other": "Your have switch language for {{count}} times" } // src/resources/zh.json { ++"numOfTimesSwitchingLanguage": "您已更换了语言{{count}}次", ++"numOfTimesSwitchingLanguage_zero": "您已更换了语言{{count}}次", ++"numOfTimesSwitchingLanguage_plural": "您已更换了语言{{count}}次" }
Next, let's add the logic for counting the number of times we change the
language in App.js
.
const App = () => { const { t, i18n } = useTranslation(); const [selectedLanguage, setSelectedLanguage] = useState('en'); ++const [count, setCount] = useState(0); const handleLanguageSelect = (event) => { setSelectedLanguage(event.target.value); i18n.changeLanguage(event.target.value); ++ setCount(count => count + 1); }; return ( <div className="h-screen flex justify-center items-center"> <div className="mx-auto bg-white p-4 rounded space-y-2"> // Other codes <section> <p>{t('welcome')}</p> <p>{t('selectedLanguage', { language: selectedLanguage })}</p> ++ <p>{t('numOfTimesSwitchingLanguage', { count })}</p> </section> </div> </div> ); };
Let's refresh the page and see the outcome.
Great! we can see it changes to plurals once the count is more than 1. One
important note here is that we must use count
in order for
pluralisation to work.
You can do more complex pluralisation with i18next. Please feel free to check out the i18next plural document if you like to.
We have explored a couple of functionality of i18next, but there is one imperfection of our language changes app because it does not persist in the language selection after you refresh the page. Luckily, there is a i18next-browser-languagedetector plugin that going to help us achieve this. This plugin also helps us to detect languages from cookies, URL, browser settings.
Now, let's install the plugin.
yarn install i18next-browser-languagedetector
Then, add the plugin in src/i18n.config.js
:
// Other import ++import LanguageDetector from 'i18next-browser-languagedetector'; i18n .use(initReactI18next) ++.use(LanguageDetector) .init({ // Other configs });
Previously, we have set en
as the default language, but now we
want to use i18next.language
as the default language. Thus, let's
make some changes in App.js
.
const App = () => { const { t, i18n } = useTranslation(); --const [selectedLanguage, setSelectedLanguage] = useState('en'); ++const [selectedLanguage, setSelectedLanguage] = useState(i18n.language); const [count, setCount] = useState(0); // Other codes };
Alright, we can try to switch language to zh
and refresh the page
now. We should be able to see the default language stays at zh
.
If you're working with translators or to get better performance with CDN, you may want to load the translation texts from CDN instead of embedding everything in your code.
To do this, i18next-http-backend plugin is here to help us load the translation texts from the server easily.
First of all, let's install the plugin.
yarn install i18next-http-backend
Then, we need to change some of the configurations in
i18n.config.js
.
import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; --import en from './resources/en.json'; --import zh from './resources/zh.json'; import LanguageDetector from 'i18next-browser-languagedetector'; ++import HttpApi from 'i18next-http-backend'; i18n .use(initReactI18next) .use(LanguageDetector) ++.use(HttpApi) .init({ -- resources: { -- en: { -- translation: en, -- }, -- zh: { -- translation: zh -- }, -- }, fallbackLng: 'en', interpolation: { escapeValue: false, /** * Add interpolation format method to customize the formatting */ format: (value, format, lng) => { if (format === 'uppercase') { return value.toUpperCase(); } return value; }, }, ++ backend: { ++ loadPath: '/resources/{{lng}}.json', ++ }, });
Basically, we have made 2 changes: first, we tell the i18n
to use
the plugin and set the loadPath
to
/resources/{{lng}}.json
where lng
will be replaced
by the currently selected language code. Secondly, we also remove
resources
key because it is no longer being used as we are
loading the translation texts remotely.
Next, we need to move the src/resources
folder to
public/resources
folder so that the files can be loaded publicly.
Lastly, we need to wrap our application with Suspense
in
index.js
.
import React, { Suspense } from 'react'; // Other codes ReactDOM.render( <React.StrictMode> ++ <Suspense fallback="Loading..."> <App /> ++ </Suspense> </React.StrictMode>, document.getElementById('root') );
In practice, we may want to add Suspense
around a specific
component instead of at the root level. However, for demo purposes, we can
just wrap it in the root level.
If you refresh the app, it should work as normal.
Speaking of performance, there is another thing that you can do to speed up the loading time of your long translation texts if your application grows large, and that is namespacing. Namespacing allows you to load the small chunk/group of translation texts at a time when you need without preloading everything upfront which is a waste as well as slow down the first load time.
You just need to change your i18next-http-backend
loaded path to
use the namespaces and add the namespace as the argument to
useTranslation
.
// i18n.config.js { backend: { -- loadPath: '/resources/{{lng}}.json', ++ loadPath: '/resources/{{lng}}/{{ns}}.json', } } // App.js -- const { t, i18n } = useTranslation(); ++ const { t, i18n } = useTranslation('YOUR_NAMESPACE');
To understand more about namespacing, please check out i18next namespaces documentation.
i18next is a robust and sustainable solution to localise your web application. It has many features and plugins to achieve what you need for localisation.
In this post, we are just exploring the tip of the iceberg, there is still much more to learn. I will suggest having a read on react-i18next and i18next documentation to learn how to use them in depth.
I hope you learned something today and please feel free to give any feedback/comment below!
Copyright © 2024 Tek Min Ewe