Tutoriels//15 min de lecture

Localisation React Native avec Expo — Publiez votre application dans 50+ langues

Eray Gündoğmuş
Partager

Les applications mobiles vivent sur l'appareil de l'utilisateur, pas sur un serveur que vous contrôlez. Cette seule différence rend la localisation dans React Native fondamentalement différente de la localisation web. Vous devez gérer l'accès hors ligne, les changements de locale de l'appareil, les métadonnées de l'app store dans plusieurs langues, et les mises à jour OTA des traductions — tout en maintenant une taille de bundle raisonnable.

Ce guide vous accompagne dans la localisation d'une application React Native avec Expo, de la détection de la locale de l'appareil à la livraison de traductions OTA prête pour la production. Nous utiliserons expo-localization pour les paramètres de l'appareil, i18next pour l'exécution des traductions, et le CDN de Better i18n pour la livraison des traductions. Pour les équipes qui construisent également un front end web, notre guide complet Next.js i18n pour 2026 couvre la pile web équivalente.

Pourquoi le i18n mobile est différent

Avant de plonger dans le code, comprenons pourquoi la localisation mobile présente des défis uniques :

1. Offline First

Les applications web peuvent récupérer les traductions à chaque requête. Les applications mobiles ne peuvent pas supposer la disponibilité du réseau. Vos traductions doivent être intégrées dans l'application comme solution de secours, avec des mises à jour OTA qui récupèrent du contenu frais quand la connectivité le permet.

2. Locale de l'appareil

Sur le web, vous détectez la locale à partir des URLs, des cookies ou des en-têtes Accept-Language. Sur mobile, l'appareil possède une locale système que l'utilisateur définit dans les paramètres de son téléphone. Votre application devrait respecter cette préférence par défaut, avec un sélecteur de langue optionnel intégré à l'application.

3. Taille du bundle

Les applications web peuvent diviser les traductions par locale et les charger à la demande. Les applications mobiles livrent un seul binaire. Chaque langue que vous intégrez augmente la taille de votre application. Pour 50+ langues, cela peut ajouter plusieurs mégaoctets à votre téléchargement. C'est l'un des défis fondamentaux abordés dans notre guide plus large sur la localisation d'applications mobiles, qui compare les approches selon les frameworks.

4. Métadonnées de l'App Store

Google Play et l'App Store exigent des métadonnées localisées (titre, description, captures d'écran) pour chaque langue prise en charge. Il s'agit d'un flux de travail distinct des traductions intégrées à l'application. Sur Android, notre guide de localisation Android couvre en détail le flux de travail des métadonnées du Play Store.

5. Support RTL

L'arabe, l'hébreu, le persan et l'ourdou nécessitent une mise en page de droite à gauche (RTL). React Native dispose d'un support RTL intégré via I18nManager, mais vous devez le gérer explicitement — inversion de mise en page, alignement du texte et icônes directionnelles.

Configuration du projet

Commencez avec un nouveau projet Expo ou ajoutez-le à un existant :

npx create-expo-app my-localized-app
cd my-localized-app

Installez les dépendances :

npx expo install expo-localization
npm install i18next react-i18next @better-i18n/react-native

Ce que fait chaque package :

PackageObjectif
expo-localizationDétection de la locale de l'appareil et préférences de formatage calendrier/nombre
i18nextExécution des traductions (résolution des clés, interpolation, pluralisation)
react-i18nextLiaisons React pour i18next (hooks, composants, contexte)
@better-i18n/react-nativeLivraison CDN, mise en cache hors ligne et mises à jour OTA pour Better i18n

1. Détection de la locale de l'appareil

La première étape est de détecter la langue préférée de l'utilisateur. Expo fournit expo-localization pour cela :

// lib/i18n/locale.ts
import { getLocales, getCalendars } from "expo-localization";

export function getDeviceLocale(): string {
  const locales = getLocales();

  if (locales.length === 0) {
    return "en";
  }

  // La première locale est la langue préférée de l'utilisateur
  const preferred = locales[0];

  // Retourner le code de langue (ex. : "en", "fr", "tr")
  // Utiliser languageTag pour la locale complète (ex. : "en-US", "fr-FR")
  return preferred.languageCode ?? "en";
}

export function getDeviceRegion(): string | null {
  const locales = getLocales();
  return locales[0]?.regionCode ?? null;
}

export function isRTL(): boolean {
  const locales = getLocales();
  return locales[0]?.textDirection === "rtl";
}

Important : getLocales() retourne un tableau ordonné des langues préférées de l'utilisateur. Sur iOS, cela vient de Réglages → Général → Langue et région. Sur Android, cela vient de Paramètres → Système → Langue. Utilisez toujours le premier élément comme préférence principale.

