BowlerKit
Features

Internationalization (i18n)

8/20/2026

Comprehensive guide for handling multiple languages and localization in the Expo (React Native) application.

The boilerplate uses a robust internationalization (i18n) system powered by i18next and react-i18next. This approach provides a flexible, scalable, and industry-standard way to manage translations in React Native applications.

Overview

The localization system consists of four main components:

  1. JSON Locale Files: Translation strings stored as JSON objects in mobile-app/locales/.
  2. i18next Configuration: Initialization and setup in mobile-app/services/i18n.ts.
  3. State Management: usePreferencesStore (Zustand) manages the current language state and handles server synchronization.
  4. React Integration: The useTranslation hook provides access to translation functions within components.

Configuration

The i18n service is configured using i18next and integrates with expo-localization to detect the device's native language settings.

mobile-app/services/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { getLocales } from 'expo-localization';

import en from '../locales/en.json';
import id from '../locales/id.json';

const resources = {
    en: { translation: en },
    id: { translation: id },
};

const deviceLocales = getLocales();
const deviceLanguage = deviceLocales[0]?.languageCode ?? 'en';

i18n.use(initReactI18next).init({
    resources,
    lng: deviceLanguage, // Use device language as default
    fallbackLng: 'en',
    interpolation: {
        escapeValue: false, // React already escapes values
    },
});

export default i18n;

Locale Files

Translation files are located in mobile-app/locales/. The boilerplate supports:

  • en.json: English
  • id.json: Bahasa Indonesia

Example Structure

{
  "auth": {
    "login_title": "Welcome Back",
    "login_button": "Sign In"
  },
  "general": {
    "title": "General"
  }
}

Usage in Code

Using the Hook

The most common way to access translations is through the useTranslation hook.

import { useTranslation } from 'react-i18next';

export default function MyComponent() {
  const { t } = useTranslation();

  return (
    <Text>{t('auth.login_title')}</Text>
  );
}

With Interpolation

You can pass variables to your translations:

  • JSON: "welcome": "Welcome, {{name}}!"
  • Code: t('welcome', { name: 'John' })

With Pluralization

  • JSON:

    "item_count_one": "{{count}} item",
    "item_count_other": "{{count}} items"
  • Code: t('item_count', { count: 5 })

Formatting Dates and Numbers

For formatting dates according to the user's locale, the boilerplate uses the date-fns library.

import { formatDistanceToNow } from 'date-fns';
import { enUS, id } from 'date-fns/locale';
import { useTranslation } from 'react-i18next';

export function RelativeTime({ date }: { date: Date }) {
  const { i18n } = useTranslation();
  
  // Select the date-fns locale based on current language
  const locale = i18n.language === 'id' ? id : enUS;

  return (
    <Text>
      {formatDistanceToNow(date, { addSuffix: true, locale })}
    </Text>
  );
}

State Management & Persistence

The application's language preference is managed by usePreferencesStore and is persisted both locally (via AsyncStorage) and on the server.

Persistence Flow

  1. Selection: User selects a language in app/settings/general.tsx.
  2. Update: changeLanguage(lang) is called, which:
    • Updates i18next instance: i18n.changeLanguage(lang).
    • Updates Zustand store: setLanguageInStore(lang).
  3. Local Storage: The store automatically saves the preference to AsyncStorage.
  4. Server Sync: Changes are debounced and asynchronously synced to the backend API (PUT /user/preferences).
  5. Initialization: On app launch, usePreferencesStore.getState().initialize() restores the saved language.

Changing Language Programmatically

const { setLanguage } = usePreferencesStore();
const { i18n } = useTranslation();

const handleLanguageChange = async (lang: string) => {
  await i18n.changeLanguage(lang);
  await setLanguage(lang);
};

Adding a New Language

To add support for a new language (e.g., Japanese - ja):

  1. Create JSON File: Create mobile-app/locales/ja.json and copy keys from en.json.
  2. Register Resource: Import and add the new file to resources in mobile-app/services/i18n.ts.
  3. Update UI: Add the new language option to the BottomSheetModal in mobile-app/app/settings/general.tsx.
  4. Update Labels: Update getLanguageLabel and getFullLanguageLabel helpers in the settings screen.

Best Practices

  1. Nested Keys: Use logical nesting (e.g., auth.login.title) to keep the locale files organized.
  2. Fallback: Always provide a value in en.json (the fallback language) when adding new keys.
  3. No Hardcoding: Never use hardcoded strings for user-facing text. Even small labels should be localized.
  4. Context: If a word has multiple meanings depending on context, use specific keys like actions.save vs file.save.
  5. Reuse: Reuse common strings (like "Cancel", "Save", "Error") by placing them in a common or general section.