Tutorials//15 Min. Lesezeit

React Native Lokalisierung mit Expo — Veröffentliche deine App in 50+ Sprachen

Eray Gündoğmuş
Teilen

Mobile Apps leben auf dem Gerät des Nutzers, nicht auf einem Server, den du kontrollierst. Dieser eine Unterschied macht die Lokalisierung in React Native grundlegend anders als Web-Lokalisierung. Du musst Offline-Zugriff, Gerätesprach-Wechsel, App-Store-Metadaten in mehreren Sprachen und OTA-Übersetzungsupdates verwalten – und dabei die Bundle-Größe vernünftig halten.

Dieser Leitfaden führt dich durch die Lokalisierung einer React Native App mit Expo, von der Gerätesprach-Erkennung bis zur produktionsbereiten OTA-Übersetzungslieferung. Wir verwenden expo-localization für Geräteeinstellungen, i18next als Übersetzungs-Runtime und Better i18n's CDN für die Übersetzungslieferung. Für Teams, die auch ein Web-Frontend entwickeln, deckt unser vollständiger Next.js i18n-Leitfaden für 2026 den entsprechenden Web-Stack ab.

Warum Mobile i18n anders ist

Bevor wir in den Code eintauchen, lass uns verstehen, warum mobile Lokalisierung einzigartige Herausforderungen hat:

1. Offline First

Web-Apps können Übersetzungen bei jeder Anfrage abrufen. Mobile Apps können keine Netzwerkverfügbarkeit voraussetzen. Deine Übersetzungen müssen als Fallback mit der App gebündelt werden, wobei OTA-Updates bei verfügbarer Verbindung frische Inhalte laden.

2. Gerätesprache

Im Web erkennst du die Sprache aus URLs, Cookies oder Accept-Language-Headers. Auf mobilen Geräten hat das Gerät eine System-Locale, die der Nutzer in den Telefoneinstellungen festlegt. Deine App sollte diese Einstellung standardmäßig respektieren, mit einem optionalen In-App-Sprachenwähler.

3. Bundle-Größe

Web-Apps können Übersetzungen nach Sprache aufteilen und bei Bedarf laden. Mobile Apps werden als einzelnes Binary ausgeliefert. Jede gebündelte Sprache erhöht die App-Größe. Bei 50+ Sprachen kann das Megabytes zum Download hinzufügen. Dies ist eine der Kernherausforderungen, die in unserem umfassenderen Leitfaden zur mobilen App-Lokalisierung behandelt wird, der Ansätze über verschiedene Frameworks vergleicht.

4. App-Store-Metadaten

Google Play und der App Store erfordern lokalisierte Metadaten (Titel, Beschreibung, Screenshots) für jede unterstützte Sprache. Dies ist ein separater Workflow von In-App-Übersetzungen. Auf Android deckt unser Android-Lokalisierungsleitfaden den Play Store-Metadaten-Workflow ausführlich ab.

5. RTL-Unterstützung

Arabisch, Hebräisch, Persisch und Urdu erfordern ein Rechts-nach-Links (RTL)-Layout. React Native hat integrierte RTL-Unterstützung durch I18nManager, aber du musst es explizit behandeln – Layout-Spiegelung, Textausrichtung und gerichtete Icons.

Projekt-Setup

Starte mit einem neuen Expo-Projekt oder füge es einem bestehenden hinzu:

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

Installiere die Abhängigkeiten:

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

Was jedes Paket macht:

PaketZweck
expo-localizationGerätesprach-Erkennung und Kalender-/Zahlenformatierungseinstellungen
i18nextÜbersetzungs-Runtime (Schlüsselauflösung, Interpolation, Pluralisierung)
react-i18nextReact-Bindings für i18next (Hooks, Komponenten, Context)
@better-i18n/react-nativeCDN-Lieferung, Offline-Caching und OTA-Updates für Better i18n

1. Gerätesprach-Erkennung

Der erste Schritt ist die Erkennung der bevorzugten Sprache des Nutzers. Expo stellt dafür expo-localization bereit:

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

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

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

  // The first locale is the user's preferred language
  const preferred = locales[0];

  // Return the language code (e.g., "en", "fr", "tr")
  // Use languageTag for full locale (e.g., "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";
}

