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.
// 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
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.
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>;
}
// 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>;
}
// 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>;
}
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;
}
}
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'
});
}
}
}
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]);
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>
);
}
Logging kann Performance beeinträchtigen. Einige Optimierungen:
// ❌ 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);
}
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);
}
}
}
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);
}
}
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
| 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.
npm install react-i18next i18nextDie 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>
);
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).
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;
}
}
});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.
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>
);
}
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 });
}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'
});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>;
}
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.
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 |
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();
});
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'
});
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.