38 Logging – Von console.log zu Production Monitoring

Eine React-Anwendung ohne Logging ist wie ein Flugzeug ohne Blackbox. Wenn etwas schiefgeht, bleibt nur raten. console.log() reicht für Quick-Debugging im Development, aber sobald die App in Production läuft, braucht man mehr: strukturierte Logs, automatisches Error-Tracking, Performance-Metriken und die Fähigkeit, Probleme zu reproduzieren, die nur bei bestimmten Nutzern auftreten.

38.1 Das Problem mit console.log

// Typischer Development-Code
function ProductList({ category }: { category: string }) {
  const [products, setProducts] = useState<Product[]>([]);
  
  console.log('ProductList rendered');  // ❌ Welche Kategorie?
  console.log('category:', category);   // ❌ Zeitstempel fehlt
  
  useEffect(() => {
    fetchProducts(category).then(data => {
      console.log('products:', data);    // ❌ Unstrukturiert
      setProducts(data);
    });
  }, [category]);
  
  return <div>{/* ... */}</div>;
}

Probleme: - Kein Kontext: Welcher User, welche Session, welcher Browser? - Keine Zeitstempel: Wann passierte was? - Kein Log-Level: Ist das Debug-Info oder ein kritischer Fehler? - Keine Struktur: Logs können nicht gefiltert, durchsucht oder aggregiert werden - Production-Leaks: console.log bleibt im Bundle, exposed möglicherweise sensible Daten

38.2 Strukturiertes Logging mit TypeScript

Strukturiertes Logging bedeutet: Jeder Log-Eintrag ist ein Objekt mit definierten Feldern, nicht nur ein String.

// Logger-Types definieren
type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogEntry {
  timestamp: string;
  level: LogLevel;
  message: string;
  context?: Record<string, unknown>;
  userId?: string;
  sessionId?: string;
  url?: string;
}

interface LoggerConfig {
  minLevel: LogLevel;
  enableConsole: boolean;
  enableRemote: boolean;
  remoteUrl?: string;
}

Die Struktur ermöglicht später Queries wie “Zeige alle Errors vom User 123 in den letzten 24h” – mit console.log unmöglich.

38.3 Custom Logger Implementation

Ein zentraler Logger kapselt die Logging-Logik und kann Environment-spezifisch konfiguriert werden.

// logger.ts
class Logger {
  private static instance: Logger;
  private config: LoggerConfig;
  private sessionId: string;
  
  private constructor(config: LoggerConfig) {
    this.config = config;
    this.sessionId = crypto.randomUUID();
  }
  
  static getInstance(config?: LoggerConfig): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger(config || {
        minLevel: import.meta.env.PROD ? 'warn' : 'debug',
        enableConsole: true,
        enableRemote: import.meta.env.PROD
      });
    }
    return Logger.instance;
  }
  
  private shouldLog(level: LogLevel): boolean {
    const levels: LogLevel[] = ['debug', 'info', 'warn', 'error'];
    const currentLevelIndex = levels.indexOf(this.config.minLevel);
    const messageLevelIndex = levels.indexOf(level);
    
    return messageLevelIndex >= currentLevelIndex;
  }
  
  private createEntry(
    level: LogLevel,
    message: string,
    context?: Record<string, unknown>
  ): LogEntry {
    return {
      timestamp: new Date().toISOString(),
      level,
      message,
      context,
      sessionId: this.sessionId,
      url: window.location.href,
      userId: this.getUserId() // Aus Auth-Context o.ä.
    };
  }
  
  private getUserId(): string | undefined {
    // Von AuthContext, localStorage, etc.
    return localStorage.getItem('userId') || undefined;
  }
  
  private output(entry: LogEntry): void {
    if (this.config.enableConsole) {
      this.logToConsole(entry);
    }
    
    if (this.config.enableRemote) {
      this.logToRemote(entry);
    }
  }
  
  private logToConsole(entry: LogEntry): void {
    const { level, message, context, timestamp } = entry;
    const style = this.getConsoleStyle(level);
    
    console[level](
      `%c[${timestamp}] ${level.toUpperCase()}: ${message}`,
      style,
      context || ''
    );
  }
  
  private getConsoleStyle(level: LogLevel): string {
    const styles = {
      debug: 'color: gray',
      info: 'color: blue',
      warn: 'color: orange; font-weight: bold',
      error: 'color: red; font-weight: bold'
    };
    return styles[level];
  }
  
  private async logToRemote(entry: LogEntry): Promise<void> {
    if (!this.config.remoteUrl) return;
    
    try {
      // Batch-Queue statt einzelner Requests
      await fetch(this.config.remoteUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(entry)
      });
    } catch (error) {
      // Fallback: Nur in console loggen, keine endlose Rekursion
      console.error('Failed to send log to remote:', error);
    }
  }
  
  // Public API
  debug(message: string, context?: Record<string, unknown>): void {
    if (!this.shouldLog('debug')) return;
    const entry = this.createEntry('debug', message, context);
    this.output(entry);
  }
  
  info(message: string, context?: Record<string, unknown>): void {
    if (!this.shouldLog('info')) return;
    const entry = this.createEntry('info', message, context);
    this.output(entry);
  }
  
  warn(message: string, context?: Record<string, unknown>): void {
    if (!this.shouldLog('warn')) return;
    const entry = this.createEntry('warn', message, context);
    this.output(entry);
  }
  
  error(message: string, error?: Error, context?: Record<string, unknown>): void {
    if (!this.shouldLog('error')) return;
    
    const entry = this.createEntry('error', message, {
      ...context,
      error: error ? {
        message: error.message,
        stack: error.stack,
        name: error.name
      } : undefined
    });
    
    this.output(entry);
  }
}

