Implementing Internationalisation in React Native Apps

A guide to implementing internationalisation (i18n) in React Native applications, including TypeScript integration, native platform considerations, and automated translation workflows.

Thursday February 27th 2025 | 7 min read

Implementing Internationalisation in React Native Apps blog post header image

Anything you can do to 'widen the funnel' of potential users for your mobile application, the better. Simply by serving your application in the user's native language, you're making it easier for them to use and more likely to convert. Implementing proper internationalisation (i18n) becomes crucial for maximising your success.

Easily expandable locale support

When building internationalised apps at Add Jam, one of our key objectives is to make adding new languages as frictionless as possible with minimum code changes. Ideally, we want to be able to add a new language by simply dropping a new translation file into our project without having to modify any code.

When internationalising a React Native app we want to:

  • Reduce the chance of errors when adding new languages
  • Make it easier for non-developers to contribute translations
  • Allow for dynamic loading of languages based on user preferences
  • Scale well as your app grows to support more languages

To achieve this, we'll use a dynamic import approach that automatically discovers and loads all available translation files. Let's see how we implement this in our React Native apps that are in production.

Setting up the foundation

Our i18n setup uses two primary dependencies:

# for managing translations
npm install i18n-js

# for detecting device language settings
npm install react-native-localize

Important Note: The dynamic import approach using require.context requires a special Metro configuration. You'll need to add unstable_allowRequireContext: true to your metro.config.js file:

 module.exports = {
   transformer: {
     unstable_allowRequireContext: true,
     // other transformer options...
   },
   // other metro config options...
 }

As the "unstable_" prefix suggests, this is an experimental feature in Metro that could change in future versions.

If you're comfortable with the idea of using an experimental feature, let's look at our complete implementation in config/i18n.ts:

import { I18n, TranslateOptions } from 'i18n-js'
import * as RNLocalize from 'react-native-localize'

// Dynamic import of translation files
let translations: Record<string, any> = {}
const localeFiles = require.context('../config/translations', true, /\.json$/)
localeFiles.keys().map((key: string) => {
  const newKey = key.replace('./', '').replace('.json', '')
  translations = {
    ...translations,
    [newKey]: localeFiles(key),
  }
})

// Types and fallbacks
type NestedKeys<T, K extends keyof T = keyof T> = K extends string
  ? T[K] extends Record<string, unknown>
    ? `${K}.${NestedKeys<T[K], keyof T[K]>}` | K
    : K
  : never

// NB we're assuming 'en' is always included
type Translations = (typeof translations)['en']
type TranslationKeys = NestedKeys<Translations>

const fallback = { languageTag: 'en' }
const { languageTag } =
  RNLocalize.findBestLanguageTag(Object.keys(translations)) || fallback

const i18n = new I18n()

i18n.translations = translations
i18n.locale = languageTag
i18n.enableFallback = true

// Helper function for type-safe translations
export function t(key: TranslationKeys, options?: TranslateOptions): string {
  return i18n.t(key, options)
}

export const locale = languageTag
export default i18n

Let's break down what's happening in our implementation:

  1. Dynamic Translation Loading: We use Metro's experimental require.context feature to dynamically load all JSON files in our translations directory. This automatically imports any new language files you add without code changes.

  2. Type Safety: The NestedKeys type creates a union type of all possible translation keys, including nested paths. This gives us type completion and compile-time checking when using translations.

  3. Locale Detection: We use react-native-localize to find the best matching language based on the user's device settings and if no match is found, we fallback to English ('en'):

const { languageTag } = RNLocalize.findBestLanguageTag(Object.keys(translations)) || fallback
  1. i18n Configuration: We configure the i18n instance with our translations and detected locale:

    i18n.translations = translations
    i18n.locale = languageTag
    i18n.enableFallback = true

    The enableFallback option means if a translation is missing in the selected language, it will fall back to English.

  2. Helper Function: Finally, we export a type-safe translation helper:

    export function t(key: TranslationKeys, options?: TranslateOptions): string

    This function provides type completion for translation keys and ensures we can't accidentally use invalid keys.

With this setup, adding a new language is as simple as creating a new JSON file in the translations directory (e.g., de.json for German). The dynamic import will automatically pick it up, and if a user's device is set to that language, your app will use those translations without any code changes.

Structured translation files

That's all the dry setup done, we can detect the users locale and load the correct translations. Now let's explore how we organise our strings.

Firstly we base everything off of our default locale which, as an English speaking development team, is en. Feel free to use a base locale that makes sense for your project.

