Skip to content
Colin Wren

Translating Your React Native App With i18n-js & Expo-Localization

JavaScript, Software Development5 min read

different languages

I’ll apologise in advance for the back and forth on the spelling of the word ‘locali(s|z)e’. I’m British but the library uses the American spelling.

During the development of JiffyCV I never really gave much thought to the localisation needs of an app until I went to submit to the different app stores and realised the scale of markets being supported.

Localisation is a tricky task to get right, especially in the domain JiffyCV exists in as it’s not as simple as translating text, but there are considerations into the format of data like dates (in the UK we use DD/MM/YYYY, in the US it’s MM/DD/YYYY).

Localisation also covers the names of entities and concepts. For instance in the UK the terms CV and Resume are interchangable and refer to a short summary of your skills (ideally it’s a single page) but in the US the term CV isn’t really used and a Resume can often span multiple pages.

With localisation having such an impact into how the app looks and how users understand the app’s concepts there needs to be an easy way to implement the logic to determine what to show based on the locale the user is using.

Luckily Expo provides a library called expo-localization to get the user’s locale from their device and this can be combined with i18n-js to make adding translations to your app really easy.

There are also some development patterns you can follow to integrate localisation into your code base that you should think about, even if you’re not adding localisation at this point in time.

Centralising your app’s text

Depending on the size of the app and the development approach taken, centralising your app’s text may take some time but it’s the important first step that will make localisation easier to implement.

In order to centralise your app’s text you’ll need to go through every part of your code base to identify text that is shown to the user and extract it out of a single object that can be imported to replace the current string with the value for the key that represents the text.