// Singleton-Export
export const logger = Logger.getInstance();

Verwendung in Komponenten:

import { logger } from './logger';

function ProductList({ category }: { category: string }) {
  const [products, setProducts] = useState<Product[]>([]);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    logger.debug('Fetching products', { category });
    
    fetchProducts(category)
      .then(data => {
        logger.info('Products loaded', { 
          category, 
          count: data.length 
        });
        setProducts(data);
      })
      .catch(error => {
        logger.error('Failed to fetch products', error, { category });
        setError(error);
      });
  }, [category]);
  
  if (error) {
    return <div>Error loading products</div>;
  }
  
  return <div>{/* ... */}</div>;
}

38.4 React-spezifische Logging-Patterns

38.4.1 Custom Hook für Component Lifecycle Logging

// useComponentLogger.ts
export function useComponentLogger(componentName: string) {
  const mountTimeRef = useRef<number>();
  const renderCountRef = useRef(0);
  
  // Mount
  useEffect(() => {
    mountTimeRef.current = performance.now();
    logger.debug(`${componentName} mounted`);
    
    return () => {
      const lifetime = performance.now() - (mountTimeRef.current || 0);
      logger.debug(`${componentName} unmounted`, {
        lifetime: `${lifetime.toFixed(2)}ms`,
        renderCount: renderCountRef.current
      });
    };
  }, [componentName]);
  
  // Render
  useEffect(() => {
    renderCountRef.current++;
    logger.debug(`${componentName} rendered`, {
      renderCount: renderCountRef.current
    });
  });
  
  // Error Logger
  const logError = useCallback((message: string, error?: Error, context?: Record<string, unknown>) => {
    logger.error(`${componentName}: ${message}`, error, context);
  }, [componentName]);
  
  // Performance Logger
  const logPerformance = useCallback((action: string, duration: number) => {
    logger.info(`${componentName} performance`, {
      action,
      duration: `${duration.toFixed(2)}ms`
    });
  }, [componentName]);
  
  return { logError, logPerformance };
}

// Verwendung
function ExpensiveComponent() {
  const { logError, logPerformance } = useComponentLogger('ExpensiveComponent');
  
  const handleExpensiveOperation = async () => {
    const start = performance.now();
    
    try {
      await someExpensiveOperation();
      const duration = performance.now() - start;
      logPerformance('expensiveOperation', duration);
    } catch (error) {
      logError('Expensive operation failed', error as Error);
    }
  };
  
  return <button onClick={handleExpensiveOperation}>Run</button>;
}

38.4.2 Performance Monitoring Hook

// usePerformanceLog.ts
export function usePerformanceLog(componentName: string, props?: Record<string, unknown>) {
  const renderCountRef = useRef(0);
  const lastRenderTimeRef = useRef(performance.now());
  
  useEffect(() => {
    renderCountRef.current++;
    const now = performance.now();
    const timeSinceLastRender = now - lastRenderTimeRef.current;
    
    if (timeSinceLastRender > 16.67) {  // Langsamer als 60fps
      logger.warn(`Slow render detected in ${componentName}`, {
        renderTime: `${timeSinceLastRender.toFixed(2)}ms`,
        renderCount: renderCountRef.current,
        props
      });
    }
    
    lastRenderTimeRef.current = now;
  });
}

// Verwendung
function SlowComponent({ data }: { data: LargeDataset }) {
  usePerformanceLog('SlowComponent', { dataSize: data.length });
  
  return <div>{/* Heavy rendering */}</div>;
}

38.5 Error Boundaries mit integriertem Logging

Error Boundaries sollten automatisch Fehler loggen – mit vollem Kontext.