2. Configurer i18next

Configurez i18next avec la détection de la locale de l'appareil et la livraison CDN de Better i18n :

// lib/i18n/config.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { createBetterI18nBackend } from "@better-i18n/react-native";
import { getDeviceLocale } from "./locale";

// Traductions de secours intégrées (livrées avec l'application)
import en from "../../locales/en.json";
import es from "../../locales/es.json";

const fallbackResources = {
  en: { translation: en },
  es: { translation: es },
};

const betterI18nBackend = createBetterI18nBackend({
  project: "acme/mobile-app",
  cacheTTL: 60 * 60 * 1000, // Mettre en cache les traductions pendant 1 heure
  offlineStorage: true,      // Persister dans AsyncStorage pour une utilisation hors ligne
});

i18n
  .use(betterI18nBackend)
  .use(initReactI18next)
  .init({
    lng: getDeviceLocale(),
    fallbackLng: "en",
    resources: fallbackResources, // Utilisé quand le CDN est inaccessible
    interpolation: {
      escapeValue: false, // React Native gère l'échappement
    },
    react: {
      useSuspense: true,
    },
  });

export default i18n;

Fonctionnement de la chaîne de secours :

  1. Récupération CDN — Essayer de charger les traductions depuis le CDN Better i18n
  2. Cache hors ligne — Si le CDN est inaccessible, utiliser les traductions en cache depuis AsyncStorage
  3. Ressources intégrées — Si aucun cache n'existe, utiliser les traductions livrées avec l'application

Cette stratégie à trois niveaux garantit que votre application dispose toujours de traductions, même au premier lancement sans internet.

3. Utilisation du hook de traduction

Utilisez le hook useTranslation dans vos composants :

// screens/HomeScreen.tsx
import { View, Text, StyleSheet } from "react-native";
import { useTranslation } from "react-i18next";

export function HomeScreen() {
  const { t } = useTranslation();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{t("home.title")}</Text>
      <Text style={styles.subtitle}>
        {t("home.welcome", { name: "Developer" })}
      </Text>
      <Text style={styles.count}>
        {t("home.itemCount", { count: 5 })}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20 },
  title: { fontSize: 24, fontWeight: "bold" },
  subtitle: { fontSize: 16, marginTop: 8 },
  count: { fontSize: 14, marginTop: 4, color: "#666" },
});

Structure du fichier de traduction :

{
  "home": {
    "title": "Bienvenue",
    "welcome": "Bonjour, {{name}} !",
    "itemCount_one": "{{count}} élément",
    "itemCount_other": "{{count}} éléments"
  }
}

Pluralisation : i18next gère la pluralisation automatiquement. Utilisez les suffixes _one, _other, _few, _many pour différentes formes du pluriel. C'est essentiel pour des langues comme l'arabe (6 formes du pluriel), le polonais (3 formes) et le russe (3 formes). Pour une exploration complète des règles de pluralisation dans toutes les langues, consultez notre guide dédié à la pluralisation.

4. Sélecteur de langue intégré

Bien que l'application devrait utiliser par défaut la locale de l'appareil, les utilisateurs souhaitent souvent la remplacer. Voici un composant sélecteur de langue :

// components/LanguagePicker.tsx
import { View, Text, TouchableOpacity, FlatList, StyleSheet } from "react-native";
import { useTranslation } from "react-i18next";
import AsyncStorage from "@react-native-async-storage/async-storage";

const LANGUAGES = [
  { code: "en", name: "English", nativeName: "English" },
  { code: "es", name: "Spanish", nativeName: "Español" },
  { code: "fr", name: "French", nativeName: "Français" },
  { code: "de", name: "German", nativeName: "Deutsch" },
  { code: "tr", name: "Turkish", nativeName: "Türkçe" },
  { code: "ja", name: "Japanese", nativeName: "日本語" },
  { code: "ko", name: "Korean", nativeName: "한국어" },
  { code: "ar", name: "Arabic", nativeName: "العربية" },
];

export function LanguagePicker() {
  const { i18n } = useTranslation();

  const handleLanguageChange = async (code: string) => {
    await i18n.changeLanguage(code);
    await AsyncStorage.setItem("userLanguage", code);
  };

  return (
    <FlatList
      data={LANGUAGES}
      keyExtractor={(item) => item.code}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={[
            styles.item,
            item.code === i18n.language && styles.selected,
          ]}
          onPress={() => handleLanguageChange(item.code)}
        >
          <Text style={styles.nativeName}>{item.nativeName}</Text>
          <Text style={styles.name}>{item.name}</Text>
        </TouchableOpacity>
      )}
    />
  );
}