Wichtig: getLocales() gibt ein geordnetes Array der bevorzugten Sprachen des Nutzers zurück. Auf iOS kommt das aus Einstellungen → Allgemein → Sprache & Region. Auf Android aus Einstellungen → System → Sprache. Verwende immer das erste Element als primäre Präferenz.

2. i18next konfigurieren

Richte i18next mit Gerätesprach-Erkennung und Better i18n CDN-Lieferung ein:

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

// Bundled fallback translations (shipped with the app)
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, // Cache translations for 1 hour
  offlineStorage: true,      // Persist to AsyncStorage for offline use
});

i18n
  .use(betterI18nBackend)
  .use(initReactI18next)
  .init({
    lng: getDeviceLocale(),
    fallbackLng: "en",
    resources: fallbackResources, // Used when CDN is unreachable
    interpolation: {
      escapeValue: false, // React Native handles escaping
    },
    react: {
      useSuspense: true,
    },
  });

export default i18n;

Wie die Fallback-Kette funktioniert:

  1. CDN-Abruf — Übersetzungen vom Better i18n CDN laden
  2. Offline-Cache — Wenn CDN nicht erreichbar ist, gecachte Übersetzungen aus AsyncStorage verwenden
  3. Gebündelte Ressourcen — Wenn kein Cache vorhanden ist, die mit der App ausgelieferten Übersetzungen verwenden

Diese Drei-Stufen-Strategie stellt sicher, dass deine App immer Übersetzungen hat, auch beim ersten Start ohne Internet.

3. Translation-Hook verwenden

Verwende den useTranslation-Hook in deinen Komponenten:

// 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" },
});

Übersetzungsdatei-Struktur:

{
  "home": {
    "title": "Welcome",
    "welcome": "Hello, {{name}}!",
    "itemCount_one": "{{count}} item",
    "itemCount_other": "{{count}} items"
  }
}

Pluralisierung: i18next behandelt Pluralisierung automatisch. Verwende die Suffixe _one, _other, _few, _many für verschiedene Pluralformen. Dies ist entscheidend für Sprachen wie Arabisch (6 Pluralformen), Polnisch (3 Formen) und Russisch (3 Formen). Eine vollständige Erkundung der Pluralisierungsregeln über Sprachen hinweg findest du in unserem dedizierten Pluralisierungsleitfaden.

4. In-App-Sprachenwähler

Obwohl die App standardmäßig die Gerätesprache verwenden sollte, möchten Nutzer diese oft überschreiben. Hier ist eine Sprachenwähler-Komponente:

// 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" },
});

Persistenz: Wenn der Nutzer manuell eine Sprache auswählt, speichere sie in AsyncStorage. Beim App-Start prüfe auf eine gespeicherte Präferenz, bevor du auf die Gerätesprache zurückfällst:

// In your i18n config initialization
const savedLanguage = await AsyncStorage.getItem("userLanguage");
const initialLanguage = savedLanguage || getDeviceLocale();

5. RTL-Layout-Unterstützung

Für Arabisch, Hebräisch und andere RTL-Sprachen benötigt React Native eine explizite Konfiguration:

// 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);

    // RTL changes require an app restart to take effect
    if (!__DEV__) {
      await Updates.reloadAsync();
    }
  }
}

Kritischer Hinweis: I18nManager.forceRTL() tritt erst nach einem App-Neustart in Kraft. In der Entwicklung kannst du Hot Reload verwenden, in der Produktion musst du Updates.reloadAsync() aufrufen, um die Layout-Richtungsänderung anzuwenden.

RTL-bewusste Styles

Verwende I18nManager.isRTL, um Styles bedingt zu spiegeln:

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,
  },
});

Für Expo SDK 50+ kannst du die start- und end-Eigenschaften anstelle von left und right verwenden:

const styles = StyleSheet.create({
  container: {
    paddingStart: 16, // Left in LTR, Right in RTL
    paddingEnd: 8,    // Right in LTR, Left in RTL
  },
});

6. OTA-Übersetzungsupdates

Das Killer-Feature: Übersetzungen aktualisieren, ohne eine neue App-Version im Store einzureichen.

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