// ErrorBoundary.tsx
interface Props {
  children: ReactNode;
  fallback?: (error: Error, reset: () => void) => ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }
  
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Umfangreiches Logging mit Kontext
    logger.error('React Error Boundary caught error', error, {
      componentStack: errorInfo.componentStack,
      errorBoundary: this.constructor.name,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: new Date().toISOString(),
      // Zusätzlicher App-State könnte hier hinzugefügt werden
    });
  }
  
  reset = () => {
    this.setState({ hasError: false, error: null });
    logger.info('Error Boundary reset');
  };
  
  render() {
    if (this.state.hasError && this.state.error) {
      return this.props.fallback?.(this.state.error, this.reset) || (
        <div>
          <h2>Something went wrong</h2>
          <button onClick={this.reset}>Try again</button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

38.6 Production Monitoring: Sentry Integration

Für Production braucht man externe Monitoring-Services. Sentry ist der Standard.

npm install @sentry/react
// main.tsx
import * as Sentry from '@sentry/react';

if (import.meta.env.PROD) {
  Sentry.init({
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    integrations: [
      new Sentry.BrowserTracing(),
      new Sentry.Replay({
        maskAllText: false,
        blockAllMedia: false,
      }),
    ],
    tracesSampleRate: 1.0,
    replaysSessionSampleRate: 0.1,
    replaysOnErrorSampleRate: 1.0,
  });
}

// Sentry Error Boundary verwenden
const SentryErrorBoundary = Sentry.withErrorBoundary(App, {
  fallback: <ErrorPage />,
  showDialog: true,
});

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <SentryErrorBoundary />
  </React.StrictMode>
);

Custom Logger mit Sentry-Integration:

// logger.ts erweitern
import * as Sentry from '@sentry/react';

class Logger {
  // ... vorheriger Code
  
  error(message: string, error?: Error, context?: Record<string, unknown>): void {
    if (!this.shouldLog('error')) return;
    
    const entry = this.createEntry('error', message, {
      ...context,
      error: error ? {
        message: error.message,
        stack: error.stack,
        name: error.name
      } : undefined
    });
    
    this.output(entry);
    
    // Zusätzlich an Sentry
    if (import.meta.env.PROD && error) {
      Sentry.captureException(error, {
        contexts: {
          custom: context
        },
        tags: {
          component: context?.component as string
        }
      });
    }
  }
  
  // Breadcrumbs für besseren Kontext
  info(message: string, context?: Record<string, unknown>): void {
    if (!this.shouldLog('info')) return;
    const entry = this.createEntry('info', message, context);
    this.output(entry);
    
    if (import.meta.env.PROD) {
      Sentry.addBreadcrumb({
        category: 'info',
        message,
        data: context,
        level: 'info'
      });
    }
  }
}

38.7 Environment-spezifische Konfiguration

Development vs. Production erfordert unterschiedliches Logging-Verhalten.

// config/logger.config.ts
export const loggerConfig: Record<string, LoggerConfig> = {
  development: {
    minLevel: 'debug',
    enableConsole: true,
    enableRemote: false,
  },
  staging: {
    minLevel: 'info',
    enableConsole: true,
    enableRemote: true,
    remoteUrl: import.meta.env.VITE_LOG_API_STAGING
  },
  production: {
    minLevel: 'warn',
    enableConsole: false,  // Keine console.logs in Production!
    enableRemote: true,
    remoteUrl: import.meta.env.VITE_LOG_API_PROD
  }
};

// Logger initialisieren
const env = import.meta.env.MODE as keyof typeof loggerConfig;
export const logger = Logger.getInstance(loggerConfig[env]);

38.8 User Actions Tracking

Neben Errors und Performance sollten wichtige User-Actions geloggt werden – für Analytics und Debugging.

// useActionLogger.ts
export function useActionLogger() {
  const logAction = useCallback((
    action: string, 
    category: string,
    context?: Record<string, unknown>
  ) => {
    logger.info(`User action: ${action}`, {
      category,
      ...context,
      timestamp: Date.now()
    });
  }, []);
  
  return { logAction };
}

// Verwendung
function ProductCard({ product }: { product: Product }) {
  const { logAction } = useActionLogger();
  
  const handleAddToCart = () => {
    logAction('add_to_cart', 'ecommerce', {
      productId: product.id,
      productName: product.name,
      price: product.price
    });
    
    addToCart(product);
  };
  
  const handleView = () => {
    logAction('view_product', 'ecommerce', {
      productId: product.id,
      category: product.category
    });
  };
  
  useEffect(() => {
    handleView();
  }, []);
  
  return (
    <div>
      <h3>{product.name}</h3>
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}

38.9 Performance-Überlegungen

Logging kann Performance beeinträchtigen. Einige Optimierungen:

38.9.1 Lazy Evaluation

// ❌ Falsch: Teure Operation wird immer ausgeführt
logger.debug('User state: ' + JSON.stringify(expensiveUserObject));

// ✓ Richtig: Nur wenn Debug-Level aktiv
logger.debug('User state', () => ({ user: expensiveUserObject }));

// Logger-Implementation
debug(message: string, contextOrFactory?: Record<string, unknown> | (() => Record<string, unknown>)): void {
  if (!this.shouldLog('debug')) return;  // Early return
  
  const context = typeof contextOrFactory === 'function' 
    ? contextOrFactory() 
    : contextOrFactory;
  
  const entry = this.createEntry('debug', message, context);
  this.output(entry);
}

38.9.2 Batch Logging

Statt jeden Log-Eintrag einzeln zu senden, sammeln und batchen:

class Logger {
  private batchQueue: LogEntry[] = [];
  private batchTimeout: NodeJS.Timeout | null = null;
  private readonly BATCH_SIZE = 10;
  private readonly BATCH_INTERVAL = 5000; // 5 Sekunden
  
  private async logToRemote(entry: LogEntry): Promise<void> {
    this.batchQueue.push(entry);
    
    if (this.batchQueue.length >= this.BATCH_SIZE) {
      this.flushBatch();
    } else if (!this.batchTimeout) {
      this.batchTimeout = setTimeout(() => this.flushBatch(), this.BATCH_INTERVAL);
    }
  }
  
  private async flushBatch(): Promise<void> {
    if (this.batchQueue.length === 0) return;
    
    const batch = [...this.batchQueue];
    this.batchQueue = [];
    
    if (this.batchTimeout) {
      clearTimeout(this.batchTimeout);
      this.batchTimeout = null;
    }
    
    try {
      await fetch(this.config.remoteUrl!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ logs: batch })
      });
    } catch (error) {
      console.error('Failed to send log batch:', error);
    }
  }
}