const styles = StyleSheet.create({
  item: {
    flexDirection: "row",
    justifyContent: "space-between",
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: "#eee",
  },
  selected: { backgroundColor: "#f0f7ff" },
  nativeName: { fontSize: 16, fontWeight: "600" },
  name: { fontSize: 14, color: "#666" },
});

Persistance : Quand l'utilisateur sélectionne manuellement une langue, enregistrez-la dans AsyncStorage. Au lancement de l'application, vérifiez s'il existe une préférence enregistrée avant de revenir à la locale de l'appareil :

// Dans l'initialisation de votre configuration i18n
const savedLanguage = await AsyncStorage.getItem("userLanguage");
const initialLanguage = savedLanguage || getDeviceLocale();

5. Support de la mise en page RTL

Pour l'arabe, l'hébreu et d'autres langues RTL, React Native nécessite une configuration explicite :

// lib/i18n/rtl.ts
import { I18nManager } from "react-native";
import * as Updates from "expo-updates";

const RTL_LANGUAGES = ["ar", "he", "fa", "ur"];

export async function handleRTL(languageCode: string) {
  const shouldBeRTL = RTL_LANGUAGES.includes(languageCode);
  const isCurrentlyRTL = I18nManager.isRTL;

  if (shouldBeRTL !== isCurrentlyRTL) {
    I18nManager.forceRTL(shouldBeRTL);
    I18nManager.allowRTL(shouldBeRTL);

    // Les changements RTL nécessitent un redémarrage de l'application pour prendre effet
    if (!__DEV__) {
      await Updates.reloadAsync();
    }
  }
}

Note importante : I18nManager.forceRTL() ne prend pas effet avant le redémarrage de l'application. En développement, vous pouvez utiliser le rechargement à chaud, mais en production vous devez appeler Updates.reloadAsync() pour appliquer le changement de direction de la mise en page.

Styles adaptés au RTL

Utilisez I18nManager.isRTL pour inverser conditionnellement les styles :

import { I18nManager, StyleSheet } from "react-native";

const styles = StyleSheet.create({
  row: {
    flexDirection: I18nManager.isRTL ? "row-reverse" : "row",
  },
  icon: {
    marginLeft: I18nManager.isRTL ? 0 : 8,
    marginRight: I18nManager.isRTL ? 8 : 0,
  },
});

Pour Expo SDK 50+, vous pouvez utiliser les propriétés start et end au lieu de left et right :

const styles = StyleSheet.create({
  container: {
    paddingStart: 16, // Gauche en LTR, Droite en RTL
    paddingEnd: 8,    // Droite en LTR, Gauche en RTL
  },
});

6. Mises à jour OTA des traductions

La fonctionnalité phare : mettre à jour les traductions sans soumettre une nouvelle version de l'application au store.

// lib/i18n/ota.ts
import { AppState } from "react-native";
import i18n from "./config";

export function setupOTAUpdates() {
  // Vérifier les nouvelles traductions quand l'application passe au premier plan
  const subscription = AppState.addEventListener("change", (state) => {
    if (state === "active") {
      refreshTranslations();
    }
  });

  return () => subscription.remove();
}

async function refreshTranslations() {
  try {
    // Le backend Better i18n gère l'invalidation du cache
    // Cela déclenche une nouvelle récupération si le TTL du cache a expiré
    await i18n.reloadResources(i18n.language);
  } catch (error) {
    // Échec silencieux — l'application continue avec les traductions en cache
    console.warn("Translation refresh failed:", error);
  }
}

Fonctionnement des mises à jour OTA avec Better i18n :

  1. Vous mettez à jour les traductions dans la plateforme Better i18n (via AI Drawer, l'éditeur ou MCP)
  2. Vous publiez les modifications sur le CDN
  3. La prochaine fois que le TTL du cache de l'application expire et que l'utilisateur ouvre l'application, les nouvelles traductions sont récupérées
  4. L'interface se met à jour automatiquement grâce au cycle de re-rendu de React

Pas de soumission à l'app store. Pas de mise à jour du binaire. Les traductions sont en ligne en quelques minutes. Pour un aperçu plus large de la façon dont la livraison OTA des traductions s'intègre dans une stratégie de localisation multiplateforme, notre guide sur les traductions OTA pour mobile et web couvre les décisions architecturales en profondeur.

7. Formatage des dates, nombres et devises

Utilisez expo-localization pour un formatage adapté à la locale :

// lib/i18n/formatting.ts
import { getLocales } from "expo-localization";

export function formatNumber(value: number): string {
  const locale = getLocales()[0]?.languageTag ?? "en-US";
  return new Intl.NumberFormat(locale).format(value);
}

export function formatCurrency(value: number, currency: string = "USD"): string {
  const locale = getLocales()[0]?.languageTag ?? "en-US";
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(value);
}

export function formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string {
  const locale = getLocales()[0]?.languageTag ?? "en-US";
  return new Intl.DateTimeFormat(locale, options).format(date);
}

export function formatRelativeTime(date: Date): string {
  const locale = getLocales()[0]?.languageTag ?? "en-US";
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });

  const diffMs = date.getTime() - Date.now();
  const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));

  if (Math.abs(diffDays) < 1) {
    const diffHours = Math.round(diffMs / (1000 * 60 * 60));
    return rtf.format(diffHours, "hour");
  }

  return rtf.format(diffDays, "day");
}

