i18n (internationalization) is the process of making an application work in different languages and cultures. This includes things like, translations of string resources through to to handling date formats as per the user’s language/culture settings, along with things like decimal separators and more.
There are several libraries available for helping with i18n coding but we’re going to focus on the react-i18next in this post, for no other reason that it’s by other teams in the company I’m working for at the moment.
Getting Started
The react-i18next website is a really good place to get started and frankly I’m very likely to cover much the same code here, so their website should be your first port of call.
If you’re want to instead follow thought the process here then, let’s create a simple React app with TypeScript (as is my way) using
npx create-react-app i18n-app --template typescript
Now add the packages react-i18next and i18next i.e.
npm install react-i18next i18next --save
In this Getting Started I’m going to also include the following libraries which will handle loading the translation files and more
npm install i18next-http-backend i18next-browser-languagedetector --save
Adding localized strings
We’re going to first show an example of embeding the string into a .ts file, however it’s much more likely we’ll want them in a separate file (or multiple files). Let’s get this started by creating a file named i18n1.ts.
In the file we’ll just write one string and here it is
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import LanguageDetector from "i18next-browser-languagedetector"; i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources: { en: { translation: { welcome: "code: Hello string en World" }, }, fr: { translation: { welcome: "code: Bonjour string fr World" } } }, debug: true, interpolation: { escapeValue: false, }, }); export default i18n;
As you can see we’re using the LanguageDetector to automatically set our resources. On my browser the language is set to en-GB so my expectation is to see the en strings as I’ve not listed en-GB specific strings.
IMPORTANT: Before we can use this, go to index.tsx and import this file import “./i18n1”; so the bundler includes it.
Display/using our localized strings
Now we’ll need to actually use our translation strings. react-i18next comes with hooks, HOC’s and standard JS type functions to interact with our translated strings. Let’s clear out most of the App.tsx and make it look like this
import React from "react"; import "./App.css"; import { useTranslation } from "react-i18next"; const lngs: any = { en: { nativeName: "English"}, fr: { nativeName: "French"}, } function App() { const { t, i18n } = useTranslation(); return ( <div className="App"> <div className="App-header"> {t("welcome")} <div> {Object.keys(lngs).map(lng => { return <button key={lng} style={{margin: "3px"}} onClick={() => i18n.changeLanguage(lng)} disabled={i18n.resolvedLanguage === lng}>{lngs[lng].nativeName}</button> })} </div> </div> </div> ); } export default App;
This code will simply display the English/French buttons to allow us to change the language as we wish. Notice, however, that if we add a new language, such as de: { nativeName: “German”} and no strings exist for that language, you’ll end up seeing the “key” for the string. We can solve this later.
At this point if all is working, you can start the application up and switch between the languages. You’ll see that the strings will be prefixed with code: just to make it clear where the strings are coming from, i.e. our code file.
Moving to resource type files (part 1)
As I mentioned, we probably don’t want to embed our string in code. It’s preferable to move them into their own .JSON files.
Create a folder within the src folder named locales (the names of the folders and files doesn’t really matter but it’s good to be consistent) and within that we’ll have one folder name en-GB and another named fr. So the English strings are specific to GB but the French covers all French languages locales.
Now in en-GB create the file translations.json which will look like this
{ "welcome": "src: Hello en-GB World" }
For the French translations, add translations.json to the fr folder and it should look like this
{ "welcome": "src: Bonjour fr World" }
Note the src: prefix is again, just there to allow us to see where our resources are coming from in this demo.
We now need to change our i18n.ts file to look like this
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import enGB from "../src/locales/en-GB/translation.json"; import fr from "../src/locales/fr/translation.json"; const resources = { en: { translation: enGB }, fr: { translation: fr } }; i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, debug: true, interpolation: { escapeValue: false, }, }); export default i18n;
So in this code we’reimporting the JSON and then assigning to the resources const. This is very similar to the other way we bought the resources into the i18n.ts file, just we’re importing via JSON files.
Moving to resource type files (part 2)
There’s another way to import the translated strings and that is to simply include the files in the public folder of our React application. So in the public folder add the same folders and files, i.e. locales folder with en-GB and fr folders with the same translation.json files as the last example. I’ve changed the src: prefix on the strings to public: again just so I can prove, for this demo, where the strings originate from.
In other words, here’s the en-GB translations.json file
{ "welcome": "public: Hello en-GB World" }
and the fr translations.json file
{ "welcome": "public: Bonjour fr World" }
Now back in our i18n.ts file change it to look like this
import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import Backend from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: { "en" : ["en-GB"] }, debug: true, interpolation: { escapeValue: false, }, }); export default i18n;
Fallback
My browser is setup as English (United Kingdom) which is en-GB and all works well. But what happens if we add a detect a language where we have no translation strings for? Well let’s try it by adding a German option to the lngs const in our App.tsx, so it looks like this
const lngs: any = { en: { nativeName: "English"}, fr: { nativeName: "French"}, de: { nativeName: "German"}, }
Now, what happens ? Well we’ve see the key for the string, which is probably not what we want in production, better to fall back to a known language. So we need to change the i18n.ts file and add a fallbackLng, I’ll include the whole i18n object so it’s obvious
i18n .use(LanguageDetector) .use(initReactI18next) .init({ resources, lng: "en-GB", fallbackLng: "en", debug: true, interpolation: { escapeValue: false, }, });
Now if our application encounters a locale it’s not setup for, it’ll default to the fallback language (in this case) English. We can also achieve this using
fallbackLng: { "default": ["en"] }, Using this syntax we can also map different languages to specific fallback languages, so for example we might map Swiss locale to map to use French or Italian. This is achieved by passing an array of fallback languages (as taken from the <a href="https://www.i18next.com/principles/fallback" rel="noopener" target="_blank">Fallback documentation</a> [code] fallbackLng: { "de-CH": ["fr", "it"], // French and Italian are also spoken in Switzerland },
Finally with regards fallback languages, we can write code to determine the translation to use, again the Fallback documentation has a good example of this, so I’d suggest checking that link out.
Code
Code for this post is available on github which includes i18n files 1-3 for each of these options for creating translations. just change the index.tsx to import the one you wish to use.