38.9.3 Sampling für High-Frequency Logs

class Logger {
  private sampleRate = 0.1; // 10% der Logs
  
  debug(message: string, context?: Record<string, unknown>): void {
    if (!this.shouldLog('debug')) return;
    
    // Nur 10% der Debug-Logs senden
    if (Math.random() > this.sampleRate) return;
    
    const entry = this.createEntry('debug', message, context);
    this.output(entry);
  }
}

38.10 Häufige Fehler

Fehler 1: Sensible Daten loggen

// ❌ Falsch: Password im Log
logger.debug('Login attempt', { email, password });

// ✓ Richtig: Sensible Daten filtern
logger.debug('Login attempt', { email });

Fehler 2: Synchrones Logging blockiert UI

// ❌ Falsch: Blockiert Main Thread
function expensiveLog() {
  const data = JSON.stringify(hugeObject);  // Langsam!
  logger.info('Data:', data);
}

// ✓ Richtig: Async oder lazy
function expensiveLog() {
  logger.info('Data', () => ({ data: hugeObject }));
}

Fehler 3: Logs in Render-Methoden

// ❌ Falsch: Fired bei jedem Re-Render
function Component() {
  logger.debug('Component rendering');  // Viel zu oft!
  return <div>...</div>;
}

// ✓ Richtig: In useEffect oder Event-Handler
function Component() {
  useEffect(() => {
    logger.debug('Component mounted');
  }, []);
  
  return <div>...</div>;
}

Fehler 4: console.log in Production

// ❌ Falsch: Bleibt im Bundle
console.log('Debug info');

// ✓ Richtig: Über Logger mit Environment-Filtering
logger.debug('Debug info');  // Wird in Production unterdrückt

38.11 Zusammenfassung: Logging-Levels und Use Cases

Level Wann verwenden Production Beispiel
debug Development Details ❌ Nie Component Renders, State Changes
info Normale Events ⚠️ Selektiv User Actions, API Calls
warn Potenzielle Probleme ✅ Ja Deprecated APIs, Performance Warnings
error Kritische Fehler ✅ Ja API Failures, Uncaught Exceptions

Strukturiertes Logging mit TypeScript, Custom Hooks und Sentry-Integration transformiert Debugging von “Hoffnung und Raten” zu datengetriebener Fehlerdiagnose. Der initiale Setup-Aufwand zahlt sich aus, sobald der erste Production-Bug auftritt, den man ohne Logs nie reproduzieren könnte.

Ich schaue mir zuerst das hochgeladene Kapitel an, um zu sehen, was behandelt werden soll.# Internationalisierung – Mehrsprachige React-Anwendungen

Eine Anwendung, die nur auf Englisch verfügbar ist, schließt automatisch einen Großteil der Weltbevölkerung aus. Internationalisierung (i18n – 18 steht für die Buchstaben zwischen ‘i’ und ‘n’) bedeutet mehr als nur Texte übersetzen. Datums- und Zahlenformate variieren, Grammatikregeln unterscheiden sich, manche Sprachen lesen von rechts nach links, und kulturelle Konventionen beeinflussen UI-Design.