Note : Le moteur Hermes de React Native prend en charge les API Intl depuis Hermes 0.12. Si vous utilisez une version plus ancienne, ajoutez les polyfills intl-pluralrules et @formatjs/intl-numberformat.

8. Test des composants localisés

Testez vos composants avec différentes locales :

// __tests__/HomeScreen.test.tsx
import { render, screen } from "@testing-library/react-native";
import { I18nextProvider } from "react-i18next";
import i18n from "../lib/i18n/config";
import { HomeScreen } from "../screens/HomeScreen";

function renderWithI18n(component: React.ReactElement, locale: string = "en") {
  i18n.changeLanguage(locale);
  return render(
    <I18nextProvider i18n={i18n}>{component}</I18nextProvider>
  );
}

describe("HomeScreen", () => {
  it("renders in English", () => {
    renderWithI18n(<HomeScreen />, "en");
    expect(screen.getByText("Welcome")).toBeTruthy();
  });

  it("renders in Spanish", () => {
    renderWithI18n(<HomeScreen />, "es");
    expect(screen.getByText("Bienvenido")).toBeTruthy();
  });

  it("handles pluralization", () => {
    renderWithI18n(<HomeScreen />, "en");
    expect(screen.getByText("5 items")).toBeTruthy();
  });
});

9. Localisation de l'App Store

Votre fiche dans l'app store nécessite une localisation séparée. Expo prend en charge cela via app.json :

{
  "expo": {
    "name": "My App",
    "locales": {
      "es": "./locales/store/es.json",
      "fr": "./locales/store/fr.json",
      "de": "./locales/store/de.json",
      "tr": "./locales/store/tr.json",
      "ja": "./locales/store/ja.json"
    }
  }
}

Chaque fichier de locale contient les métadonnées de l'app store :

{
  "CFBundleDisplayName": "Mi Aplicación",
  "NSLocationWhenInUseUsageDescription": "Necesitamos tu ubicación para mostrarte tiendas cercanas."
}

Pour iOS, cela localise le nom de l'application sur l'écran d'accueil et les dialogues d'autorisation. Pour Android, vous devez gérer les métadonnées du Play Store via la Google Play Console ou Fastlane.

Liste de contrôle pour la production

Avant de publier votre application React Native localisée :

  • La détection de la locale par défaut fonctionne depuis les paramètres de l'appareil
  • Les traductions de secours sont intégrées dans le binaire de l'application
  • Le mode hors ligne fonctionne avec les traductions en cache
  • Les mises à jour OTA récupèrent les nouvelles traductions au premier plan
  • La mise en page RTL est testée avec l'arabe ou l'hébreu
  • La pluralisation fonctionne pour toutes les langues cibles
  • Le formatage des dates/nombres respecte la locale de l'appareil
  • Le sélecteur de langue persiste la préférence de l'utilisateur
  • Les métadonnées de l'app store sont traduites pour tous les marchés cibles
  • La taille du bundle est raisonnable (vérifier avec npx expo export)
  • Les performances — les traductions se chargent sans bloquer l'interface utilisateur

Conclusion

Localiser une application React Native avec Expo est plus complexe que la localisation web, mais les outils ont considérablement mûri. Avec expo-localization pour les préférences de l'appareil, i18next pour l'exécution des traductions, et Better i18n pour la livraison CDN et les mises à jour OTA, vous pouvez publier une application multilingue prête pour la production sans la complexité de gérer les fichiers de traduction manuellement.

L'idée clé : intégrez un minimum de traductions, livrez le reste depuis le CDN. Cela maintient la taille de votre application petite tout en prenant en charge 50+ langues grâce aux mises à jour OTA. Vos utilisateurs obtiennent des traductions dans leur langue, et vous n'avez pas besoin de soumettre une nouvelle version de l'application chaque fois qu'une traduction change.

Pour les équipes souhaitant étendre les mêmes principes aux applications Flutter multiplateformes, notre guide de localisation Flutter intl détaille la configuration équivalente pour l'écosystème Dart.


Ressources associées

Comments

Loading comments...