Files
fotospiel-app/docs/archive/prp/12-i18n.md
2025-11-20 12:31:21 +01:00

5.8 KiB
Raw Blame History

12 — Internationalization (i18n)

Overview

  • Default Locale: de (Deutsch), fallback en (English).
  • Supported Locales: MVP focuses on de and en; expandable via config/app.php locales array.
  • Strategy: Hybrid approach Laravel PHP for backend/Blade views, react-i18next for React/Vite PWAs (Marketing, Tenant Admin, Guest).
  • Translatable Fields: JSON columns in DB for dynamic content (e.g., name, description, title, body_markdown in models like Package, EventType, LegalPage).
  • Namespaces: Organized by feature (e.g., marketing, auth, guest, admin) to avoid key collisions; glossary terms centralized in a shared namespace if needed.
  • Date/Number Formatting: Locale-aware via Laravel's Carbon/Intl; PWAs use Intl API or date-fns with locale bundles.
  • RTL Support: Not in MVP; future via CSS classes and i18next RTL plugin.
  • SEO: Multilingual URLs with prefixes (/de/, /en/), hreflang tags, canonical links, translated meta (title, description, og:).

Backend (Laravel/PHP)

  • Config: config/app.php locale => 'de', fallback_locale => 'en', available_locales => ['de', 'en'].
  • Translation Files:
    • PHP arrays: resources/lang/\{locale\}/\{group\}.php (e.g., marketing.php, auth.php, legal.php) for Blade and API responses.
    • JSON for PWAs: public/lang/\{locale\}/\{namespace\}.json (e.g., public/lang/de/marketing.json) migrated from PHP where possible; loaded via dedicated route.
  • Routing:
    • Prefixed groups: Route::prefix('\{locale?\}')->where(['locale' => 'de|en'])->middleware('SetLocale') in routes/web.php.
    • Fallbacks: Non-prefixed routes redirect to /de/\{path\} (e.g., /login/de/login).
    • Auth routes (login, register, logout): Prefixed and named (e.g., Route::get('/login', ...)->name('login')).
    • API routes: Locale from header/session; no URL prefix for /api/v1.
  • Middleware: SetLocale Extracts locale from URL segment(1), sets App::setLocale(), stores in session; defaults to 'de'.
  • JSON Loader Route: Route::get('/lang/\{locale\}/\{namespace\}.json', ...) Serves from public_path('lang/\{locale\}/\{namespace\}.json'); Vite proxy forwards requests.
  • DB Translations: Use JSON fields with spatie/laravel-translatable or native casts; admin UI (Filament) for editing per locale.
  • Legal Pages: Dynamic via LegalPage model; rendered with __($key) or JSON for PWAs.

Frontend (React/Vite PWAs)

  • Library: react-i18next with i18next-http-backend for async JSON loads.
  • Setup (resources/js/i18n.js):
    • Init: i18n.use(Backend).use(LanguageDetector).init({ lng: 'de', fallbackLng: 'en', ns: ['marketing', 'auth'], backend: { loadPath: '/lang/\{\{lng\}\}/\{\{ns\}\}.json' } }).
    • Detection: Path-based (order: ['path'], lookupFromPathIndex: 0) for prefixed URLs; cookie/session fallback.
    • Provider: Wrap <App> in <I18nextProvider i18n=\{i18n\}> in app.tsx.
  • Usage:
    • Hook: const { t } = useTranslation('namespace'); in components (e.g., t('marketing.home.title')).
    • Interpolation: Placeholders \{count\}; pluralization via i18next rules.
    • Dynamic Keys: Avoid; use namespaces for organization.
  • Inertia Integration:
    • Page Resolver: resolvePageComponent(`./Pages/\${name}.tsx`, import.meta.glob('./Pages/**/*.tsx')) Matches capital 'Pages' directory.
    • Props: Pass locale from middleware to pages.
    • Links: <Link href={/${'{'}locale{'}'}/path}> für prefixed navigation (e.g., Header.tsx).
  • Marketing Frontend:
    • Namespaces: marketing (Home, Packages, Blog, Features), auth (Login, Register).
    • Components: All hard-coded strings replaced (e.g., Home.tsx: t('marketing.hero.title')); SEO meta via Head with t().
    • Header: Locale selector; dynamic links (e.g., /\{locale\}/login with t('auth.header.login')).
  • Guest/Tenant PWAs:
    • Similar setup; load JSON on app init.
    • Guest: Anonymous, locale from URL or default 'de'; strings for UI (e.g., gallery, upload).
    • Tenant Admin: User-preferred locale from profile; sync with backend.
  • Automation:
    • Extraction: i18next-scanner (npm script: i18next-scanner --config i18next-scanner.config.js) to scan TSX for t('key') and update JSON.
    • Validation: Missing keys log warnings; dev mode strict.

SEO & Accessibility

  • Multilingual URLs: /de/home, /en/home; 301 redirects for non-prefixed.
  • Hreflang: <link rel="alternate" hreflang="de" href="/de/home"> in <Head>.
  • Canonical: <link rel="canonical" href=\{currentUrl\}> based on detected locale.
  • Meta: Translated via t('seo.title'); og:locale='de_DE'.
  • Sitemap: Generate with de/ and en/ variants; update public/sitemap.xml.
  • Robots.txt: Allow both locales; noindex for dev.
  • Accessibility: ARIA labels with t(); screen reader support for language switches.

Migration from PHP to JSON

  • Extract keys from resources/lang/\{locale\}/marketing.php to public/lang/\{locale\}/marketing.json.
  • Consolidate: Remove duplicates; use nested objects (e.g., { "header": { "login": "Anmelden" } }).
  • Fallback: PHP arrays remain for backend; JSON for PWAs.

Testing & Maintenance

  • Tests: PHPUnit for backend (__('key')); Vitest/Jest for frontend (t('key') renders correctly).
  • Linting: ESLint rule for missing translations; i18next-scanner in pre-commit.
  • Deployment: JSON files in public; cache-bust via Vite hashes.
  • Expansion: Add locales via config; migrate more namespaces (e.g., guest, admin).

Decisions & Trade-offs

  • Path-based detection over query params for SEO/clean URLs.
  • JSON over PHP for PWAs: Faster async loads, no server roundtrips.
  • No auto-redirect on locale mismatch in MVP; user chooses via selector.
  • ADR: Use react-i18next over next-intl (Inertia compatibility).