React-i18next hat sich als Standard-Lösung etabliert. Die Library baut auf i18next auf – einer robusten, framework-agnostischen Internationalisierungsbibliothek – und erweitert sie um React-spezifische Hooks und Komponenten.

38.12 Setup: Installation und Basis-Konfiguration

npm install react-i18next i18next

Die Konfiguration erfolgt typischerweise in einer separaten Datei:

// src/i18n/config.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

// Übersetzungs-Ressourcen
const resources = {
  en: {
    translation: {
      welcome: "Welcome to React",
      greeting: "Hello, {{name}}",
      itemCount: "{{count}} item",
      itemCount_other: "{{count}} items",
      navigation: {
        home: "Home",
        about: "About",
        contact: "Contact"
      }
    }
  },
  de: {
    translation: {
      welcome: "Willkommen bei React",
      greeting: "Hallo, {{name}}",
      itemCount: "{{count}} Element",
      itemCount_other: "{{count}} Elemente",
      navigation: {
        home: "Startseite",
        about: "Über uns",
        contact: "Kontakt"
      }
    }
  }
};

i18n
  .use(initReactI18next)
  .init({
    resources,
    lng: 'de',           // Standard-Sprache
    fallbackLng: 'en',   // Fallback wenn Übersetzung fehlt
    
    interpolation: {
      escapeValue: false  // React escaped bereits
    },
    
    debug: import.meta.env.DEV  // Logging in Development
  });

export default i18n;
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './i18n/config';  // i18n vor App importieren

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

38.13 useTranslation: Der Kern-Hook

Der useTranslation Hook stellt die Übersetzungsfunktion t bereit und ermöglicht Sprachwechsel.

import { useTranslation } from 'react-i18next';

function Welcome() {
  const { t, i18n } = useTranslation();
  
  const changeLanguage = (lng: string) => {
    i18n.changeLanguage(lng);
  };
  
  return (
    <div>
      <h1>{t('welcome')}</h1>
      
      <div>
        <button onClick={() => changeLanguage('en')}>English</button>
        <button onClick={() => changeLanguage('de')}>Deutsch</button>
      </div>
      
      <p>Current language: {i18n.language}</p>
    </div>
  );
}

Die t-Funktion nimmt einen Key und gibt den übersetzten Text zurück. Bei fehlendem Key wird der Key selbst zurückgegeben (Development) oder der Fallback-Text (Production).

38.14 Interpolation: Dynamische Werte in Übersetzungen

Placeholder in Übersetzungen werden zur Laufzeit mit Werten gefüllt.

// Übersetzungen
const resources = {
  en: {
    translation: {
      greeting: "Hello, {{name}}!",
      itemsInCart: "You have {{count}} items in your cart",
      lastLogin: "Last login: {{date, datetime}}",
      price: "Price: {{amount, currency}}"
    }
  },
  de: {
    translation: {
      greeting: "Hallo, {{name}}!",
      itemsInCart: "Sie haben {{count}} Artikel im Warenkorb",
      lastLogin: "Letzter Login: {{date, datetime}}",
      price: "Preis: {{amount, currency}}"
    }
  }
};
function UserGreeting({ user }: { user: User }) {
  const { t } = useTranslation();
  
  return (
    <div>
      <h2>{t('greeting', { name: user.name })}</h2>
      <p>{t('itemsInCart', { count: user.cartItems.length })}</p>
      <p>{t('lastLogin', { date: user.lastLoginAt })}</p>
      <p>{t('price', { amount: 49.99 })}</p>
    </div>
  );
}

Für Datums- und Zahlenformatierung integriert sich i18next mit dem Intl API:

// i18n/config.ts erweitern
i18n.init({
  // ... andere Config
  
  interpolation: {
    escapeValue: false,
    
    // Custom Format-Funktionen
    format: (value, format, lng) => {
      if (format === 'currency') {
        return new Intl.NumberFormat(lng, {
          style: 'currency',
          currency: 'EUR'
        }).format(value);
      }
      
      if (format === 'datetime') {
        return new Intl.DateTimeFormat(lng, {
          dateStyle: 'medium',
          timeStyle: 'short'
        }).format(value);
      }
      
      return value;
    }
  }
});

38.15 Pluralisierung: Sprachspezifische Grammatik

Verschiedene Sprachen haben unterschiedliche Plural-Regeln. Englisch hat zwei Formen (singular/plural), Polnisch hat fünf, Arabisch sechs.