export function setupOTAUpdates() {
  // Check for new translations when the app comes to foreground
  const subscription = AppState.addEventListener("change", (state) => {
    if (state === "active") {
      refreshTranslations();
    }
  });

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

async function refreshTranslations() {
  try {
    // The Better i18n backend handles cache invalidation
    // This triggers a fresh fetch if the cache TTL has expired
    await i18n.reloadResources(i18n.language);
  } catch (error) {
    // Silent fail — the app continues with cached translations
    console.warn("Translation refresh failed:", error);
  }
}

Wie OTA mit Better i18n funktioniert:

  1. Du aktualisierst Übersetzungen in der Better i18n-Plattform (über AI Drawer, Editor oder MCP)
  2. Du veröffentlichst die Änderungen im CDN
  3. Beim nächsten Ablauf des Cache-TTL und wenn der Nutzer die App öffnet, werden frische Übersetzungen abgerufen
  4. Die UI aktualisiert sich automatisch durch Reacts Re-Render-Zyklus

Kein App-Store-Einreichung. Kein Binary-Update. Übersetzungen gehen in Minuten live. Für einen umfassenderen Blick darauf, wie OTA-Übersetzungslieferung in eine plattformübergreifende Lokalisierungsstrategie passt, behandelt unser Leitfaden zu OTA-Übersetzungen für Mobile und Web die Architekturentscheidungen ausführlich.

7. Datums-, Zahlen- und Währungsformatierung

Verwende expo-localization für locale-bewusste Formatierung:

// 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");
}

Hinweis: React Natives Hermes-Engine unterstützt Intl-APIs seit Hermes 0.12. Wenn du auf einer älteren Version bist, füge die Polyfills intl-pluralrules und @formatjs/intl-numberformat hinzu.

8. Lokalisierte Komponenten testen

Teste deine Komponenten mit verschiedenen Sprachen:

// __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. App-Store-Lokalisierung

Dein App-Store-Eintrag benötigt eine separate Lokalisierung. Expo unterstützt dies über 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"
    }
  }
}

Jede Locale-Datei enthält die App-Store-Metadaten:

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

Für iOS lokalisiert dies den App-Namen auf dem Home-Bildschirm und in Berechtigungsdialogen. Für Android musst du Play-Store-Metadaten über die Google Play Console oder Fastlane verwalten.

Produktions-Checkliste

Bevor du deine lokalisierte React Native App veröffentlichst:

  • Standard-Sprach-Erkennung funktioniert aus den Geräteeinstellungen
  • Fallback-Übersetzungen sind mit dem App-Binary gebündelt
  • Offline-Modus funktioniert mit gecachten Übersetzungen
  • OTA-Updates rufen frische Übersetzungen im Vordergrund ab
  • RTL-Layout ist mit Arabisch oder Hebräisch getestet
  • Pluralisierung funktioniert für alle Zielsprachen
  • Datums-/Zahlenformatierung respektiert die Gerätesprache
  • Sprachenwähler speichert die Nutzerpräferenz
  • App-Store-Metadaten sind für alle Zielmärkte übersetzt
  • Bundle-Größe ist angemessen (prüfe mit npx expo export)
  • Performance — Übersetzungen laden ohne die UI zu blockieren

Fazit

Die Lokalisierung einer React Native App mit Expo ist aufwändiger als Web-Lokalisierung, aber das Tooling hat sich erheblich weiterentwickelt. Mit expo-localization für Gerätepräferenzen, i18next als Übersetzungs-Runtime und Better i18n für CDN-Lieferung und OTA-Updates kannst du eine produktionsreife mehrsprachige App ausliefern, ohne die Komplexität der manuellen Verwaltung von Übersetzungsdateien.

Die entscheidende Erkenntnis: Minimale Übersetzungen bündeln, den Rest vom CDN liefern. Das hält die App-Größe klein, während 50+ Sprachen durch OTA-Updates unterstützt werden. Deine Nutzer bekommen Übersetzungen in ihrer Sprache, und du musst keine neue App-Version einreichen, jedes Mal wenn sich eine Übersetzung ändert.

Für Teams, die dieselben Prinzipien auf plattformübergreifende Flutter-Apps ausweiten möchten, führt unser Flutter intl-Lokalisierungsleitfaden durch das entsprechende Setup für das Dart-Ökosystem.


Verwandte Ressourcen

Comments

Loading comments...