1// Original - Text hardcoded in component
2export function ExampleComponent() {
3 return (
4 <Text>This is some text</Text>
5 )
8// Refactored - Text is stored in a centralised object
9const keys = {
10 example: 'example-string',
13const appStrings = {
14 [keys.example]: 'This is some text',
17const exampleString = appStrings[keys.example];
19export function ExampleComponent() {
20 return (
21 <Text>{exampleString}</Text>
22 )
By moving the strings in the app to a centralised object we can prepare the app to handle localisation

When centralising the text in JiffyCV I chose to create a set of enums that represented the key for the text in a specific component and then created a heirarchy that represented the different screens and the components rendered on them.

This made it easier to understand the context of the key being used to retreive a localised string as I could clearly see the heirarchy and where that localised string would be used.

1enum OnboardingTabs = {
2 Welcome = 'welcome-tab',
3 Instructions = 'instructions-tab',
4 GetStarted = 'get-started-tab',
7enum WelcomeScreen = {
8 Welcome = 'welcome-screen-welcome',
11enum IntructionsScreen = {
12 HowItWorks = 'instructions-screen-how-it-works',
13 WhyWeBuiltIt = 'instructions-screen-why-we-built-it',
16enum GetStartedScreen = {
17 SignIn = 'get-started-screen-sign-in',
18 CreateAccount = 'get-started-screen-create-account',
21export const tokens = {
22 tabs: {
23 onboarding: OnboardingTabs,
24 },
25 screens: {
26 onboarding: {
27 welcome: WelcomeScreen,
28 instructions: InstructionsScreen,
29 getStarted: GetStartedScreen,
30 }
31 }
34// Usage
35const howItWorksString = strings[tokens.screens.onboarding.instructions.HowItWorks];
For large scale apps it might be easier to use a structured object to hold the keys used to look up strings so you have a semantic way of getting the correct key

During this process you might find that you have duplicated strings spread across a number of files and it can be tempting to remove this duplication. Before doing so, it’s important to think about the context that those duplicated strings appear in as when those strings are translated they might require different text to convey those contextual differences.

If you have dynamic strings that use variables such as those using template literals you can replace these with strings that use placeholders instead. The format for these placeholders is %{name} where name would be the name of the variable that you’ll be passing in via a context (more on that later).

Getting the user’s locale

The user’s locale is controlled by the user‘s device settings so we need to use the locale constant from expo-localization to read this value and use it in our code.

Other locale information that expo-localization provides that can be useful is the user’s timezone (via timezone constant) and if the language reads right to left or left to right (via isRTL constant).

On Android the user is able to change their locale on the fly and return to your app so if you use the constants when your app boots these may be out of date. The getLocalizationAsync function allows you to query the device for these updated locale settings and returns the same set of constants for you to use.

Setting up i18n-js to return localised strings

The Expo documentation uses i18n-js for storing the localised strings but if you’re using another library you should be able to integrate with this by passing in the locale value from expo-localization .

Returning localised strings using i18n-js is relatively simple. The i18n object has a translations object that takes a key in either a language code (e.g. en ) or language and region separated by a hyphen (e.g. en-GB ) format.

The value for the language in translations is an object with keys that match the keys we used when centralising our app’s text. As we already built an object as part of that centralisation we can just import and use that for the default language.

1import * as Localisation from 'expo-localization';
2import i18n from 'i18n-js';
3import { britishEnglish } from "./enGB";
4import { americanEnglish } from './enUS';
6i18n.translations = {
7 en: britishEnglish,
8 ['en-GB']: britishEnglish,
9 ['en-US']: americanEnglish,
12i18n.locale = Localisation.locale;
13i18n.fallbacks = true;
15export default i18n;
In this example en-GB and en-US are centralised objects like the ‘appStrings’ object in the previous example.

For additional locales we can then create copies of the existing centralised string object and update the values to be the localised versions of that string.

In order to set the locale to be used we pass the locale value from expo-localization to i18n.locale . This means that later when we want to get a string via the i18n.t() function that it returns strings for that locale.

Another setting to enable is the i18n.fallbacks value, setting this to true will allow keys that aren’t found in a locale to be pulled from locales that have them defined. This can be useful when you’re localising regional differences and don’t want to duplicate entries.

Using localised strings in your app with i18n-js

Once we have the user’s locale and we have i18n-js set up we can start adding localised versions of our app’s strings into our app.

In order to get a localised string we need to import our i18n instance and use the i18n.t() function, passing in the key for the string we want to get the value of.

The enum structure I set up for JiffyCV works wonders here as I can create a path through that to return the appropriate key. In order to make my life even easier I added an export of that structure to my i18n module allowing me to import both from one place.

1// Similar to the previous example but the keys are exported as part of the i18n module as tokens
2import i18n, { tokens } from './i18n';
4const exampleText = i18n.t(tokens.example);
6export function Example() {
7 return (
8 <Text>{exampleText}</Text>
9 )
By exporting the tokens as part of the i18n module we can handle all localisation needs with one import

Working with dynamic strings

If you converted your template literal strings in to strings with placeholders as part of the centralisation effort then you can set the value of the placeholders by passing in a context object.

This context object uses the placeholder name as a key and will set the substitute placeholder with the provided value when returning the localised string.

1// Definition in centralised strings object
2const strings = {
3 [tokens.hello]: 'Hello %{name}',
6// Usage in component
7import i18n, { tokens } from './i18n';
9export function Example({ name }) {
10 const exampleText = i18n.t(tokens.hello, { name: 'World!'});
11 return (
12 <Text>{exampleText}</Text>
13 )
By providing a context object you can use dynamic strings in your app while also being able to localise them

Advanced usage of i18n-js

The i18n-js library offers a lot more functionality that I’ve not personally used so won’t be covering but documentation can be found on the libraries’ Github.

When reading the documentation I found it easier to read the JavaScript spec files than the README as the README is very heavily geared towards using the library as part of a Ruby on Rails app but the tests are concise enough that they document the functionality well.


The below Snack shows using expo-localization and i18n to provide localisations for en-GB and en-US . If you run the Snack on your local device and use those locales you’ll see different text.

You can hardcode the locale the app uses (which might be easier than switching locales) by updating the value of i18n.locale in the i18n/index.ts file.


By using useful data structures and design patterns you can make localisation easy enough to implement and in doing so we vastly increase the range of markets the app can be made available in.

Even if you’re not actively looking to implement localisation into your app you can still refactor your app to support it without impacting your development approach.