const resources = {
  en: {
    translation: {
      item: "{{count}} item",
      item_other: "{{count}} items"
    }
  },
  de: {
    translation: {
      item: "{{count}} Element",
      item_other: "{{count}} Elemente"
    }
  },
  pl: {
    translation: {
      item_one: "{{count}} przedmiot",
      item_few: "{{count}} przedmioty",
      item_many: "{{count}} przedmiotów"
    }
  }
};
function ItemCounter({ count }: { count: number }) {
  const { t } = useTranslation();
  
  return <p>{t('item', { count })}</p>;
}

// count = 0  → "0 items" / "0 Elemente"
// count = 1  → "1 item"  / "1 Element"
// count = 5  → "5 items" / "5 Elemente"

i18next kennt die Plural-Regeln für über 200 Sprachen basierend auf Unicode CLDR.

38.16 Namespaces: Organisation großer Übersetzungen

Bei größeren Apps werden Übersetzungen unübersichtlich. Namespaces gruppieren zusammengehörige Texte.

// Separate Dateien pro Namespace
// src/i18n/locales/en/common.json
{
  "navigation": {
    "home": "Home",
    "about": "About"
  },
  "buttons": {
    "save": "Save",
    "cancel": "Cancel"
  }
}

// src/i18n/locales/en/products.json
{
  "title": "Products",
  "addToCart": "Add to cart",
  "outOfStock": "Out of stock"
}

// src/i18n/locales/en/checkout.json
{
  "title": "Checkout",
  "shipping": "Shipping address",
  "payment": "Payment method"
}
// i18n/config.ts
import commonEN from './locales/en/common.json';
import productsEN from './locales/en/products.json';
import checkoutEN from './locales/en/checkout.json';

import commonDE from './locales/de/common.json';
import productsDE from './locales/de/products.json';
import checkoutDE from './locales/de/checkout.json';

const resources = {
  en: {
    common: commonEN,
    products: productsEN,
    checkout: checkoutEN
  },
  de: {
    common: commonDE,
    products: productsDE,
    checkout: checkoutDE
  }
};

i18n.init({
  resources,
  defaultNS: 'common',
  ns: ['common', 'products', 'checkout'],
  // ... rest
});
// Namespace spezifizieren
function ProductList() {
  const { t } = useTranslation('products');
  
  return (
    <div>
      <h1>{t('title')}</h1>
      <button>{t('addToCart')}</button>
    </div>
  );
}

// Oder explizit mit Namespace-Prefix
function MixedComponent() {
  const { t } = useTranslation();
  
  return (
    <div>
      <h1>{t('products:title')}</h1>
      <button>{t('common:buttons.save')}</button>
    </div>
  );
}

38.17 TypeScript: Typsichere Übersetzungen

TypeScript kann validieren, dass Translation-Keys existieren und Interpolation-Parameter korrekt sind.

// src/i18n/types.ts
import 'react-i18next';
import type common from './locales/en/common.json';
import type products from './locales/en/products.json';

declare module 'react-i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common';
    resources: {
      common: typeof common;
      products: typeof products;
    };
  }
}

Jetzt validiert TypeScript die Keys:

function Component() {
  const { t } = useTranslation();
  
  // ✓ Korrekt
  t('navigation.home');
  
  // ❌ TypeScript Error: Key existiert nicht
  t('navigation.nonexistent');
  
  // ❌ TypeScript Error: Namespace fehlt
  t('products:title');  // Ohne 'products' im useTranslation
}

function ProductComponent() {
  const { t } = useTranslation('products');
  
  // ✓ Jetzt korrekt
  t('title');
  t('addToCart');
}

Für Interpolation-Parameter:

// Erweiterte Type-Definitionen
declare module 'react-i18next' {
  interface CustomTypeOptions {
    // ... resources
    
    // Interpolation-Parameter typisieren
    interpoLation: {
      escapeValue: false;
    };
  }
}

// Übersetzungen mit Typ-Info
const resources = {
  en: {
    translation: {
      greeting: "Hello, {{name}}!",
      itemCount: "{{count}} items"
    }
  }
} as const;

// Nutzung
function Greeting({ userName }: { userName: string }) {
  const { t } = useTranslation();
  
  // ✓ Parameter stimmt
  t('greeting', { name: userName });
  
  // ❌ TypeScript könnte warnen (mit erweiterten Types)
  t('greeting', { user: userName });
}

38.18 Language Switcher: UI für Sprachwechsel

Ein praktischer Language Switcher als wiederverwendbare Komponente:

// components/LanguageSwitcher.tsx
import { useTranslation } from 'react-i18next';

const languages = {
  en: { name: 'English', flag: '🇬🇧' },
  de: { name: 'Deutsch', flag: '🇩🇪' },
  fr: { name: 'Français', flag: '🇫🇷' },
  es: { name: 'Español', flag: '🇪🇸' }
} as const;

type Language = keyof typeof languages;

