Introduction
I always make sure to set up i18n (internationalization) in my apps even if there will probably be only one language used. The pain of adapting an app that wasn't thought to be multilingual in the first place is too big to procrastinate on it. Fortunately, I now have an easy setup, with auto-completion with typescript, autodetection of the user language and more.
Setup
To get the user language and handle our translations, we use 2 libraries:
react-native-localization: a toolbox with a simple API to get the user's locale, units and timezone. (nb: if you use expo, expo-localization is also a good library to use)
react-i18next: a powerful and popular framework for internationalization in React / React-native.
npm install react-native-localize react-18next i18next
After the installation, we need only one configuration file and JSON files that will contain our translations for each language.
import AsyncStorage from '@react-native-async-storage/async-storage'
import i18n, { LanguageDetectorAsyncModule } from 'i18next'
import { initReactI18next } from 'react-i18next'
import * as RNLocalize from 'react-native-localize'
import fr from './dictionaries/fr.json'
export const locales = {
fr: {
translation: fr,
},
}
export const DEFAULT_LOCALE = 'fr'
export const defaultLanguage =
RNLocalize.findBestAvailableLanguage(Object.keys(locales))?.languageTag || DEFAULT_LOCALE
export const currentLanguage = i18n.language || defaultLanguage
const useLanguageStorage: LanguageDetectorAsyncModule = {
type: 'languageDetector',
async: true,
detect: async (callback) => {
AsyncStorage.getItem('language').then((lang) => {
if (lang) return callback(lang)
})
},
init: () => null,
cacheUserLanguage: async (language: string) => {
AsyncStorage.setItem('language', language)
},
}
i18n
.use(useLanguageStorage)
.use(initReactI18next)
.init({
fallbackLng: defaultLanguage,
resources: locales,
react: {
useSuspense: false,
},
})
export default i18n
Then you will have to import that file in your App.tsx
and that's it.
Autocompletion with Typescript
One annoying thing when using translation keys is typing the paths again and again. To improve the developer experience I add a small definition to get suggestions with Typescript, it helps to avoid mistakes and reduce the coding time.
Create a file react-18next.d.ts at the root of your project directory or in a types
folder and add this code:
import 'react-i18next'
import fr from './i18n/dictionaries/fr.json'
declare module 'react-i18next' {
// and extend them!
interface Resources {
translation: typeof fr
}
}
With this setup, you should have something like this in VScode
Usage
Most common usage
Almost every time a text is used in a screen, you will add that text linked to a key in the JSON file, use the hook provided by i18next
which is called useTranslation
and use the key in your text
Access outside of components
Sometimes you may need to use your translations outside of your components in config files or whatever. To do that you can directly import your i18n file and access the t
method
import i18next from './i18n'
i18next.t('my.key')
Nested texts/multiple styles
One powerful feature of react-i18next is the Trans
component which can interpret custom tags in your translations to use a component for a part of your text.
"myKey":"it is my text <color>colored part</color> end of text"
<Text>
<Trans
components={{ color: <Text font="bold"
color="primary" /> }}
>
{t("path.to.my.key"}
</Trans>
<Text>
Update the language
To update the language, you use the same hook
const { t, i18n } = useTranslation('');
...
<Button onPress={()=>i18n.changeLanguage("de")>{i18n("switchLanguage")}<Button/>
Handle dates, currencies, API values
To handle dates, my go-to library is dayjs because it is a very popular library, complete and it has the same API as Momentjs (which I used by in the days).
import 'dayjs/locale/fr' // load on demand
dayjs.locale('fr') // use French locale globally
dayjs('2018-05-05').locale('fr-FR').format() // use on a specific text
For currencies you have multiple ways to tackle this problem, now I use Intl
const { i18n } = useTranslation()
<Text>{Intl.NumberFormat(i18n.language, {
style: 'currency',
currency: 'EUR'}).format(45012)}</Text>
On a final note, make sure that all your form/dynamic values are linked to a slug or a key that is related to your API. With that setup, add another language will be extremely fast instead of a tedious task.