We've internationalised loads of products across mobile and web and something to be careful of is making your strings an unmaintainable jumble. Its all too easy to end up with a translation file that has 1000s of strings in it, loads of duplicates and a maintenance headache.

What works for our projects is to have two main concepts:

  • A defaults key with commonly used strings ie cancel, update, save etc
  • Translations that mirror our application structure and naming conventions

This makes it easier to maintain consistency across the application and makes it easier to onboard new team members.

{
  "defaults": {
    "open": "Open",
    "close": "Close",
    "cancel": "Cancel",
    "ok": "OK",
    "yes": "Yes",
    "no": "No",
    "back": "Back",
    "next": "Next",
    "done": "Done",
    "save": "Save"
  },
  "components": {
    "UploadButton": {
      "start": "Start Upload",
      "pending": "Upload Pending",
      "error": "Upload Failed"
    },
  },
  "screens": {
    "Home": {
      "title": "Home",
      "description": "Welcome to the home screen"
    },
    "Settings": {
      "title": "Settings",
      "description": "Welcome to the settings screen"
    }
  },
  "utils": {
    "compressImage": {
      "success": "Image compressed successfully",
      "error": "Failed to compress image"
    }
  }
}

This structure makes it easier to:

  • Locate specific translations
  • Maintain consistency across the application
  • Onboard new team members

Using Translations in Components

Implementation in components is straightforward:

import { t } from 'app/config/i18n'
import { Text } from 'react-native'

const MyComponent = () => (
  <Text>
    {t("defaults.welcome")}
  </Text>
)

The t function is type-safe and will give you completion for the keys you can use in your translations.

Automating Translations

Now we have the technical setup we need to actually get the content translated. This can be hard and expensive to do manually, so we've automated the process as much as possible.

To speed up the translation process, we use OpenAI's API to automatically translate our English content to other languages. While machine translation isn't perfect, it provides a solid starting point that can be refined by native speakers as your product scales.

Native platform considerations

Our i18n setup for our React Native app is only translating the 'Javascript' parts of our application. This means to have fully translated content that we need to ensure that any content that is displayed in the native platform is also translated.

iOS (Xcode)

  1. Open your Xcode project
  2. Select your project in the navigator
  3. Click the "Info" tab
  4. Under "Localizations", click the + button
  5. Choose your target language
  6. Create a InfoPlist.strings file for each language:
// InfoPlist.strings (French)
"CFBundleDisplayName" = "Mon Application";

Android (Android Studio)

  1. Navigate to android/app/src/main/res
  2. Create a new values-[language code] folder (e.g., values-fr for French)
  3. Add a strings.xml file:
<!-- values-fr/strings.xml -->
<resources>
    <string name="app_name">Mon Application</string>
</resources>

Internationalisation best practices

  1. Use semantic keys: Instead of button.ok, use action.confirm
  2. Maintain context: Group translations in a way that matches your application structure (ie components, screens, utils)
  3. Test different languages: Regularly test your app in all supported languages
  4. Consider text length: Remember that translations may be longer or shorter than English, in React Native numberOfLines is your friend

Implementing internationalisation in React Native requires careful planning but pays dividends in accessibility and user satisfaction. Our TypeScript-first approach ensures type safety while maintaining developer productivity.

Need help implementing internationalisation in your React Native application? Add Jam specialises in building robust, scalable mobile applications. Contact us to discuss how we can help make your app global-ready.


This implementation has been battle-tested across multiple production applications built by Add Jam. For more insights into React Native development, check out other React Native blog posts such as Using Live Activities in a React Native App.

Michael Hayes's avatar

Michael Hayes

Co-founder

Recent case studies

Here's a look at some of products we've brought to market recently

Educational Intelligence: Money Matters

Educational Intelligence: Money Matters

How Add Jam partnered with Educational Intelligence to create Money Matters, a digital platform addressing the UK's financial literacy crisis where 12.9 million adults struggle with money management.

Great Glasgow Coffee Run

Great Glasgow Coffee Run

Celebrating Glasgow's vibrant coffee culture and running community through an interactive digital experience that maps out the perfect coffee-fuelled running route through the city.

One Walk A Day

One Walk A Day

During lockdown we rapidly prototyped a health and wellbeing app using React Native then expanded on the concept and redeveloped using SwiftUI

We take products from an idea to revenue

Add Jam is your plug in team of web and mobile developers, designers and product managers. We work with you to create, ship and scale digital products that people use and love.

Hello, let's chat 👋