export function LanguageSwitcher() {
  const { i18n } = useTranslation();
  const currentLanguage = i18n.language as Language;
  
  const handleChange = (lng: Language) => {
    i18n.changeLanguage(lng);
    // Optional: In localStorage speichern
    localStorage.setItem('preferredLanguage', lng);
  };
  
  return (
    <select 
      value={currentLanguage} 
      onChange={(e) => handleChange(e.target.value as Language)}
    >
      {Object.entries(languages).map(([code, { name, flag }]) => (
        <option key={code} value={code}>
          {flag} {name}
        </option>
      ))}
    </select>
  );
}

Browser-Sprache als Default verwenden:

// i18n/config.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(LanguageDetector)  // Auto-Detection
  .use(initReactI18next)
  .init({
    // ... resources
    
    detection: {
      order: ['localStorage', 'navigator'],
      caches: ['localStorage']
    },
    
    fallbackLng: 'en'
  });

38.19 Lazy Loading: Übersetzungen on-demand laden

Für große Apps: Übersetzungen nur laden, wenn gebraucht.

npm install i18next-http-backend
// i18n/config.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';

i18n
  .use(HttpBackend)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json'
    },
    
    ns: ['common'],
    defaultNS: 'common',
    
    react: {
      useSuspense: true  // Suspense-Support
    }
  });

Ordnerstruktur:

public/
  locales/
    en/
      common.json
      products.json
      checkout.json
    de/
      common.json
      products.json
      checkout.json

Mit Suspense:

import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Router>
        <Routes>
          <Route path="/products" element={<ProductsPage />} />
          <Route path="/checkout" element={<CheckoutPage />} />
        </Routes>
      </Router>
    </Suspense>
  );
}

// ProductsPage lädt automatisch products.json
function ProductsPage() {
  const { t } = useTranslation('products');
  return <div>{t('title')}</div>;
}

38.20 Trans Component: JSX in Übersetzungen

Manchmal braucht man HTML/React-Komponenten in Übersetzungen.

const resources = {
  en: {
    translation: {
      terms: "I agree to the <1>Terms of Service</1> and <3>Privacy Policy</3>",
      formatted: "This is <strong>bold</strong> and <em>italic</em>"
    }
  }
};
import { Trans } from 'react-i18next';

function TermsCheckbox() {
  return (
    <label>
      <input type="checkbox" />
      <Trans i18nKey="terms">
        I agree to the 
        <a href="/terms">Terms of Service</a> 
        and 
        <a href="/privacy">Privacy Policy</a>
      </Trans>
    </label>
  );
}

// Oder mit components-Prop
function FormattedText() {
  return (
    <Trans
      i18nKey="formatted"
      components={{
        strong: <strong />,
        em: <em />
      }}
    />
  );
}

Die Zahlen in den Übersetzungen (<1>, <3>) entsprechen den Child-Indices der Trans-Komponente.

38.21 RTL-Sprachen: Right-to-Left Support

Arabisch, Hebräisch und andere Sprachen lesen von rechts nach links. Das beeinflusst Layout, Padding, Margins.

// i18n/config.ts
const resources = {
  en: { translation: {...}, dir: 'ltr' },
  de: { translation: {...}, dir: 'ltr' },
  ar: { translation: {...}, dir: 'rtl' },
  he: { translation: {...}, dir: 'rtl' }
};
// App.tsx
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';

function App() {
  const { i18n } = useTranslation();
  
  useEffect(() => {
    // HTML dir-Attribut setzen
    const direction = i18n.dir();
    document.documentElement.dir = direction;
    document.documentElement.lang = i18n.language;
  }, [i18n.language]);
  
  return <div>{/* App content */}</div>;
}

CSS für RTL:

/* Statt fest left/right */
.sidebar {
  /* ❌ Falsch */
  margin-left: 20px;
  
  /* ✓ Richtig: Logical Properties */
  margin-inline-start: 20px;
}

.icon {
  /* ❌ Falsch */
  padding-right: 10px;
  
  /* ✓ Richtig */
  padding-inline-end: 10px;
}

/* Oder mit Direktiven */
[dir="rtl"] .icon {
  transform: scaleX(-1);  /* Icons spiegeln */
}
Property LTR RTL Logical Property
margin-left links rechts margin-inline-start
margin-right rechts links margin-inline-end
padding-left links rechts padding-inline-start
text-align: left links rechts text-align: start

38.22 Testing: i18n in Tests

Für Tests braucht man Mock-Übersetzungen oder echte Test-Ressourcen.

// test/setup.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n
  .use(initReactI18next)
  .init({
    lng: 'en',
    fallbackLng: 'en',
    
    resources: {
      en: {
        translation: {
          welcome: "Welcome",
          greeting: "Hello, {{name}}"
        }
      }
    },
    
    interpolation: {
      escapeValue: false
    }
  });

