Files
fotospiel-app/docs/prp/12-i18n.md

79 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`}>` for 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).