export default i18n;
// Component.test.tsx
import { render, screen } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from './test/setup';
import Welcome from './Welcome';

test('renders welcome message', () => {
  render(
    <I18nextProvider i18n={i18n}>
      <Welcome />
    </I18nextProvider>
  );
  
  expect(screen.getByText('Welcome')).toBeInTheDocument();
});

test('renders greeting with name', () => {
  render(
    <I18nextProvider i18n={i18n}>
      <Greeting name="Alice" />
    </I18nextProvider>
  );
  
  expect(screen.getByText('Hello, Alice')).toBeInTheDocument();
});

Oder mit Custom Render Helper:

// test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from './setup';

function customRender(ui: React.ReactElement, options?: RenderOptions) {
  return render(
    <I18nextProvider i18n={i18n}>
      {ui}
    </I18nextProvider>,
    options
  );
}

export * from '@testing-library/react';
export { customRender as render };
// Component.test.tsx
import { render, screen } from './test/utils';  // Custom render

test('renders welcome message', () => {
  render(<Welcome />);  // Kein Provider nötig
  expect(screen.getByText('Welcome')).toBeInTheDocument();
});

38.23 Häufige Fehler

Fehler 1: String-Concatenation statt vollständiger Sätze

// ❌ Falsch: Funktioniert nicht in allen Sprachen
const resources = {
  en: {
    translation: {
      itemsStart: "You have",
      itemsEnd: "items in cart"
    }
  }
};

// Im Code: t('itemsStart') + ' ' + count + ' ' + t('itemsEnd')
// Problem: Wortstellung variiert in Sprachen

// ✓ Richtig: Kompletter Satz
const resources = {
  en: {
    translation: {
      itemsInCart: "You have {{count}} items in cart"
    }
  },
  de: {
    translation: {
      itemsInCart: "Sie haben {{count}} Artikel im Warenkorb"
    }
  }
};

Fehler 2: Hardcoded Formatierung

// ❌ Falsch: US-Format hardcoded
const date = new Date();
const formatted = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;

// ✓ Richtig: Intl API
const { i18n } = useTranslation();
const formatted = new Intl.DateTimeFormat(i18n.language).format(date);
// en: "12/25/2024"
// de: "25.12.2024"

Fehler 3: Context ignorieren

// ❌ Falsch: Gleicher Key für verschiedene Kontexte
const resources = {
  en: {
    translation: {
      close: "Close"  // Button oder "close to"?
    }
  }
};

// ✓ Richtig: Context-spezifische Keys
const resources = {
  en: {
    translation: {
      button_close: "Close",
      nearby: "Close to the station"
    }
  }
};

Fehler 4: Fehlende Fallbacks

// ❌ Falsch: Keine Fallback-Sprache
i18n.init({
  lng: 'de',
  // fallbackLng fehlt!
});

// Was passiert wenn deutsche Übersetzung fehlt? → Key wird angezeigt

// ✓ Richtig: Immer Fallback definieren
i18n.init({
  lng: 'de',
  fallbackLng: 'en'
});

38.24 Performance: Übersetzungen optimieren

Bei tausenden Translation-Keys kann Bundle-Size und Performance leiden.

Strategie 1: Code-Splitting mit Namespaces

// Nur common-Namespace initial laden
i18n.init({
  ns: ['common'],
  defaultNS: 'common',
  
  backend: {
    loadPath: '/locales/{{lng}}/{{ns}}.json'
  }
});

// products-Namespace wird erst geladen wenn ProductPage rendert
function ProductPage() {
  const { t } = useTranslation('products');  // Lazy-loaded
  return <div>{t('title')}</div>;
}

Strategie 2: Memoization bei teuren Übersetzungen

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

function ExpensiveList({ items }: { items: Item[] }) {
  const { t } = useTranslation();
  
  // Übersetzungen nur einmal pro Sprache berechnen
  const translatedCategories = useMemo(() => {
    return items.map(item => ({
      ...item,
      category: t(`categories.${item.categoryKey}`)
    }));
  }, [items, t]);
  
  return (
    <ul>
      {translatedCategories.map(item => (
        <li key={item.id}>{item.category}</li>
      ))}
    </ul>
  );
}

Strategie 3: Production-Build ohne Debug-Logs

i18n.init({
  debug: import.meta.env.DEV,  // Nur in Development
  
  saveMissing: import.meta.env.DEV,  // Missing Keys nur in Dev tracken
  
  // ... rest
});

Internationalisierung ist kein Afterthought – sie beeinflusst Architektur, UI-Design und User Experience fundamental. React-i18next macht den technischen Teil handhabbar, aber kulturelle Sensibilität und durchdachte Übersetzungsprozesse bleiben menschliche Aufgaben.