From 60f8de9162be0c9bb0d2e98967c6222e59067ef8 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Fri, 3 Oct 2025 13:05:13 +0200 Subject: [PATCH] feat(i18n): Complete localization of marketing frontend with react-i18next, prefixed URLs, JSON migrations, and automation --- .../Auth/MarketingRegisterController.php | 6 +- app/Http/Controllers/MarketingController.php | 10 +- app/Http/Controllers/ProfileController.php | 66 +- app/Http/Kernel.php | 70 ++ app/Http/Middleware/HandleInertiaRequests.php | 6 + app/Http/Middleware/SetLocale.php | 21 +- docs/prp/04-data-model-migrations.md | 29 +- docs/prp/07-guest-pwa-routes-components.md | 4 +- docs/prp/07-guest-pwa.md | 2 +- docs/prp/12-i18n.md | 82 +- docs/prp/tenant-app-specs/functional-specs.md | 2 +- docs/prp/tenant-app-specs/pages-ui.md | 2 +- docs/prp/tenant-app-specs/settings-config.md | 2 +- i18next-scanner.config.js | 45 + package-lock.json | 994 +++++++++++++++++- package.json | 16 +- public/lang/de/auth.json | 57 + public/lang/de/marketing.json | 266 +++++ public/lang/en/auth.json | 57 + public/lang/en/marketing.json | 257 +++++ public/sitemap.xml | 79 ++ resources/css/app.css | 6 + resources/js/app.tsx | 20 +- resources/js/components/Layout/AppLayout.tsx | 23 + resources/js/components/Layout/Header.tsx | 153 +++ resources/js/components/ui/switch.tsx | 27 + resources/js/hooks/use-appearance.ts | 68 ++ resources/js/i18n.js | 29 + .../js/layouts/marketing/MarketingLayout.tsx | 79 +- resources/js/pages/Profile/Account.tsx | 45 + resources/js/pages/Profile/Index.tsx | 38 + resources/js/pages/Profile/Orders.tsx | 61 ++ resources/js/pages/auth/login.tsx | 24 +- resources/js/pages/auth/register.tsx | 57 +- resources/js/pages/marketing/Blog.tsx | 34 +- resources/js/pages/marketing/BlogShow.tsx | 11 +- resources/js/pages/marketing/Home.tsx | 292 +++-- resources/js/pages/marketing/Kontakt.tsx | 82 +- resources/js/pages/marketing/Occasions.tsx | 116 +- resources/js/pages/marketing/Packages.tsx | 216 ++-- resources/js/pages/marketing/Success.tsx | 24 +- resources/lang/de/marketing.json | 243 +++++ resources/lang/en/marketing.json | 243 +++++ resources/views/layouts/marketing.blade.php | 1 - routes/auth.php | 15 - routes/web.php | 64 +- 46 files changed, 3454 insertions(+), 590 deletions(-) create mode 100644 app/Http/Kernel.php create mode 100644 i18next-scanner.config.js create mode 100644 public/lang/de/auth.json create mode 100644 public/lang/de/marketing.json create mode 100644 public/lang/en/auth.json create mode 100644 public/lang/en/marketing.json create mode 100644 public/sitemap.xml create mode 100644 resources/js/components/Layout/AppLayout.tsx create mode 100644 resources/js/components/Layout/Header.tsx create mode 100644 resources/js/components/ui/switch.tsx create mode 100644 resources/js/hooks/use-appearance.ts create mode 100644 resources/js/i18n.js create mode 100644 resources/js/pages/Profile/Account.tsx create mode 100644 resources/js/pages/Profile/Index.tsx create mode 100644 resources/js/pages/Profile/Orders.tsx create mode 100644 resources/lang/de/marketing.json create mode 100644 resources/lang/en/marketing.json diff --git a/app/Http/Controllers/Auth/MarketingRegisterController.php b/app/Http/Controllers/Auth/MarketingRegisterController.php index 52545b9..f78bbef 100644 --- a/app/Http/Controllers/Auth/MarketingRegisterController.php +++ b/app/Http/Controllers/Auth/MarketingRegisterController.php @@ -28,11 +28,10 @@ class MarketingRegisterController extends Controller { $package = $package_id ? Package::find($package_id) : null; - App::setLocale('de'); + //App::setLocale('de'); - return Inertia::render('Auth/Register', [ + return Inertia::render('auth/register', [ 'package' => $package, - 'privacyHtml' => view('legal.datenschutz')->render(), ]); } @@ -157,3 +156,4 @@ class MarketingRegisterController extends Controller + diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php index fd62e15..825fc0c 100644 --- a/app/Http/Controllers/MarketingController.php +++ b/app/Http/Controllers/MarketingController.php @@ -33,11 +33,9 @@ class MarketingController extends Controller public function index() { - $packages = [ - ['id' => 'basic', 'name' => 'Basic', 'events' => 1, 'price' => 0, 'description' => '1 Event, 100 Fotos, Grundfunktionen'], - ['id' => 'standard', 'name' => 'Standard', 'events' => 10, 'price' => 99, 'description' => '10 Events, Unbegrenzt Fotos, Erweiterte Features'], - ['id' => 'premium', 'name' => 'Premium', 'events' => 50, 'price' => 199, 'description' => '50 Events, Support & Custom, Alle Features'], - ]; + $packages = Package::where('type', 'endcustomer')->orderBy('price')->get()->map(function ($p) { + return $p->append(['features', 'limits']); + }); return Inertia::render('marketing/Home', compact('packages')); } @@ -417,7 +415,7 @@ class MarketingController extends Controller public function occasionsType($locale, $type) { - $validTypes = ['weddings', 'birthdays', 'corporate-events', 'family-celebrations']; + $validTypes = ['hochzeit', 'geburtstag', 'firmenevent']; if (!in_array($type, $validTypes)) { abort(404, 'Invalid occasion type'); } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index cb11314..634fc6e 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -4,63 +4,43 @@ namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; +use Inertia\Inertia; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Hash; -use Illuminate\Validation\Rules; -use Illuminate\View\View; -use Illuminate\Http\RedirectResponse; class ProfileController extends Controller { - /** - * Display the user's profile form. - */ - public function edit(Request $request): View + public function index() { - return view('profile.edit', [ - 'user' => $request->user(), + $user = Auth::user()->load('purchases.packages'); + return Inertia::render('Profile/Index', [ + 'user' => $user, ]); } - /** - * Update the user's profile information. - */ - public function update(Request $request, User $user): RedirectResponse + public function account() { - // Authorized via auth middleware + $user = Auth::user()->load('purchases.packages'); + if (request()->isMethod('post')) { + $validated = request()->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|unique:users,email,' . $user->id, + ]); - $request->validate([ - 'username' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:users,username,' . $user->id], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id], - 'first_name' => ['required', 'string', 'max:255'], - 'last_name' => ['required', 'string', 'max:255'], - 'address' => ['required', 'string'], - 'phone' => ['required', 'string', 'max:20'], + $user->update($validated); + + return back()->with('success', 'Profil aktualisiert.'); + } + + return Inertia::render('Profile/Account', [ + 'user' => $user, ]); - - $user->update($request->only([ - 'username', 'email', 'first_name', 'last_name', 'address', 'phone' - ])); - - return back()->with('status', 'profile-updated'); } - /** - * Update the user's password. - */ - public function updatePassword(Request $request, User $user): RedirectResponse + public function orders() { - // Authorized via auth middleware - - $request->validate([ - 'current_password' => ['required', 'current_password'], - 'password' => ['required', 'confirmed', Rules\Password::defaults()], + $user = Auth::user()->load('purchases.packages'); + return Inertia::render('Profile/Orders', [ + 'purchases' => $user->purchases, ]); - - $user->update([ - 'password' => Hash::make($request->password), - ]); - - return back()->with('status', 'password-updated'); } } \ No newline at end of file diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..afee9b9 --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,70 @@ + + */ + protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, + \App\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::class, + \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + \App\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ]; + + /** + * The application's route middleware groups. + * + * @var array> + */ + protected $middlewareGroups = [ + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \App\Http\Middleware\HandleInertiaRequests::class, + \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, + ], + + 'api' => [ + // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + 'throttle:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'signed' => \App\Http\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'locale' => \App\Http\Middleware\SetLocale::class, + ]; +} \ No newline at end of file diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index a62e452..c6ed985 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -61,6 +61,12 @@ class HandleInertiaRequests extends Middleware ], 'supportedLocales' => $supportedLocales, 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', + 'locale' => app()->getLocale(), + 'translations' => [ + 'marketing' => __('marketing'), + 'auth' => __('auth'), + 'profile' => __('profile'), + ], ]; } } diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php index 03299f4..87bafe0 100644 --- a/app/Http/Middleware/SetLocale.php +++ b/app/Http/Middleware/SetLocale.php @@ -4,6 +4,8 @@ namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Session; use Symfony\Component\HttpFoundation\Response; class SetLocale @@ -15,16 +17,25 @@ class SetLocale */ public function handle(Request $request, Closure $next): Response { - $locale = $request->segment(1); + $locale = $request->segment(1); // Erste URL-Segment als Locale (z.B. /de/packages -> 'de') + // Unterstützte Sprachen $supportedLocales = ['de', 'en']; if (in_array($locale, $supportedLocales)) { - app()->setLocale($locale); - session()->put('locale', $locale); + // Locale setzen + App::setLocale($locale); + Session::put('locale', $locale); } else { - $locale = session('locale', config('app.locale', 'de')); - app()->setLocale($locale); + // Fallback zu 'de' + $defaultLocale = 'de'; + App::setLocale($defaultLocale); + Session::put('locale', $defaultLocale); + + // Redirect zu default Locale, wenn keine Locale in URL + if (!$locale) { + return redirect("/{$defaultLocale}" . $request->getRequestUri()); + } } return $next($request); diff --git a/docs/prp/04-data-model-migrations.md b/docs/prp/04-data-model-migrations.md index d5673e9..43e9a32 100644 --- a/docs/prp/04-data-model-migrations.md +++ b/docs/prp/04-data-model-migrations.md @@ -97,39 +97,36 @@ use Illuminate\\Database\\Migrations\\Migration; use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Support\\Facades\\Schema; -// Event types (global) Schema::create('event_types', function (Blueprint $table) { $table->id(); - $table->json('name'); + $table->json('name'); // Translatable: { "de": "Hochzeit", "en": "Wedding" } $table->string('slug', 100)->unique(); $table->string('icon', 64)->nullable(); $table->json('settings')->nullable(); $table->timestamps(); }); -// Events (tenant-scoped) Schema::create('events', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); - $table->json('name'); + $table->json('name'); // Translatable: { "de": "Event Name", "en": "Event Name" } $table->date('date'); $table->string('slug'); - $table->json('description')->nullable(); + $table->json('description')->nullable(); // Translatable $table->json('settings')->nullable(); $table->foreignId('event_type_id')->constrained('event_types'); $table->boolean('is_active')->default(true); - $table->string('default_locale', 5)->default('de'); + $table->string('default_locale', 5)->default('de'); // For event-specific i18n fallback $table->timestamps(); $table->unique(['tenant_id', 'slug']); }); -// Emotions (global library) Schema::create('emotions', function (Blueprint $table) { $table->id(); - $table->json('name'); + $table->json('name'); // Translatable: { "de": "Freude", "en": "Joy" } $table->string('icon', 50); $table->string('color', 7); - $table->json('description')->nullable(); + $table->json('description')->nullable(); // Translatable $table->integer('sort_order')->default(0); $table->boolean('is_active')->default(true); }); @@ -141,22 +138,21 @@ Schema::create('emotion_event_type', function (Blueprint $table) { $table->primary(['emotion_id', 'event_type_id']); }); -// Tasks (with optional tenant/event scoping) Schema::create('tasks', function (Blueprint $table) { $table->id(); $table->foreignId('emotion_id')->constrained('emotions'); $table->foreignId('event_type_id')->nullable()->constrained('event_types')->nullOnDelete(); - $table->json('title'); - $table->json('description'); + $table->json('title'); // Translatable + $table->json('description'); // Translatable $table->string('difficulty', 16)->default('easy'); // app enum - $table->json('example_text')->nullable(); + $table->json('example_text')->nullable(); // Translatable $table->integer('sort_order')->default(0); $table->boolean('is_active')->default(true); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete(); $table->string('scope', 16)->default('global'); // global|tenant|event $table->foreignId('event_id')->nullable()->constrained('events')->nullOnDelete(); $table->timestamps(); - $table->unique(['tenant_id', 'emotion_id', 'title']); + $table->unique(['tenant_id', 'emotion_id', 'title->de']); // Example for de fallback; adjust for multi-locale }); // Photos @@ -196,8 +192,8 @@ Schema::create('legal_pages', function (Blueprint $table) { $table->id(); $table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete(); $table->string('slug', 32); // imprint|privacy|terms|custom - $table->json('title'); - $table->json('body_markdown'); + $table->json('title'); // Translatable + $table->json('body_markdown'); // Translatable Markdown content per locale $table->string('locale_fallback', 5)->default('de'); $table->unsignedInteger('version')->default(1); $table->timestamp('effective_from')->nullable(); @@ -211,3 +207,4 @@ Schema::create('legal_pages', function (Blueprint $table) { - Prefer app-level enums (string columns + validation) over DB `ENUM`. - Use `cascadeOnDelete()` only where child data must be removed with parent; otherwise `nullOnDelete()`. - Every tenant-owned table should include `tenant_id` and appropriate composite indexes. +- **i18n Integration**: JSON fields (e.g., `name`, `description`) store locale-specific values as `{ "de": "Text", "en": "Text" }`. Use Laravel's `json` cast or spatie/laravel-translatable for access. Fallback to `default_locale` or global fallback ('de'). Update via Filament resources with locale selectors. Ensure indexes on JSON paths if querying (e.g., `->de` for German titles). diff --git a/docs/prp/07-guest-pwa-routes-components.md b/docs/prp/07-guest-pwa-routes-components.md index 0f36e99..e750f27 100644 --- a/docs/prp/07-guest-pwa-routes-components.md +++ b/docs/prp/07-guest-pwa-routes-components.md @@ -81,7 +81,8 @@ apps/guest-pwa/ usePollStats.ts // polls /events/:slug/stats every 10s usePollGalleryDelta.ts // polls /events/:slug/photos?since=... i18n/ - de.json + config.ts // i18next init with react-i18next, backend loadPath '/lang/{{lng}}/guest.json' + de.json // Namespace: guest (e.g., { "gallery": { "title": "Galerie" } }) en.json main.tsx App.tsx @@ -145,6 +146,7 @@ State & Data - TanStack Query for server data (events, photos); optimistic updates for likes. - Zustand store for local-only state (profile, queue, banners). - IndexedDB for upload queue; CacheStorage for shell/assets. +- i18n: react-i18next; load 'guest' namespace JSON from /lang/{locale}/guest.json; path-based detection for /de/e/:slug, /en/e/:slug; useTranslation('guest') in components. - Polling: focus-aware intervals (10s stats, 30s gallery); use document visibility to pause; backoff on failures. Accessibility & Performance diff --git a/docs/prp/07-guest-pwa.md b/docs/prp/07-guest-pwa.md index df2dc46..b68f044 100644 --- a/docs/prp/07-guest-pwa.md +++ b/docs/prp/07-guest-pwa.md @@ -82,7 +82,7 @@ Technical Notes - Storage: IndexedDB for queue + cache; `CacheStorage` for shell/assets. - Background Sync: use Background Sync API when available; fallback to retry on app open. - Accessibility: large tap targets, high contrast, keyboard support, reduced motion. -- i18n: default `de`, fallback `en`; all strings in locale files; RTL not in MVP. +- i18n: react-i18next with JSON files (`public/lang/{locale}/guest.json`); default `de`, fallback `en`; path-based detection (/de/, /en/); RTL not in MVP. Strings for UI (e.g., gallery, upload, tasks) extracted via i18next-scanner; integrate with prefixed routing and middleware. - Media types: Photos only (no videos) — decision locked for MVP and v1. - Realtime model: periodic polling (no WebSockets). Home counters every 10s; gallery delta every 30s with exponential backoff when tab hidden or offline. diff --git a/docs/prp/12-i18n.md b/docs/prp/12-i18n.md index a8c200d..71d8d54 100644 --- a/docs/prp/12-i18n.md +++ b/docs/prp/12-i18n.md @@ -1,6 +1,78 @@ -# 12 — Internationalization +# 12 — Internationalization (i18n) -- Default locale `de`, fallback `en`. -- Translatable fields stored as JSON (`name`, `description`, `title`, `body_markdown`). -- PWA copy managed via i18n files; glossary maintained centrally. -- Date/number formatting per locale; right-to-left not in MVP. +## 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 `` in `` 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: `` 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**: `` in ``. +- **Canonical**: `` 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). diff --git a/docs/prp/tenant-app-specs/functional-specs.md b/docs/prp/tenant-app-specs/functional-specs.md index fcbf56a..f8db25a 100644 --- a/docs/prp/tenant-app-specs/functional-specs.md +++ b/docs/prp/tenant-app-specs/functional-specs.md @@ -51,7 +51,7 @@ Die App ist API-first und interagiert ausschließlich über den Backend-API-Endp ### Error Handling & UX - **Rate Limits**: 429-Responses handhaben mit Retry-Logic und User-Feedback ("Zu viele Anfragen, versuche es später"). - **Offline Mode**: Degradiertes UI (Read-Only); Sync-Status-Indikator. -- **i18n**: Unterstützung für de/en; Locale aus User-Profile. +- **i18n**: react-i18next mit JSON (`public/lang/{locale}/admin.json`); de/en; Locale aus User-Profile oder URL-Prefix (/de/, /en/); Detection via LanguageDetector; RTL nicht in MVP. ## API-Integration Die App konsumiert den API-Contract aus docs/prp/03-api.md. Schlüssel-Endpunkte: diff --git a/docs/prp/tenant-app-specs/pages-ui.md b/docs/prp/tenant-app-specs/pages-ui.md index ffe7774..b875f48 100644 --- a/docs/prp/tenant-app-specs/pages-ui.md +++ b/docs/prp/tenant-app-specs/pages-ui.md @@ -10,7 +10,7 @@ - **Navigation**: Tabbar unten (Dashboard, Events, Photos, Settings); Side-Menu für Profile/Logout. - **Offline-Indikator**: Banner oben ("Offline-Modus: Änderungen werden synchronisiert"). - **Loading**: Spinner für API-Calls; Skeleton-Screens für Listen. -- **i18n**: Rechts-nach-Links für de/en; Icons von Framework7-Icons (Material). +- **i18n**: LTR für de/en (react-i18next); alle Strings via `t('admin.key')`; Icons von Lucide React (aktuell, nicht Framework7). ## Benötigte Seiten und Komponenten diff --git a/docs/prp/tenant-app-specs/settings-config.md b/docs/prp/tenant-app-specs/settings-config.md index 85198cd..dd29fdc 100644 --- a/docs/prp/tenant-app-specs/settings-config.md +++ b/docs/prp/tenant-app-specs/settings-config.md @@ -8,7 +8,7 @@ Diese Settings werden lokal in der App gespeichert (via Capacitor Preferences oder IndexedDB) und beeinflussen das Verhalten der App: ### Core App Settings -- **language**: String (default: 'de') – UI-Sprache; Sync mit User-Locale. +- **language**: String (default: 'de') – UI-Sprache; Sync mit User-Profile und i18next (react-i18next); Fallback 'en'; URL-Prefix (/de/, /en/) für persistente Wechsel. - **themeMode**: String ('system' | 'light' | 'dark') – Dark Mode-Präferenz; Framework7-Theming. - **offlineMode**: Boolean (default: true) – Aktiviert Offline-Caching und Background-Sync. - **pushNotifications**: Boolean (default: true) – Erlaubt Push-Registrierung. diff --git a/i18next-scanner.config.js b/i18next-scanner.config.js new file mode 100644 index 0000000..b38d478 --- /dev/null +++ b/i18next-scanner.config.js @@ -0,0 +1,45 @@ +module.exports = { + input: [ + 'resources/js/**/*.tsx', + '!resources/js/**/*.d.ts', + '!resources/js/i18n/**' + ], + output: './', + options: { + debug: true, + removeUnusedKeys: true, + sort: true, + lineEnding: '\n', + attr: { + list: ['t'], + translateAttribute: 'i18nKey', + }, + func: { + list: ['t', 'useTranslation'], + extensions: ['.tsx'], + }, + trans: { + component: 'Trans', + i18nKey: 'i18nKey', + extensions: ['.tsx'], + fallbackKey: (ns, value) => value, + }, + lngs: ['de', 'en'], + ns: ['marketing', 'auth', 'guest', 'admin'], + defaultLng: 'de', + defaultNs: 'marketing', + defaultValue: '__STRING_NOT_TRANSLATED__', + resource: { + loadPath: 'public/lang/{{lng}}/{{ns}}.json', + savePath: 'public/lang/{{lng}}/{{ns}}.json', + jsonIndent: 2, + lineEnding: '\n' + }, + nsSeparators: ['.'], + keySeparator: false, + interpolation: { + prefix: '{{', + suffix: '}}' + } + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4de22a6..adc63a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,18 @@ "@inertiajs/react": "^2.1.0", "@playwright/mcp": "^0.0.37", "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", @@ -32,15 +33,20 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", + "date-fns": "^4.1.0", "embla-carousel": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "globals": "^15.14.0", "html5-qrcode": "^2.3.8", + "i18next": "^25.5.3", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^16.0.0", "react-router-dom": "^7.8.2", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", @@ -57,6 +63,7 @@ "eslint-config-prettier": "^10.0.1", "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", + "i18next-scanner": "^4.6.0", "playwright": "^1.55.1", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0", @@ -489,6 +496,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1354,6 +1370,19 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@headlessui/react": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz", @@ -1970,6 +1999,7 @@ "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", @@ -2183,6 +2213,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", @@ -2327,6 +2358,7 @@ "version": "1.2.14", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", @@ -2515,6 +2547,7 @@ "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", @@ -2593,6 +2626,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", @@ -3981,6 +4043,22 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-class-fields": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/acorn-class-fields/-/acorn-class-fields-0.3.7.tgz", + "integrity": "sha512-jdUWSFce0fuADUljmExz4TWpPkxmRW/ZCPRqeeUzbGf0vFUcpQYbyq52l75qGd0oSwwtAepeL6hgb/naRgvcKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-private-class-elements": "^0.2.7" + }, + "engines": { + "node": ">=4.8.2" + }, + "peerDependencies": { + "acorn": "^6 || ^7 || ^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3990,6 +4068,82 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-private-class-elements": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/acorn-private-class-elements/-/acorn-private-class-elements-0.2.7.tgz", + "integrity": "sha512-+GZH2wOKNZOBI4OOPmzpo4cs6mW297sn6fgIk1dUI08jGjhAaEwvC39mN2gJAg2lmAQJ1rBkFqKWonL3Zz6PVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.8.2" + }, + "peerDependencies": { + "acorn": "^6.1.0 || ^7 || ^8" + } + }, + "node_modules/acorn-private-methods": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/acorn-private-methods/-/acorn-private-methods-0.3.3.tgz", + "integrity": "sha512-46oeEol3YFvLSah5m9hGMlNpxDBCEkdceJgf01AjqKYTK9r6HexKs2rgSbLK81pYjZZMonhftuUReGMlbbv05w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-private-class-elements": "^0.2.7" + }, + "engines": { + "node": ">=4.8.2" + }, + "peerDependencies": { + "acorn": "^6 || ^7 || ^8" + } + }, + "node_modules/acorn-stage3": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-stage3/-/acorn-stage3-4.0.0.tgz", + "integrity": "sha512-BR+LaADtA6GTB5prkNqWmlmCLYmkyW0whvSxdHhbupTaro2qBJ95fJDEiRLPUmiACGHPaYyeH9xmNJWdGfXRQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-class-fields": "^0.3.7", + "acorn-private-methods": "^0.3.3", + "acorn-static-class-features": "^0.2.4" + }, + "engines": { + "node": ">=4.8.2" + }, + "peerDependencies": { + "acorn": "^7.4 || ^8" + } + }, + "node_modules/acorn-static-class-features": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/acorn-static-class-features/-/acorn-static-class-features-0.2.4.tgz", + "integrity": "sha512-5X4mpYq5J3pdndLmIB0+WtFd/mKWnNYpuTlTzj32wUu/PMmEGOiayQ5UrqgwdBNiaZBtDDh5kddpP7Yg2QaQYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn-private-class-elements": "^0.2.7" + }, + "engines": { + "node": ">=4.8.2" + }, + "peerDependencies": { + "acorn": "^6.1.0 || ^7 || ^8" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -4048,6 +4202,20 @@ "node": ">=14" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4248,12 +4416,55 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/bare-events": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.7.0.tgz", + "integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.5.tgz", @@ -4263,6 +4474,18 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -4339,6 +4562,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4520,6 +4768,31 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4649,6 +4922,13 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -4690,6 +4970,35 @@ } } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4770,6 +5079,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -5025,6 +5344,13 @@ "node": ">=10.13.0" } }, + "node_modules/ensure-type": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ensure-type/-/ensure-type-1.5.1.tgz", + "integrity": "sha512-Dxe+mVF4MupV6eueWiFa6hUd9OL9lIM2/LqR40k1P+dwG+G2il2UigXTU9aQlaw+Y/N0BKSaTofNw73htTbC5g==", + "dev": true, + "license": "MIT" + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -5035,6 +5361,13 @@ "node": ">=6" } }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true, + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -5464,6 +5797,20 @@ "node": ">=4" } }, + "node_modules/esprima-next": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/esprima-next/-/esprima-next-5.8.4.tgz", + "integrity": "sha512-8nYVZ4ioIH4Msjb/XmhnBdz5WRRBaYqevKa1cv9nGJdCehMbzZCPNEEnqfLCZVetUVrUPEcb5IYyu1GG4hFqgg==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -5516,6 +5863,16 @@ "node": ">= 0.6" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -5664,6 +6021,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5927,6 +6291,20 @@ "node": ">=14.14" } }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6122,6 +6500,26 @@ "node": ">=10.13.0" } }, + "node_modules/glob-stream": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.3.tgz", + "integrity": "sha512-fqZVj22LtFJkHODT+M4N1RJQ3TjnnQhfE9GwZI8qXscYarnhpip70poMldRnP8ipQ/w0B621kOhfc53/J9bd/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -6181,6 +6579,67 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "through2": "^2.0.1" + } + }, + "node_modules/gulp-sort/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-sort/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gulp-sort/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp-sort/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/gulp-sort/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6271,6 +6730,15 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html5-qrcode": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", @@ -6328,6 +6796,99 @@ "node": ">=18.18.0" } }, + "node_modules/i18next": { + "version": "25.5.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz", + "integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/i18next-scanner": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/i18next-scanner/-/i18next-scanner-4.6.0.tgz", + "integrity": "sha512-I/xKcwKfii3L3is3bUvfaIU0QA/wYhpZnjppfrzyb61QQddxkcpspASEtmfnxSYvE6yIaAxDlIxg0EHV7mxssg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.0.4", + "acorn-jsx": "^5.3.1", + "acorn-stage3": "^4.0.0", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "clone-deep": "^4.0.0", + "commander": "^9.0.0", + "deepmerge": "^4.0.0", + "ensure-type": "^1.5.0", + "eol": "^0.9.1", + "esprima-next": "^5.7.0", + "gulp-sort": "^2.0.0", + "i18next": "*", + "lodash": "^4.0.0", + "parse5": "^6.0.0", + "sortobject": "^4.0.0", + "through2": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-fs": "^4.0.0" + }, + "bin": { + "i18next-scanner": "bin/cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/i18next-scanner/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6341,6 +6902,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6627,6 +7209,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -6697,6 +7289,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -6836,6 +7441,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -6891,6 +7506,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7017,6 +7642,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -7045,6 +7680,16 @@ "vite": "^7.0.0" } }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7685,6 +8330,29 @@ "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -8077,6 +8745,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -8376,6 +9051,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8535,6 +9217,32 @@ "react": "^19.1.1" } }, + "node_modules/react-i18next": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz", + "integrity": "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 25.5.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8653,6 +9361,21 @@ } } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -8712,6 +9435,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true, + "license": "ISC" + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8746,6 +9486,19 @@ "node": ">=4" } }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -9144,6 +9897,19 @@ "shadcn": "dist/index.js" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9264,6 +10030,19 @@ "dev": true, "license": "MIT" }, + "node_modules/sortobject": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/sortobject/-/sortobject-4.17.0.tgz", + "integrity": "sha512-gzx7USv55AFRQ7UCWJHHauwD/ptUHF9MLXCGO3f5M9zauDPZ/4a9H6/VVbOXefdpEoI1unwB/bArHIVMbWBHmA==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9318,6 +10097,28 @@ "node": ">= 0.4" } }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -9325,6 +10126,16 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9580,6 +10391,36 @@ "node": ">=18" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -9668,6 +10509,19 @@ "node": ">=8.0" } }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -9691,6 +10545,12 @@ "node": ">=16" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -10058,6 +10918,23 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -10068,6 +10945,80 @@ "node": ">= 0.8" } }, + "node_modules/vinyl": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^2.1.2", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.2.tgz", + "integrity": "sha512-XRFwBLLTl8lRAOYiBqxY279wY46tVxLaRhSwo3GzKEuLz1giffsOquWWboD/haGf5lx+JyTigCFfe7DWHoARIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.3", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.1", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/vite": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", @@ -10177,6 +11128,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -10187,6 +11147,22 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10319,6 +11295,16 @@ "dev": true, "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3162407..c32f3a6 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "build": "vite build", "build:ssr": "vite build && vite build --ssr", "dev": "vite", + "extract:i18n": "i18next-scanner", "format": "prettier --write resources/", "format:check": "prettier --check resources/", "lint": "eslint . --fix", @@ -19,6 +20,7 @@ "eslint-config-prettier": "^10.0.1", "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", + "i18next-scanner": "^4.6.0", "playwright": "^1.55.1", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.1.0", @@ -31,17 +33,18 @@ "@inertiajs/react": "^2.1.0", "@playwright/mcp": "^0.0.37", "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", @@ -54,15 +57,20 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", + "date-fns": "^4.1.0", "embla-carousel": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", "globals": "^15.14.0", "html5-qrcode": "^2.3.8", + "i18next": "^25.5.3", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-i18next": "^16.0.0", "react-router-dom": "^7.8.2", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", diff --git a/public/lang/de/auth.json b/public/lang/de/auth.json new file mode 100644 index 0000000..7b75399 --- /dev/null +++ b/public/lang/de/auth.json @@ -0,0 +1,57 @@ +{ + "failed": "Diese Anmeldedaten wurden nicht gefunden.", + "password": "Das Passwort ist falsch.", + "throttle": "Zu viele Login-Versuche. Bitte versuche es in :seconds Sekunden erneut.", + "login": { + "title": "Anmelden", + "description": "Geben Sie Ihre E-Mail und Ihr Passwort ein, um sich anzumelden.", + "email": "E-Mail-Adresse", + "email_placeholder": "email@example.com", + "password": "Passwort", + "password_placeholder": "Passwort", + "remember": "Angemeldet bleiben", + "submit": "Anmelden", + "forgot": "Passwort vergessen?", + "no_account": "Kein Account?", + "sign_up": "Registrieren" + }, + "register": { + "title": "Registrieren", + "welcome": "Willkommen bei Fotospiel – Erstellen Sie Ihren Account", + "description": "Registrierung ermöglicht Zugriff auf Events, Galerien und personalisierte Features.", + "package_name": "Paket", + "package_description": "Beschreibung", + "package_price_free": "Kostenlos", + "package_price": "{{price}} €", + "first_name": "Vorname", + "first_name_placeholder": "Vorname", + "last_name": "Nachname", + "last_name_placeholder": "Nachname", + "email": "E-Mail-Adresse", + "email_placeholder": "email@example.com", + "address": "Adresse", + "address_placeholder": "Adresse", + "phone": "Telefonnummer", + "phone_placeholder": "Telefonnummer", + "username": "Benutzername", + "username_placeholder": "Benutzername", + "password": "Passwort", + "password_placeholder": "Passwort", + "confirm_password": "Passwort bestätigen", + "confirm_password_placeholder": "Passwort bestätigen", + "privacy_consent": "Ich stimme der Datenschutzerklärung zu und akzeptiere die Verarbeitung meiner persönlichen Daten.", + "submit": "Account erstellen", + "has_account": "Bereits registriert?", + "login": "Anmelden", + "errors_title": "Fehler bei der Registrierung:", + "privacy_policy": "Datenschutzerklärung" + }, + "header": { + "login": "Anmelden", + "register": "Registrieren" + }, + "verification": { + "notice": "Bitte bestätigen Sie Ihre E-Mail-Adresse.", + "resend": "E-Mail erneut senden" + } +} \ No newline at end of file diff --git a/public/lang/de/marketing.json b/public/lang/de/marketing.json new file mode 100644 index 0000000..abeda9c --- /dev/null +++ b/public/lang/de/marketing.json @@ -0,0 +1,266 @@ +{ + "home": { + "title": "Startseite - Fotospiel", + "hero_title": "Fotospiel", + "hero_description": "Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren – einfach, mobil und datenschutzkonform. Besser als Konkurrenz, geliebt von Tausenden.", + "cta_explore": "Pakete entdecken", + "hero_image_alt": "Event-Fotos mit QR-Code", + "how_title": "So funktioniert es", + "step1_title": "Paket wählen", + "step1_desc": "Wähle das passende Paket für dein Event.", + "step2_title": "QR-Code teilen", + "step2_desc": "Teile den QR-Code mit deinen Gästen.", + "step3_title": "Fotos sammeln", + "step3_desc": "Gäste laden Fotos hoch – sicher und einfach.", + "features_title": "Warum Fotospiel?", + "feature1_title": "Sicher & Datenschutzkonform", + "feature1_desc": "GDPR-konform, keine PII-Speicherung.", + "feature2_title": "Mobil & PWA", + "feature2_desc": "Funktioniert offline, installierbar wie App.", + "feature3_title": "Einfach zu bedienen", + "feature3_desc": "Intuitive UI für Gäste und Organisatoren.", + "packages_title": "Unsere Pakete", + "view_details": "Details ansehen", + "all_packages": "Alle Pakete ansehen", + "contact_title": "Kontakt", + "name_label": "Name", + "email_label": "E-Mail", + "message_label": "Nachricht", + "sending": "Wird gesendet...", + "send": "Senden", + "testimonials_title": "Was unsere Kunden sagen", + "testimonial1": "Toll für Hochzeiten! Einfach und sicher.", + "testimonial2": "Beste App für Event-Fotos.", + "testimonial3": "Schnell und benutzerfreundlich.", + "faq_title": "Häufige Fragen", + "faq1_q": "Ist es kostenlos?", + "faq1_a": "Ja, es gibt ein kostenloses Paket für kleine Events.", + "faq2_q": "Wie funktioniert der QR-Code?", + "faq2_a": "Gäste scannen und laden Fotos hoch – einfach!" + }, + "packages": { + "title": "Unsere Packages", + "hero_title": "Entdecken Sie unsere flexiblen Packages", + "hero_description": "Von kostenlosem Einstieg bis Premium-Features: Passen Sie Ihr Event-Paket an Ihre Bedürfnisse an. Einfach, sicher und skalierbar.", + "cta_explore": "Pakete entdecken", + "tab_endcustomer": "Endkunden", + "tab_reseller": "Reseller & Agenturen", + "section_endcustomer": "Packages für Endkunden (Einmalkauf pro Event)", + "section_reseller": "Packages für Reseller (Jährliches Abo)", + "free": "Kostenlos", + "one_time": "Einmalkauf", + "subscription": "Abo", + "year": "Jahr", + "max_photos": "Fotos", + "max_guests": "Gäste", + "gallery_days": "Tage Galerie", + "max_events_year": "Events/Jahr", + "buy_now": "Jetzt kaufen", + "subscribe_now": "Jetzt abonnieren", + "register_buy": "Registrieren und kaufen", + "register_subscribe": "Registrieren und abonnieren", + "faq_title": "Häufige Fragen zu Packages", + "faq_q1": "Was ist ein Package?", + "faq_a1": "Ein Package definiert Limits und Features für Ihr Event, z.B. Anzahl Fotos und Galerie-Dauer.", + "faq_q2": "Kann ich upgraden?", + "faq_a2": "Ja, wählen Sie bei Event-Erstellung ein höheres Package oder upgraden Sie später.", + "faq_q3": "Was passiert bei Ablauf?", + "faq_a3": "Die Galerie bleibt lesbar, aber Uploads sind blockiert. Verlängern Sie einfach.", + "faq_q4": "Zahlungssicher?", + "faq_a4": "Ja, via Stripe oder PayPal – sicher und GDPR-konform.", + "final_cta": "Bereit für Ihr nächstes Event?", + "contact_us": "Kontaktieren Sie uns", + "feature_live_slideshow": "Live-Slideshow", + "feature_analytics": "Analytics", + "feature_watermark": "Wasserzeichen", + "feature_branding": "Branding", + "feature_support": "Support", + "feature_basic_uploads": "Basis-Uploads", + "feature_unlimited_sharing": "Unbegrenztes Teilen", + "feature_no_watermark": "Kein Wasserzeichen", + "feature_custom_tasks": "Benutzerdefinierte Tasks", + "feature_advanced_analytics": "Erweiterte Analytics", + "feature_priority_support": "Priorisierter Support", + "feature_limited_sharing": "Begrenztes Teilen", + "feature_no_branding": "Kein Branding", + "feature_0": "Basis-Feature", + "feature_reseller_dashboard": "Reseller-Dashboard", + "feature_custom_branding": "Benutzerdefiniertes Branding", + "feature_advanced_reporting": "Erweiterte Berichterstattung", + "for_endcustomers": "Für Endkunden", + "for_resellers": "Für Reseller", + "details_show": "Details anzeigen", + "comparison_title": "Packages vergleichen", + "price": "Preis", + "max_photos_label": "Max. Fotos", + "max_guests_label": "Max. Gäste", + "gallery_days_label": "Galerie-Tage", + "watermark_label": "Wasserzeichen", + "no_watermark": "Kein Wasserzeichen", + "custom_branding": "Benutzerdefiniertes Branding", + "max_tenants": "Max. Tenants", + "max_events": "Max. Events/Jahr", + "faq_free": "Was ist das Free Package?", + "faq_upgrade": "Kann ich upgraden?", + "faq_reseller": "Was für Reseller?", + "faq_payment": "Zahlung sicher?", + "faq_free_desc": "Das Free Package bietet grundlegende Features für kleine Events mit begrenzter Anzahl an Fotos und Gästen.", + "faq_upgrade_desc": "Ja, Sie können jederzeit upgraden, um mehr Features und Limits zu erhalten. Der Upgrade ist nahtlos und Ihre Daten bleiben erhalten.", + "faq_reseller_desc": "Reseller-Packages sind jährliche Abos für Agenturen, die mehrere Events verwalten. Inklusive Dashboard und Branding-Optionen.", + "faq_payment_desc": "Alle Zahlungen werden über sichere Provider wie Stripe oder PayPal abgewickelt. Ihre Daten sind GDPR-konform geschützt.", + "testimonials": { + "anna": "Fotospiel hat unsere Hochzeit perfekt gemacht! Die Gäste konnten einfach Fotos teilen, und die Galerie war ein Hit.", + "max": "Als Event-Organisator liebe ich die Analytics und das einfache Branding. Super für Firmenevents!", + "lisa": "Kostenloses Paket für Geburtstage – einfach und sicher. Kein Stress mit Apps!" + }, + "what_customers_say": "Was unsere Kunden sagen", + "close": "Schließen", + "to_order": "Jetzt bestellen", + "currency": { + "euro": "€" + }, + "view_details": "Details ansehen", + "feature": "Feature" + }, + "blog": { + "title": "Fotospiel - Blog", + "hero_title": "Fotospiel Blog", + "hero_description": "Tipps, News und Guides für perfekte Event-Fotos mit QR-Codes, PWA und mehr. Bleiben Sie informiert!", + "hero_cta": "Mehr über Fotospiel", + "posts_title": "Aktuelle Blog-Beiträge", + "by": "Von", + "team": "Fotospiel Team", + "published_at": "Veröffentlicht am", + "read_more": "Lesen", + "back": "Zurück zum Blog", + "empty": "Noch keine Beiträge verfügbar. Bleiben Sie dran!", + "our_blog": "Unser Blog", + "latest_posts": "Neueste Beiträge", + "no_posts": "Keine Beiträge verfügbar.", + "read_more_link": "Mehr lesen" + }, + "kontakt": { + "title": "Kontakt - Fotospiel", + "description": "Haben Sie Fragen? Schreiben Sie uns!", + "name": "Name", + "email": "E-Mail", + "message": "Nachricht", + "sending": "Wird gesendet...", + "send": "Senden", + "back_home": "Zurück zur Startseite" + }, + "occasions": { + "title": "Fotospiel für :type", + "hero_title": "Fotospiel für :type", + "hero_description": "Sammeln Sie unvergessliche Fotos von Ihren Gästen mit QR-Codes. Perfekt für :type – einfach, mobil und datenschutzkonform.", + "cta": "Paket wählen", + "weddings": { + "title": "Hochzeiten mit Fotospiel", + "description": "Fangen Sie romantische Momente ein: Gäste teilen Fotos via QR, wählen Emotionen wie 'Romantisch' oder 'Freudig'. Besser als traditionelle Fotoboxen.", + "benefits_title": "Vorteile für Hochzeiten", + "benefit1": "QR-Code für Gäste: Einfaches Teilen ohne App-Download.", + "benefit2": "Emotion-Filter: Kategorisieren von Fotos (z.B. 'Tanz', 'Kuss').", + "benefit3": "Private Galerie: Nur genehmigte Fotos sichtbar.", + "benefit4": "Download: Hochauflösend für Album.", + "image_alt": "Hochzeitsfotos" + }, + "birthdays": { + "title": "Geburtstage feiern", + "description": "Lassen Sie Freunde und Familie spontane Fotos teilen. QR auf der Torte – Spaß garantiert!", + "benefits_title": "Vorteile für Geburtstage", + "benefit1": "Schnelle Uploads: Kamera oder Galerie.", + "benefit2": "Likes & Shares: Beliebte Momente hervorheben.", + "benefit3": "Offline-fähig: PWA funktioniert ohne Internet.", + "benefit4": "Anonym: Keine Registrierung erforderlich.", + "image_alt": "Geburtstagsfotos" + }, + "corporate": { + "title": "Firmenevents professionell", + "description": "Networking und Team-Building: Fotos zentral sammeln, Highlights intern teilen.", + "benefits_title": "Vorteile für Firmenevents", + "benefit1": "QR an Ständen: Gäste fotografieren sich selbst.", + "benefit2": "Kategorien: 'Team', 'Netzwerk', 'Präsentation'.", + "benefit3": "Export: Für Social Media oder Intranet.", + "benefit4": "GDPR-sicher: Keine PII gespeichert.", + "image_alt": "Firmenevent-Fotos" + }, + "family": { + "title": "Familienfeiern", + "description": "Von Taufen bis Jubiläen: Erinnerungen von allen Verwandten sammeln.", + "benefits_title": "Vorteile für Familienfeiern", + "benefit1": "Einfach für alle Altersgruppen: Große Schrift, touch-freundlich.", + "benefit2": "Emotionen: 'Familie', 'Glück', 'Einheit'.", + "benefit3": "Teilen: Via Link oder QR für Nachfeier.", + "benefit4": "Unbegrenzt: Im Premium-Plan.", + "image_alt": "Familienfotos" + }, + "not_found": "Anlass nicht gefunden.", + "hochzeit_title": "Hochzeit – Perfekte Gastfotos mit QR", + "hochzeit_desc": "Machen Sie Ihre Hochzeit unvergesslich mit Fotospiel. Gäste teilen Fotos einfach via QR-Code – sicher, privat und in Echtzeit. Von Zeremonie bis Party, alle Momente zentral gesammelt.", + "hochzeit_feature1": "Live-Slideshow für Gäste", + "hochzeit_feature2": "Emotion-basierte Foto-Filter", + "hochzeit_feature3": "Unbegrenzte Galerie für 30 Tage", + "hochzeit_cta": "Hochzeitspaket wählen", + "geburtstag_title": "Geburtstag – Feiern mit geteilten Erinnerungen", + "geburtstag_desc": "Feiern Sie Geburtstage mit Fotospiel! QR-Code für Gäste zum Hochladen von Fotos – von Kinder- bis Erwachsenen-Partys. Einfach teilen, liken und downloaden.", + "geburtstag_feature1": "Kostenloses Paket für kleine Partys", + "geburtstag_feature2": "Schnelle Uploads via PWA", + "geburtstag_feature3": "Privat und datenschutzkonform", + "geburtstag_cta": "Geburtstagspaket entdecken", + "firmenevent_title": "Firmenevent – Team-Events und Konferenzen", + "firmenevent_desc": "Für Firmenevents, Teambuildings und Konferenzen: Fotospiel sammelt alle Fotos zentral via QR. Branding, Analytics und sichere Galerie für Ihr Unternehmen.", + "firmenevent_feature1": "Benutzerdefiniertes Branding für Firmenlogo", + "firmenevent_feature2": "Erweiterte Analytics", + "firmenevent_feature3": "Priorisierter Support", + "firmenevent_cta": "Firmenpaket anfragen" + }, + "success": { + "title": "Erfolg", + "verify_email": "E-Mail verifizieren", + "check_email": "Überprüfen Sie Ihre E-Mail auf den Verifizierungslink.", + "redirecting": "Weiterleitung zum Admin-Bereich...", + "complete_purchase": "Kauf abschließen", + "login_to_continue": "Melden Sie sich an, um fortzufahren.", + "loading": "Laden...", + "email_verify_title": "E-Mail verifizieren", + "email_verify_desc": "Bitte überprüfen Sie Ihre E-Mail und klicken Sie auf den Verifizierungslink.", + "resend_verification": "Verifizierung erneut senden", + "already_registered": "Bereits registriert? Anmelden", + "purchase_complete_title": "Kauf abschließen", + "purchase_complete_desc": "Melden Sie sich an, um fortzufahren.", + "login": "Anmelden", + "no_account": "Kein Konto? Registrieren" + }, + "blog_show": { + "title_suffix": " - Fotospiel Blog", + "by_author": "Von", + "published_on": "Veröffentlicht am", + "back_to_blog": "Zurück zum Blog" + }, + "nav": { + "home": "Startseite", + "how_it_works": "So funktioniert es", + "features": "Features", + "occasions": "Anlässe", + "blog": "Blog", + "packages": "Pakete", + "contact": "Kontakt", + "discover_packages": "Pakete entdecken", + "privacy": "Datenschutz", + "impressum": "Impressum" + }, + "footer": { + "company": "Fotospiel GmbH", + "rights_reserved": "Alle Rechte vorbehalten" + }, + "register": { + "free": "Kostenlos" + }, + "currency": { + "euro": "€" + }, + "meta": { + "title": "Fotospiel - Sammle Gastfotos für Events mit QR-Codes", + "description": "Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren – einfach, mobil und datenschutzkonform." + } +} \ No newline at end of file diff --git a/public/lang/en/auth.json b/public/lang/en/auth.json new file mode 100644 index 0000000..18cf943 --- /dev/null +++ b/public/lang/en/auth.json @@ -0,0 +1,57 @@ +{ + "failed": "These credentials do not match our records.", + "password": "The provided password is incorrect.", + "throttle": "Too many login attempts. Please try again in :seconds seconds.", + "login": { + "title": "Log in", + "description": "Enter your email and password below to log in.", + "email": "Email address", + "email_placeholder": "email@example.com", + "password": "Password", + "password_placeholder": "Password", + "remember": "Remember me", + "submit": "Log in", + "forgot": "Forgot password?", + "no_account": "Don't have an account?", + "sign_up": "Sign up" + }, + "register": { + "title": "Register", + "welcome": "Welcome to Fotospiel – Create your account", + "description": "Registration enables access to events, galleries, and personalized features.", + "package_name": "Package", + "package_description": "Description", + "package_price_free": "Free", + "package_price": "{{price}} €", + "first_name": "First name", + "first_name_placeholder": "First name", + "last_name": "Last name", + "last_name_placeholder": "Last name", + "email": "Email address", + "email_placeholder": "email@example.com", + "address": "Address", + "address_placeholder": "Address", + "phone": "Phone number", + "phone_placeholder": "Phone number", + "username": "Username", + "username_placeholder": "Username", + "password": "Password", + "password_placeholder": "Password", + "confirm_password": "Confirm password", + "confirm_password_placeholder": "Confirm password", + "privacy_consent": "I agree to the privacy policy and accept the processing of my personal data.", + "submit": "Create account", + "has_account": "Already registered?", + "login": "Log in", + "errors_title": "Registration errors:", + "privacy_policy": "Privacy policy" + }, + "header": { + "login": "Log in", + "register": "Register" + }, + "verification": { + "notice": "Please confirm your email address.", + "resend": "Resend email" + } +} \ No newline at end of file diff --git a/public/lang/en/marketing.json b/public/lang/en/marketing.json new file mode 100644 index 0000000..9115cae --- /dev/null +++ b/public/lang/en/marketing.json @@ -0,0 +1,257 @@ +{ + "home": { + "title": "Home - Fotospiel", + "hero_title": "Fotospiel", + "hero_description": "Collect guest photos for events with QR codes. Our secure PWA platform for guests and organizers – simple, mobile, and privacy-compliant. Better than competitors, loved by thousands.", + "cta_explore": "Discover Packages", + "hero_image_alt": "Event photos with QR code", + "how_title": "How it works", + "step1_title": "Choose Package", + "step1_desc": "Choose the right package for your event.", + "step2_title": "Share QR Code", + "step2_desc": "Share the QR code with your guests.", + "step3_title": "Collect Photos", + "step3_desc": "Guests upload photos – secure and easy.", + "features_title": "Why Fotospiel?", + "feature1_title": "Secure & Privacy Compliant", + "feature1_desc": "GDPR compliant, no PII storage.", + "feature2_title": "Mobile & PWA", + "feature2_desc": "Works offline, installable like an app.", + "feature3_title": "Easy to Use", + "feature3_desc": "Intuitive UI for guests and organizers.", + "packages_title": "Our Packages", + "view_details": "View Details", + "all_packages": "View All Packages", + "contact_title": "Contact", + "name_label": "Name", + "email_label": "Email", + "message_label": "Message", + "sending": "Sending...", + "send": "Send", + "testimonials_title": "What our customers say", + "testimonial1": "Great for weddings! Simple and secure.", + "testimonial2": "Best app for event photos.", + "testimonial3": "Fast and user-friendly.", + "faq_title": "Frequently Asked Questions", + "faq1_q": "Is it free?", + "faq1_a": "Yes, there is a free package for small events.", + "faq2_q": "How does the QR code work?", + "faq2_a": "Guests scan and upload photos – easy!" + }, + "packages": { + "title": "Our Packages", + "hero_title": "Discover our flexible Packages", + "hero_description": "From free entry to premium features: Tailor your event package to your needs. Simple, secure, and scalable.", + "cta_explore": "Discover Packages", + "tab_endcustomer": "End Customers", + "tab_reseller": "Resellers & Agencies", + "section_endcustomer": "Packages for End Customers (One-time purchase per event)", + "section_reseller": "Packages for Resellers (Annual Subscription)", + "free": "Free", + "one_time": "One-time purchase", + "subscription": "Subscription", + "year": "Year", + "max_photos": "Photos", + "max_guests": "Guests", + "gallery_days": "Gallery Days", + "max_events_year": "Events/Year", + "buy_now": "Buy Now", + "subscribe_now": "Subscribe Now", + "register_buy": "Register and Buy", + "register_subscribe": "Register and Subscribe", + "faq_title": "Frequently Asked Questions about Packages", + "faq_q1": "What is a Package?", + "faq_a1": "A Package defines limits and features for your event, e.g. number of photos and gallery duration.", + "faq_q2": "Can I upgrade?", + "faq_a2": "Yes, choose a higher package when creating the event or upgrade later.", + "faq_q3": "What happens when it expires?", + "faq_a3": "The gallery remains readable, but uploads are blocked. Simply extend.", + "faq_q4": "Payment secure?", + "faq_a4": "Yes, via Stripe or PayPal – secure and GDPR compliant.", + "final_cta": "Ready for your next event?", + "contact_us": "Contact Us", + "feature_live_slideshow": "Live Slideshow", + "feature_analytics": "Analytics", + "feature_watermark": "Watermark", + "feature_branding": "Branding", + "feature_support": "Support", + "feature_basic_uploads": "Basic Uploads", + "feature_unlimited_sharing": "Unlimited Sharing", + "feature_no_watermark": "No Watermark", + "feature_custom_tasks": "Custom Tasks", + "feature_advanced_analytics": "Advanced Analytics", + "feature_priority_support": "Priority Support", + "feature_limited_sharing": "Limited Sharing", + "feature_no_branding": "No Branding", + "feature_0": "Basic Feature", + "feature_reseller_dashboard": "Reseller Dashboard", + "feature_custom_branding": "Custom Branding", + "feature_advanced_reporting": "Advanced Reporting", + "for_endcustomers": "For End Customers", + "for_resellers": "For Resellers", + "details_show": "Show Details", + "comparison_title": "Compare Packages", + "price": "Price", + "max_photos_label": "Max. Photos", + "max_guests_label": "Max. Guests", + "gallery_days_label": "Gallery Days", + "watermark_label": "Watermark", + "no_watermark": "No Watermark", + "custom_branding": "Custom Branding", + "max_tenants": "Max. Tenants", + "max_events": "Max. Events/Year", + "faq_free": "What is the Free Package?", + "faq_upgrade": "Can I upgrade?", + "faq_reseller": "What for Resellers?", + "faq_payment": "Payment secure?", + "testimonials": { + "anna": "Fotospiel made our wedding perfect! Guests could easily share photos, and the gallery was a hit.", + "max": "As an event organizer, I love the analytics and easy branding. Great for corporate events!", + "lisa": "Free package for birthdays – simple and secure. No app hassle!" + }, + "what_customers_say": "What our customers say", + "close": "Close", + "to_order": "Order Now" + }, + "blog": { + "title": "Fotospiel - Blog", + "hero_title": "Fotospiel Blog", + "hero_description": "Tips, news, and guides for perfect event photos with QR codes, PWA, and more. Stay informed!", + "hero_cta": "More about Fotospiel", + "posts_title": "Latest Blog Posts", + "by": "By", + "team": "Fotospiel Team", + "published_at": "Published on", + "read_more": "Read More", + "back": "Back to Blog", + "empty": "No posts available yet. Stay tuned!", + "our_blog": "Our Blog", + "latest_posts": "Latest Posts", + "no_posts": "No posts available.", + "read_more_link": "Read More" + }, + "kontakt": { + "title": "Contact - Fotospiel", + "description": "Have questions? Write to us!", + "name": "Name", + "email": "Email", + "message": "Message", + "sending": "Sending...", + "send": "Send", + "back_home": "Back to Home" + }, + "occasions": { + "title": "Fotospiel for :type", + "hero_title": "Fotospiel for :type", + "hero_description": "Collect unforgettable photos from your guests with QR codes. Perfect for :type – simple, mobile, and privacy-compliant.", + "cta": "Choose Package", + "weddings": { + "title": "Weddings with Fotospiel", + "description": "Capture romantic moments: Guests share photos via QR, choose emotions like 'Romantic' or 'Joyful'. Better than traditional photo booths.", + "benefits_title": "Benefits for Weddings", + "benefit1": "QR Code for Guests: Easy sharing without app download.", + "benefit2": "Emotion Filter: Categorize photos (e.g. 'Dance', 'Kiss').", + "benefit3": "Private Gallery: Only approved photos visible.", + "benefit4": "Download: High-res for album.", + "image_alt": "Wedding photos" + }, + "birthdays": { + "title": "Celebrate Birthdays", + "description": "Let friends and family share spontaneous photos. QR on the cake – fun guaranteed!", + "benefits_title": "Benefits for Birthdays", + "benefit1": "Quick Uploads: Camera or gallery.", + "benefit2": "Likes & Shares: Highlight popular moments.", + "benefit3": "Offline-capable: PWA works without internet.", + "benefit4": "Anonymous: No registration required.", + "image_alt": "Birthday photos" + }, + "corporate": { + "title": "Corporate Events Professional", + "description": "Networking and team-building: Collect photos centrally, share highlights internally.", + "benefits_title": "Benefits for Corporate Events", + "benefit1": "QR at Booths: Guests photograph themselves.", + "benefit2": "Categories: 'Team', 'Network', 'Presentation'.", + "benefit3": "Export: For social media or intranet.", + "benefit4": "GDPR-secure: No PII stored.", + "image_alt": "Corporate event photos" + }, + "family": { + "title": "Family Celebrations", + "description": "From baptisms to anniversaries: Collect memories from all relatives.", + "benefits_title": "Benefits for Family Celebrations", + "benefit1": "Easy for all ages: Large font, touch-friendly.", + "benefit2": "Emotions: 'Family', 'Happiness', 'Unity'.", + "benefit3": "Share: Via link or QR for after-party.", + "benefit4": "Unlimited: In premium plan.", + "image_alt": "Family photos" + }, + "not_found": "Event type not found.", + "hochzeit_title": "Wedding – Perfect Guest Photos with QR", + "hochzeit_desc": "Make your wedding unforgettable with Fotospiel. Guests share photos easily via QR code – secure, private, and in real-time. From ceremony to party, all moments centrally collected.", + "hochzeit_feature1": "Live Slideshow for Guests", + "hochzeit_feature2": "Emotion-based Photo Filters", + "hochzeit_feature3": "Unlimited Gallery for 30 Days", + "hochzeit_cta": "Choose Wedding Package", + "geburtstag_title": "Birthday – Celebrate with Shared Memories", + "geburtstag_desc": "Celebrate birthdays with Fotospiel! QR code for guests to upload photos – from kids' to adult parties. Easy to share, like, and download.", + "geburtstag_feature1": "Free Package for Small Parties", + "geburtstag_feature2": "Quick Uploads via PWA", + "geburtstag_feature3": "Private and Privacy Compliant", + "geburtstag_cta": "Discover Birthday Package", + "firmenevent_title": "Corporate Event – Team Events and Conferences", + "firmenevent_desc": "For corporate events, team buildings, and conferences: Fotospiel collects all photos centrally via QR. Branding, analytics, and secure gallery for your company.", + "firmenevent_feature1": "Custom Branding for Company Logo", + "firmenevent_feature2": "Advanced Analytics", + "firmenevent_feature3": "Priority Support", + "firmenevent_cta": "Request Corporate Package" + }, + "success": { + "title": "Success", + "verify_email": "Verify Email", + "check_email": "Check your email for the verification link.", + "redirecting": "Redirecting to Admin Area...", + "complete_purchase": "Complete Purchase", + "login_to_continue": "Log in to continue.", + "loading": "Loading...", + "email_verify_title": "Verify Email", + "email_verify_desc": "Please check your email and click the verification link.", + "resend_verification": "Resend Verification", + "already_registered": "Already registered? Log in", + "purchase_complete_title": "Complete Purchase", + "purchase_complete_desc": "Log in to continue.", + "login": "Log In", + "no_account": "No Account? Register" + }, + "blog_show": { + "title_suffix": " - Fotospiel Blog", + "by_author": "By", + "published_on": "Published on", + "back_to_blog": "Back to Blog" + }, + "nav": { + "home": "Home", + "how_it_works": "How it Works", + "features": "Features", + "occasions": "Occasions", + "blog": "Blog", + "packages": "Packages", + "contact": "Contact", + "discover_packages": "Discover Packages", + "privacy": "Privacy", + "impressum": "Imprint" + }, + "footer": { + "company": "Fotospiel GmbH", + "rights_reserved": "All Rights Reserved" + }, + "register": { + "free": "Free" + }, + "currency": { + "euro": "€" + }, + "meta": { + "title": "Fotospiel - Collect Guest Photos for Events with QR Codes", + "description": "Collect guest photos for events with QR codes. Our secure PWA platform for guests and organizers – simple, mobile, and privacy-compliant." + } +} \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..10c9d32 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,79 @@ + + + + https://fotospiel.app/ + 2025-10-02 + weekly + 1.0 + + + + + https://fotospiel.app/de/ + 2025-10-02 + weekly + 1.0 + + + + + https://fotospiel.app/en/ + 2025-10-02 + weekly + 1.0 + + + + + https://fotospiel.app/de/packages + 2025-10-02 + monthly + 0.8 + + + + + https://fotospiel.app/en/packages + 2025-10-02 + monthly + 0.8 + + + + + https://fotospiel.app/de/blog + 2025-10-02 + daily + 0.7 + + + + + https://fotospiel.app/en/blog + 2025-10-02 + daily + 0.7 + + + + + https://fotospiel.app/de/kontakt + 2025-10-02 + monthly + 0.6 + + + + + https://fotospiel.app/en/kontakt + 2025-10-02 + monthly + 0.6 + + + + \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index 756a69f..abc073c 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -260,3 +260,9 @@ background-size: 400% 400%, 400% 400%; animation: aurora 20s ease infinite; } + +.dark .bg-aurora-enhanced { + background: radial-gradient(circle at 20% 80%, #0f4c75 0%, #2a1b3d 50%, #1a1a2e 100%), linear-gradient(-45deg, #0f3460, #533483, #0b6b9f, #0f4c75); + background-size: 400% 400%, 400% 400%; + animation: aurora 20s ease infinite; +} diff --git a/resources/js/app.tsx b/resources/js/app.tsx index ae09978..467770c 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -4,16 +4,32 @@ import { createInertiaApp } from '@inertiajs/react'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { createRoot } from 'react-dom/client'; import { initializeTheme } from './hooks/use-appearance'; +import AppLayout from './Components/Layout/AppLayout'; +import { I18nextProvider } from 'react-i18next'; +import i18n from './i18n'; const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; createInertiaApp({ title: (title) => title ? `${title} - ${appName}` : appName, - resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')), + resolve: (name) => resolvePageComponent( + `./Pages/${name}.tsx`, + import.meta.glob('./Pages/**/*.tsx') + ).then((page) => { + if (page) { + const PageComponent = (page as any).default; + return (props: any) => ; + } + return null; + }), setup({ el, App, props }) { const root = createRoot(el); - root.render(); + root.render( + + + + ); }, progress: { color: '#4B5563', diff --git a/resources/js/components/Layout/AppLayout.tsx b/resources/js/components/Layout/AppLayout.tsx new file mode 100644 index 0000000..d33742b --- /dev/null +++ b/resources/js/components/Layout/AppLayout.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { usePage } from '@inertiajs/react'; +import Header from './Header'; + +interface AppLayoutProps { + children: React.ReactNode; + header?: React.ReactNode; + footer?: React.ReactNode; +} + +const AppLayout: React.FC = ({ children, header, footer }) => { + const { auth } = usePage().props; + + return ( +
+ {header ||
} +
{children}
+ {footer} +
+ ); +}; + +export default AppLayout; \ No newline at end of file diff --git a/resources/js/components/Layout/Header.tsx b/resources/js/components/Layout/Header.tsx new file mode 100644 index 0000000..aa57d45 --- /dev/null +++ b/resources/js/components/Layout/Header.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { usePage } from '@inertiajs/react'; +import { Link, router } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; +import { useAppearance } from '@/hooks/use-appearance'; +import { Button } from '@/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Sun, Moon } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const Header: React.FC = () => { + const { auth, locale } = usePage().props as any; + const { t } = useTranslation('auth'); + const { appearance, updateAppearance } = useAppearance(); + + const toggleTheme = () => { + const newAppearance = appearance === 'dark' ? 'light' : 'dark'; + updateAppearance(newAppearance); + }; + + const handleLanguageChange = (value: string) => { + router.visit(`/${value}`, { preserveState: true, replace: true }); + }; + + const handleLogout = () => { + router.post(`/${locale}/logout`); + }; + + return ( +
+
+
+ + Fotospiel + + +
+ + + {auth.user ? ( + + + + + + +
+

{auth.user.name}

+

{auth.user.email}

+
+
+ + + + Profil + + + + + Bestellungen + + + + + Abmelden + +
+
+ ) : ( + <> + + {t('header.login')} + + + {t('header.register')} + + + )} +
+
+
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/resources/js/components/ui/switch.tsx b/resources/js/components/ui/switch.tsx new file mode 100644 index 0000000..455c23b --- /dev/null +++ b/resources/js/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/resources/js/hooks/use-appearance.ts b/resources/js/hooks/use-appearance.ts new file mode 100644 index 0000000..304be09 --- /dev/null +++ b/resources/js/hooks/use-appearance.ts @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react'; + +type Appearance = 'light' | 'dark' | 'system'; + +export function useAppearance(): { appearance: Appearance; updateAppearance: (mode: Appearance) => void } { + const [appearance, setAppearance] = useState('system'); + + useEffect(() => { + const stored = localStorage.getItem('theme') as Appearance | null; + if (stored) { + setAppearance(stored); + } else { + setAppearance('system'); + } + }, []); + + const updateAppearance = (mode: Appearance) => { + setAppearance(mode); + localStorage.setItem('theme', mode); + if (mode === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }; + + useEffect(() => { + if (appearance === 'system') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + if (mediaQuery.matches) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + const listener = (e: MediaQueryListEvent) => { + if (e.matches) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }; + mediaQuery.addEventListener('change', listener); + return () => mediaQuery.removeEventListener('change', listener); + } else if (appearance === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [appearance]); + + return { appearance, updateAppearance }; +} + +export function initializeTheme() { + const stored = localStorage.getItem('theme') as Appearance | null; + if (stored) { + if (stored === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } else { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + if (mediaQuery.matches) { + document.documentElement.classList.add('dark'); + } + } +} \ No newline at end of file diff --git a/resources/js/i18n.js b/resources/js/i18n.js new file mode 100644 index 0000000..94a4af0 --- /dev/null +++ b/resources/js/i18n.js @@ -0,0 +1,29 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: 'de', + debug: process.env.NODE_ENV === 'development', + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: '/lang/{{lng}}/{{ns}}.json', + }, + ns: ['marketing', 'auth'], + defaultNS: 'marketing', + supportedLngs: ['de', 'en'], + detection: { + order: ['path', 'cookie', 'localStorage', 'htmlTag', 'subdomain'], + lookupFromPathIndex: 0, + caches: ['cookie'], + }, + }); + +export default i18n; \ No newline at end of file diff --git a/resources/js/layouts/marketing/MarketingLayout.tsx b/resources/js/layouts/marketing/MarketingLayout.tsx index 139b9e5..4a185ee 100644 --- a/resources/js/layouts/marketing/MarketingLayout.tsx +++ b/resources/js/layouts/marketing/MarketingLayout.tsx @@ -1,45 +1,66 @@ -import React, { ReactNode } from 'react'; -import { Head } from '@inertiajs/react'; -import MarketingHeader from '@/components/marketing/MarketingHeader'; -import MarketingFooter from '@/components/marketing/MarketingFooter'; +import React from 'react'; +import { Head, Link, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; interface MarketingLayoutProps { - children: ReactNode; + children: React.ReactNode; title?: string; - description?: string; } -const MarketingLayout: React.FC = ({ - children, - title = 'Fotospiel - Event-Fotos einfach und sicher mit QR-Codes', - description = 'Sammle Gastfotos für Events mit QR-Codes und unserer PWA-Plattform. Für Hochzeiten, Firmenevents und mehr. Kostenloser Einstieg.' -}) => { +const MarketingLayout: React.FC = ({ children, title }) => { + const { t } = useTranslation('marketing'); + const { url } = usePage(); + + const { translations } = usePage().props as any; + const marketing = translations?.marketing || {}; + + const getString = (key: string, fallback: string) => { + const value = marketing[key]; + return typeof value === 'string' ? value : fallback; + }; + + const currentLocale = url.startsWith('/en/') ? 'en' : 'de'; + const alternateLocale = currentLocale === 'de' ? 'en' : 'de'; + const path = url.replace(/^\/(de|en)?/, ''); + const canonicalUrl = `https://fotospiel.app/${currentLocale}${path}`; + const alternateUrl = `https://fotospiel.app/${alternateLocale}${path}`; + return ( <> - {title} - - + {title || t('meta.title', getString('title', 'Fotospiel'))} + + + + + + + + -
- -
+
+
{children}
- +
+
+

© 2025 Fotospiel. Alle Rechte vorbehalten.

+
+ + {t('nav.privacy', getString('nav.privacy', 'Datenschutz'))} + + + {t('nav.impressum', getString('nav.impressum', 'Impressum'))} + + + {t('nav.contact', getString('nav.contact', 'Kontakt'))} + +
+
+
); }; -export default MarketingLayout; \ No newline at end of file +export default MarketingLayout; diff --git a/resources/js/pages/Profile/Account.tsx b/resources/js/pages/Profile/Account.tsx new file mode 100644 index 0000000..1d60b24 --- /dev/null +++ b/resources/js/pages/Profile/Account.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useForm } from '@inertiajs/react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +const ProfileAccount = () => { + const { data, setData, post, processing, errors } = useForm({ + name: '', + email: '', + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post('/profile/account'); + }; + + return ( +
+ + + Account bearbeiten + + +
+
+ + setData('name', e.target.value)} /> + {errors.name &&

{errors.name}

} +
+
+ + setData('email', e.target.value)} /> + {errors.email &&

{errors.email}

} +
+ +
+
+
+
+ ); +}; + +export default ProfileAccount; \ No newline at end of file diff --git a/resources/js/pages/Profile/Index.tsx b/resources/js/pages/Profile/Index.tsx new file mode 100644 index 0000000..a1391a2 --- /dev/null +++ b/resources/js/pages/Profile/Index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { usePage } from '@inertiajs/react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import Account from './Account'; +import Orders from './Orders'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +const ProfileIndex = () => { + const { user } = usePage().props as any; + + return ( +
+ + + Mein Profil + + +

Hallo, {user.name}!

+

Email: {user.email}

+
+
+ + + Account + Bestellungen + + + + + + + + +
+ ); +}; + +export default ProfileIndex; \ No newline at end of file diff --git a/resources/js/pages/Profile/Orders.tsx b/resources/js/pages/Profile/Orders.tsx new file mode 100644 index 0000000..d97d58c --- /dev/null +++ b/resources/js/pages/Profile/Orders.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { usePage } from '@inertiajs/react'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { format } from 'date-fns'; + +interface Purchase { + id: number; + created_at: string; + package: { + name: string; + price: number; + }; + status: string; +} + +const ProfileOrders = () => { + const { purchases } = usePage().props as any; + + return ( +
+ + + Bestellungen + + + + + + Paket + Preis + Datum + Status + + + + {purchases.map((purchase) => ( + + {purchase.package.name} + {purchase.package.price} € + {format(new Date(purchase.created_at), 'dd.MM.yyyy')} + + + {purchase.status} + + + + ))} + +
+ {purchases.length === 0 && ( +

Keine Bestellungen gefunden.

+ )} +
+
+
+ ); +}; + +export default ProfileOrders; \ No newline at end of file diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index c1d7d86..e17e341 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -1,5 +1,6 @@ import { FormEvent, useEffect, useState } from 'react'; import { Head, useForm } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import InputError from '@/components/input-error'; import TextLink from '@/components/text-link'; import { Button } from '@/components/ui/button'; @@ -18,6 +19,7 @@ interface LoginProps { export default function Login({ status, canResetPassword }: LoginProps) { const [hasTriedSubmit, setHasTriedSubmit] = useState(false); + const { t } = useTranslation('auth'); const { data, setData, post, processing, errors, clearErrors } = useForm({ email: '', @@ -52,13 +54,13 @@ export default function Login({ status, canResetPassword }: LoginProps) { }, [errors, hasTriedSubmit]); return ( - - + +
- + { setData('email', e.target.value); @@ -81,10 +83,10 @@ export default function Login({ status, canResetPassword }: LoginProps) {
- + {canResetPassword && ( - Forgot password? + {t('login.forgot')} )}
@@ -95,7 +97,7 @@ export default function Login({ status, canResetPassword }: LoginProps) { required tabIndex={2} autoComplete="current-password" - placeholder="Password" + placeholder={t('login.password_placeholder')} value={data.password} onChange={(e) => { setData('password', e.target.value); @@ -115,19 +117,19 @@ export default function Login({ status, canResetPassword }: LoginProps) { checked={data.remember} onCheckedChange={(checked) => setData('remember', Boolean(checked))} /> - +
- Don't have an account?{' '} + {t('login.no_account')}{' '} - Sign up + {t('login.sign_up')}
diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx index b1be3b8..3907e24 100644 --- a/resources/js/pages/auth/register.tsx +++ b/resources/js/pages/auth/register.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useForm } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import { LoaderCircle, User, Mail, Phone, Lock, Home, MapPin } from 'lucide-react'; import { Dialog, DialogContent } from '@/components/ui/dialog'; @@ -18,6 +19,7 @@ import MarketingLayout from '@/layouts/marketing/MarketingLayout'; export default function Register({ package: initialPackage, privacyHtml }: RegisterProps) { const [privacyOpen, setPrivacyOpen] = useState(false); const [hasTriedSubmit, setHasTriedSubmit] = useState(false); + const { t } = useTranslation('auth'); const { data, setData, post, processing, errors, clearErrors } = useForm({ username: '', @@ -60,23 +62,23 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis }, [errors, hasTriedSubmit]); return ( - +

- Willkommen bei Fotospiel – Erstellen Sie Ihren Account + {t('register.welcome')}

- Registrierung ermöglicht Zugriff auf Events, Galerien und personalisierte Features. + {t('register.description')}

{initialPackage && (

{initialPackage.name}

{initialPackage.description}

- {initialPackage.price === 0 ? 'Kostenlos' : `${initialPackage.price} €`} + {initialPackage.price === 0 ? t('register.package_price_free') : t('register.package_price', { price: initialPackage.price })}

)} @@ -85,7 +87,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -102,7 +104,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.first_name ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Vorname" + placeholder={t('register.first_name_placeholder')} />
{errors.first_name &&

{errors.first_name}

} @@ -110,7 +112,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -127,7 +129,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.last_name ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Nachname" + placeholder={t('register.last_name_placeholder')} />
{errors.last_name &&

{errors.last_name}

} @@ -135,7 +137,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -152,7 +154,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.email ? 'border-red-500' : 'border-gray-300'}`} - placeholder="email@example.com" + placeholder={t('register.email_placeholder')} />
{errors.email &&

{errors.email}

} @@ -160,7 +162,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -177,7 +179,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.address ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Adresse" + placeholder={t('register.address_placeholder')} />
{errors.address &&

{errors.address}

} @@ -185,7 +187,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -202,7 +204,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.phone ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Telefonnummer" + placeholder={t('register.phone_placeholder')} />
{errors.phone &&

{errors.phone}

} @@ -210,7 +212,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -227,7 +229,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.username ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Benutzername" + placeholder={t('register.username_placeholder')} />
{errors.username &&

{errors.username}

} @@ -235,7 +237,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -255,7 +257,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.password ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Passwort" + placeholder={t('register.password_placeholder')} />
{errors.password &&

{errors.password}

} @@ -263,7 +265,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis
@@ -283,7 +285,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis } }} className={`block w-full pl-10 pr-3 py-3 border rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] focus:border-[#FFB6C1] sm:text-sm ${errors.password_confirmation ? 'border-red-500' : 'border-gray-300'}`} - placeholder="Passwort bestätigen" + placeholder={t('register.confirm_password_placeholder')} />
{errors.password_confirmation &&

{errors.password_confirmation}

} @@ -305,15 +307,14 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis className="h-4 w-4 text-[#FFB6C1] focus:ring-[#FFB6C1] border-gray-300 rounded" /> {errors.privacy_consent &&

{errors.privacy_consent}

}
@@ -321,7 +322,7 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis {Object.keys(errors).length > 0 && (
-

Fehler bei der Registrierung:

+

{t('register.errors_title')}

    {Object.entries(errors).map(([key, value]) => (
  • @@ -338,14 +339,14 @@ export default function Register({ package: initialPackage, privacyHtml }: Regis className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-[#FFB6C1] hover:bg-[#FF69B4] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#FFB6C1] transition duration-300 disabled:opacity-50" > {processing && } - Account erstellen + {t('register.submit')}

    - Bereits registriert?{' '} + {t('register.has_account')}{' '} - Anmelden + {t('register.login')}

    diff --git a/resources/js/pages/marketing/Blog.tsx b/resources/js/pages/marketing/Blog.tsx index 51fabd0..f9b3016 100644 --- a/resources/js/pages/marketing/Blog.tsx +++ b/resources/js/pages/marketing/Blog.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Head, Link, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import MarketingLayout from '@/layouts/marketing/MarketingLayout'; interface Props { @@ -13,6 +14,7 @@ interface Props { const Blog: React.FC = ({ posts }) => { const { url } = usePage(); + const { t } = useTranslation('marketing'); const renderPagination = () => { if (!posts.links || posts.links.length <= 3) return null; @@ -37,28 +39,28 @@ const Blog: React.FC = ({ posts }) => { }; return ( - - + + {/* Hero Section */} -
    +
    -

    Unser Blog

    -

    Tipps, Tricks und Inspiration für perfekte Event-Fotos.

    - - Zum Blog +

    {t('blog.hero_title')}

    +

    {t('blog.hero_description')}

    + + {t('blog.hero_cta')}
    {/* Posts Section */} -
    +
    -

    Neueste Beiträge

    +

    {t('blog.posts_title')}

    {posts.data.length > 0 ? ( <>
    {posts.data.map((post) => ( -
    +
    {post.featured_image && ( = ({ posts }) => { className="w-full h-48 object-cover rounded mb-4" /> )} -

    +

    {post.title}

    -

    {post.excerpt}

    -

    - Veröffentlicht am {post.published_at} +

    {post.excerpt}

    +

    + {t('blog.by')} {post.author?.name || t('blog.team')} | {t('blog.published_at')} {post.published_at}

    - Weiterlesen + {t('blog.read_more')}
    ))} @@ -87,7 +89,7 @@ const Blog: React.FC = ({ posts }) => { {renderPagination()} ) : ( -

    Keine Beiträge verfügbar.

    +

    {t('blog.empty')}

    )}
    diff --git a/resources/js/pages/marketing/BlogShow.tsx b/resources/js/pages/marketing/BlogShow.tsx index d90d486..dd021b9 100644 --- a/resources/js/pages/marketing/BlogShow.tsx +++ b/resources/js/pages/marketing/BlogShow.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Head, Link, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import MarketingLayout from '@/layouts/marketing/MarketingLayout'; interface Props { @@ -16,15 +17,17 @@ interface Props { } const BlogShow: React.FC = ({ post }) => { + const { t } = useTranslation('blog_show'); + return ( - - + + {/* Hero Section */}

    {post.title}

    - Von {post.author?.name || 'Dem Fotospiel Team'} | {new Date(post.published_at).toLocaleDateString('de-DE')} + {t('by_author')} {post.author?.name || t('team')} | {t('published_on')} {new Date(post.published_at).toLocaleDateString('de-DE')}

    {post.featured_image && ( = ({ post }) => { href="/blog" className="bg-[#FFB6C1] text-white px-8 py-3 rounded-full font-semibold hover:bg-[#FF69B4] transition" > - Zurück zum Blog + {t('back_to_blog')}
    diff --git a/resources/js/pages/marketing/Home.tsx b/resources/js/pages/marketing/Home.tsx index 26ae5f6..fa15e18 100644 --- a/resources/js/pages/marketing/Home.tsx +++ b/resources/js/pages/marketing/Home.tsx @@ -1,13 +1,21 @@ import React from 'react'; -import { Head, Link, useForm } from '@inertiajs/react'; +import { Head, Link, useForm, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import MarketingLayout from '@/layouts/marketing/MarketingLayout'; -import { Package } from '@/types'; // Annahme: Typ für Package + +interface Package { + id: number; + name: string; + description: string; + price: number; +} interface Props { packages: Package[]; } const Home: React.FC = ({ packages }) => { + const { t } = useTranslation('marketing'); const { data, setData, post, processing, errors, reset } = useForm({ name: '', email: '', @@ -21,237 +29,203 @@ const Home: React.FC = ({ packages }) => { }); }; + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); + return ( - - + + + {/* Hero Section */} -
    +
    -

    Fotospiel

    -

    Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren – einfach, mobil und datenschutzkonform. Besser als Konkurrenz, geliebt von Tausenden.

    - - Jetzt starten – Kostenlos +

    {t('home.hero_title')}

    +

    {t('home.hero_description')}

    + + {t('home.cta_explore')}
    Event-Fotos mit QR
    - {/* How it works Section */} -
    -
    -

    So funktioniert es – in 4 einfachen Schritten mit QR-Codes

    -
    + {/* How it Works Section */} +
    +
    +

    {t('home.how_title')}

    +
    - QR-Code generieren -

    Event erstellen & QR generieren

    -

    Als Organisator: Registrieren, Event anlegen, QR-Code erstellen und drucken/teilen.

    +
    + 1 +
    +

    {t('home.step1_title')}

    +

    {t('home.step1_desc')}

    - Fotos hochladen -

    Fotos hochladen via QR

    -

    Gäste: QR scannen, PWA öffnen, Fotos via Kamera oder Galerie teilen.

    +
    + 2 +
    +

    {t('home.step2_title')}

    +

    {t('home.step2_desc')}

    - Freigaben & Likes -

    Freigaben & Likes

    -

    Emotions auswählen, Fotos liken, Galerie browsen – alles anonym.

    -
    -
    - Download & Teilen -

    Download & Teilen

    -

    Freigegebene Fotos herunterladen, Event abschließen und QR archivieren.

    +
    + 3 +
    +

    {t('home.step3_title')}

    +

    {t('home.step3_desc')}

    {/* Features Section */} -
    -
    -

    Warum Fotospiel mit QR?

    -
    -
    - Sichere QR-Uploads -

    Sichere QR-Uploads

    -

    GDPR-konform, anonyme Sessions, QR-basierte Zugriffe ohne PII-Speicherung.

    +
    +
    +

    {t('home.features_title')}

    +
    +
    +

    {t('home.feature1_title')}

    +

    {t('home.feature1_desc')}

    -
    - Mobile PWA & QR -

    Mobile PWA & QR

    -

    Offline-fähig, App-ähnlich für iOS/Android, QR-Scan für schnellen Einstieg.

    +
    +

    {t('home.feature2_title')}

    +

    {t('home.feature2_desc')}

    -
    - Schnell & Einfach -

    Schnell & Einfach mit QR

    -

    Automatische Thumbnails, Echtzeit-Updates, QR-Sharing für Gäste.

    +
    +

    {t('home.feature3_title')}

    +

    {t('home.feature3_desc')}

    - {/* Packages Teaser Section */} -
    -
    -

    Unsere Packages

    -

    Wählen Sie das passende Paket für Ihr Event – von kostenlos bis premium.

    + {/* Packages Teaser */} +
    +
    +

    {t('home.packages_title')}

    +
    + {packages.slice(0, 2).map((pkg) => ( +
    +

    {pkg.name}

    +

    {pkg.description}

    +

    {pkg.price} {t('home.currency.euro')}

    + + {t('home.view_details')} + +
    + ))} +
    - - Alle Packages ansehen + + {t('home.all_packages')}
    {/* Contact Section */} -
    -
    -

    Kontakt

    -
    +
    +
    +

    {t('home.contact_title')}

    +
    - + setData('name', e.target.value)} - required - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1]" + className="w-full p-3 border rounded-lg" /> - {errors.name &&

    {errors.name}

    } + {errors.name &&

    {errors.name}

    }
    - + setData('email', e.target.value)} - required - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1]" + className="w-full p-3 border rounded-lg" /> - {errors.email &&

    {errors.email}

    } + {errors.email &&

    {errors.email}

    }
    - + - {errors.message &&

    {errors.message}

    } + className="w-full p-3 border rounded-lg" + /> + {errors.message &&

    {errors.message}

    }
    - - {Object.keys(errors).length === 0 && data.message && !processing && ( -

    Nachricht gesendet!

    - )} - - React.useEffect(() => { - if (Object.keys(errors).length > 0) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }, [errors]);
    {/* Testimonials Section */} -
    -
    -

    Was unsere Kunden sagen

    -
    -
    -

    "Perfekt für unsere Hochzeit! QR-Sharing war super einfach."

    -

    - Anna & Max

    +
    +
    +

    {t('home.testimonials_title')}

    +
    +
    +

    "{t('home.testimonial1')}"

    +

    - Anna M.

    -
    -

    "Großes Firmenevent – alle Fotos zentral via QR."

    -

    - Team XYZ GmbH

    +
    +

    "{t('home.testimonial2')}"

    +

    - Max S.

    +
    +
    +

    "{t('home.testimonial3')}"

    +

    - Lisa K.

    {/* FAQ Section */} -
    -
    -

    Häufige Fragen

    -
    -
    -

    Ist es kostenlos?

    -

    Ja, der Basic-Tarif ist kostenlos für 1 Event mit QR. Upgrades ab 99€.

    -
    -
    -

    Datenschutz?

    -

    100% GDPR-konform. Keine personenbezogenen Daten gespeichert. QR-Zugriffe anonym. Siehe Datenschutzerklärung.

    -
    -
    -

    Wie funktioniert QR-Sharing?

    -

    Generiere QR im Dashboard, teile es – Gäste scannen, laden Fotos hoch in der PWA.

    -
    -
    -
    -
    - - {/* Packages Section (aus aktuellem TSX, angepasst) */} -
    +
    -

    Unsere Pakete

    -
    - {packages.map((pkg) => ( -
    -

    {pkg.name}

    -

    {pkg.description}

    -

    €{pkg.price}

    - - Kaufen - -
    - ))} +

    {t('home.faq_title')}

    +
    +
    +

    {t('home.faq1_q')}

    +

    {t('home.faq1_a')}

    +
    +
    +

    {t('home.faq2_q')}

    +

    {t('home.faq2_a')}

    +
    diff --git a/resources/js/pages/marketing/Kontakt.tsx b/resources/js/pages/marketing/Kontakt.tsx index 19457e1..f3afdbd 100644 --- a/resources/js/pages/marketing/Kontakt.tsx +++ b/resources/js/pages/marketing/Kontakt.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Head, Link, useForm, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import MarketingLayout from '@/layouts/marketing/MarketingLayout'; const Kontakt: React.FC = () => { @@ -10,6 +11,7 @@ const Kontakt: React.FC = () => { }); const { flash } = usePage().props as any; + const { t } = useTranslation('marketing'); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -18,72 +20,72 @@ const Kontakt: React.FC = () => { }); }; + React.useEffect(() => { + if (Object.keys(errors).length > 0) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }, [errors]); + return ( - - -
    + + +
    -

    Kontakt

    -

    Haben Sie Fragen? Schreiben Sie uns!

    +

    {t('kontakt.title')}

    +

    {t('kontakt.description')}

    - - setData('name', e.target.value)} - required - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1]" + + setData('name', e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" /> {errors.name &&

    {errors.name}

    }
    - - setData('email', e.target.value)} - required - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1]" + + setData('email', e.target.value)} + required + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-[#FFB6C1] bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" /> {errors.email &&

    {errors.email}

    }
    - - {errors.message &&

    {errors.message}

    }
    - {flash?.success &&

    {flash.success}

    } + {flash?.success &&

    {flash.success}

    } {Object.keys(errors).length > 0 && ( -
    +
      {Object.values(errors).map((error, index) => ( -
    • {error}
    • +
    • {error}
    • ))}
    )} - - React.useEffect(() => { - if (Object.keys(errors).length > 0) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }, [errors]);
    - Zurück zur Startseite + {t('kontakt.back_home')}
    diff --git a/resources/js/pages/marketing/Occasions.tsx b/resources/js/pages/marketing/Occasions.tsx index c2e8c33..f3926ae 100644 --- a/resources/js/pages/marketing/Occasions.tsx +++ b/resources/js/pages/marketing/Occasions.tsx @@ -1,83 +1,77 @@ import React from 'react'; -import { Head, Link } from '@inertiajs/react'; +import { Head, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import MarketingLayout from '@/layouts/marketing/MarketingLayout'; -interface Props { +interface OccasionsProps { type: string; } -const Occasions: React.FC = ({ type }) => { - const occasions = { - weddings: { - title: 'Hochzeiten', - description: 'Erfangen Sie die magischen Momente Ihrer Hochzeit mit professionellen Fotos.', - features: ['Unbegrenzte Fotos', 'Sofort-Download', 'Privat-Event-Code', 'Emotionen tracken'], - image: '/images/wedding-lights-background.svg' // Platzhalter +const Occasions: React.FC = ({ type }) => { + const { t } = useTranslation('marketing'); + + const occasionsContent = { + hochzeit: { + title: t('occasions.weddings.title'), + description: t('occasions.weddings.description'), + features: [ + t('occasions.weddings.benefit1'), + t('occasions.weddings.benefit2'), + t('occasions.weddings.benefit3'), + t('occasions.weddings.benefit4'), + ], + cta: t('occasions.cta'), }, - birthdays: { - title: 'Geburtstage', - description: 'Feiern Sie Geburtstage unvergesslich mit unseren Event-Foto-Lösungen.', - features: ['Schnelle Einrichtung', 'Gäste teilen Fotos', 'Themen-Filter', 'Druck-Optionen'], - image: '/images/birthday-placeholder.jpg' + geburtstag: { + title: t('occasions.birthdays.title'), + description: t('occasions.birthdays.description'), + features: [ + t('occasions.birthdays.benefit1'), + t('occasions.birthdays.benefit2'), + t('occasions.birthdays.benefit3'), + t('occasions.birthdays.benefit4'), + ], + cta: t('occasions.cta'), }, - 'corporate-events': { - title: 'Firmenevents', - description: 'Professionelle Fotos für Teamevents, Konferenzen und Unternehmensfeiern.', - features: ['Branding-Integration', 'Sichere Cloud-Speicher', 'Analytics & Reports', 'Schnelle Bearbeitung'], - image: '/images/corporate-placeholder.jpg' + firmenevent: { + title: t('occasions.corporate.title'), + description: t('occasions.corporate.description'), + features: [ + t('occasions.corporate.benefit1'), + t('occasions.corporate.benefit2'), + t('occasions.corporate.benefit3'), + t('occasions.corporate.benefit4'), + ], + cta: t('occasions.cta'), }, - 'family-celebrations': { - title: 'Familienfeiern', - description: 'Erinnerungen an Taufen, Jubiläen und Familienzusammenkünfte festhalten.', - features: ['Persönliche Alben', 'Gemeinsame Zugriffe', 'Einfache Bedienung', 'Hohe Qualität'], - image: '/images/family-placeholder.jpg' - } }; - const occasion = occasions[type as keyof typeof occasions] || occasions.weddings; + const content = occasionsContent[type as keyof typeof occasionsContent] || occasionsContent.hochzeit; return ( - - - {/* Hero Section */} -
    -
    -

    {occasion.title}

    -

    {occasion.description}

    - {occasion.image && ( - {occasion.title} - )} -
    -
    - - {/* Features Section */} -
    + + +
    -

    Warum Fotospiel für {occasion.title}?

    -
    - {occasion.features.map((feature, index) => ( -
    -
    - +
    +

    {content.title}

    +

    {content.description}

    + + {content.cta} + +
    +
    + {content.features.map((feature, index) => ( +
    +
    + {index + 1}
    -

    {feature}

    +

    {feature}

    ))}
    -
    - - Passendes Paket wählen - -
    -
    +
    ); }; diff --git a/resources/js/pages/marketing/Packages.tsx b/resources/js/pages/marketing/Packages.tsx index 8151ea4..6a4b90b 100644 --- a/resources/js/pages/marketing/Packages.tsx +++ b/resources/js/pages/marketing/Packages.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { Head, Link, usePage } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" @@ -40,11 +41,12 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag const [currentStep, setCurrentStep] = useState('step1'); const { props } = usePage(); const { auth } = props as any; + const { t } = useTranslation('marketing'); const testimonials = [ - { name: 'Anna M.', text: 'Das Starter-Paket war perfekt für unsere Hochzeit – einfach und günstig!', rating: 5 }, - { name: 'Max B.', text: 'Pro-Paket mit Analytics hat uns geholfen, die besten Momente zu finden.', rating: 5 }, - { name: 'Lisa K.', text: 'Als Reseller spare ich Zeit mit dem M-Paket – super Support!', rating: 5 }, + { name: 'Anna M.', text: t('packages.testimonials.anna'), rating: 5 }, + { name: 'Max B.', text: t('packages.testimonials.max'), rating: 5 }, + { name: 'Lisa K.', text: t('packages.testimonials.lisa'), rating: 5 }, ]; const allPackages = [...endcustomerPackages, ...resellerPackages]; @@ -72,21 +74,21 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag }; return ( - + {/* Hero Section */} -
    +
    -

    Unsere Packages

    -

    Wählen Sie das passende Paket für Ihr Event – von kostenlos bis premium.

    - - Jetzt entdecken +

    {t('packages.hero_title')}

    +

    {t('packages.hero_description')}

    + + {t('packages.cta_explore')}
    -
    +
    -

    Für Endkunden

    +

    {t('packages.section_endcustomer')}

    {/* Mobile Carousel for Endcustomer Packages */}
    @@ -95,35 +97,35 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag {endcustomerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.description}

    +

    {pkg.description}

    - {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`} + {pkg.price === 0 ? t('packages.free') : `${pkg.price} ${t('packages.currency.euro')}`}

    -
      -
    • • {pkg.events} Events
    • +
        +
      • • {pkg.events} {t('packages.one_time')}
      • {pkg.features.map((feature, index) => (
      • - {getFeatureIcon(feature)} {feature} + {getFeatureIcon(feature)} {t(`packages.feature_${feature}`)}
      • ))} - {pkg.limits?.max_photos &&
      • • Max. {pkg.limits.max_photos} Fotos
      • } - {pkg.limits?.gallery_days &&
      • • Galerie {pkg.limits.gallery_days} Tage
      • } - {pkg.limits?.max_guests &&
      • • Max. {pkg.limits.max_guests} Gäste
      • } - {pkg.watermark_allowed === false &&
      • Kein Watermark
      • } - {pkg.branding_allowed &&
      • Custom Branding
      • } + {pkg.limits?.max_photos &&
      • • {t('packages.max_photos')} {pkg.limits.max_photos}
      • } + {pkg.limits?.gallery_days &&
      • • {t('packages.gallery_days')} {pkg.limits.gallery_days}
      • } + {pkg.limits?.max_guests &&
      • • {t('packages.max_guests')} {pkg.limits.max_guests}
      • } + {pkg.watermark_allowed === false &&
      • {t('packages.no_watermark')}
      • } + {pkg.branding_allowed &&
      • {t('packages.custom_branding')}
      • }
    @@ -140,35 +142,35 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag {endcustomerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.description}

    +

    {pkg.description}

    - {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`} + {pkg.price === 0 ? t('packages.free') : `${pkg.price} ${t('packages.currency.euro')}`}

    -
      -
    • • {pkg.events} Events
    • +
        +
      • • {pkg.events} {t('packages.one_time')}
      • {pkg.features.map((feature, index) => (
      • - {getFeatureIcon(feature)} {feature} + {getFeatureIcon(feature)} {t(`packages.feature_${feature}`)}
      • ))} - {pkg.limits?.max_photos &&
      • • Max. {pkg.limits.max_photos} Fotos
      • } - {pkg.limits?.gallery_days &&
      • • Galerie {pkg.limits.gallery_days} Tage
      • } - {pkg.limits?.max_guests &&
      • • Max. {pkg.limits.max_guests} Gäste
      • } - {pkg.watermark_allowed === false &&
      • Kein Watermark
      • } - {pkg.branding_allowed &&
      • Custom Branding
      • } + {pkg.limits?.max_photos &&
      • • {t('packages.max_photos')} {pkg.limits.max_photos}
      • } + {pkg.limits?.gallery_days &&
      • • {t('packages.gallery_days')} {pkg.limits.gallery_days}
      • } + {pkg.limits?.max_guests &&
      • • {t('packages.max_guests')} {pkg.limits.max_guests}
      • } + {pkg.watermark_allowed === false &&
      • {t('packages.no_watermark')}
      • } + {pkg.branding_allowed &&
      • {t('packages.custom_branding')}
      • }
    ))} @@ -178,63 +180,63 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag {/* Comparison Section for Endcustomer */}
    -

    Endkunden-Pakete vergleichen

    +

    {t('packages.comparison_title')}

    - Preis + {t('packages.price')}
    {endcustomerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`}

    +

    {pkg.price === 0 ? t('free') : `${pkg.price} ${t('currency.euro')}`}

    ))}
    - Max. Fotos {getFeatureIcon('max_photos')} + {t('packages.max_photos_label')} {getFeatureIcon('max_photos')}
    {endcustomerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.limits?.max_photos || 'Unbegrenzt'}

    +

    {pkg.limits?.max_photos || t('unlimited')}

    ))}
    - Max. Gäste {getFeatureIcon('max_guests')} + {t('packages.max_guests_label')} {getFeatureIcon('max_guests')}
    {endcustomerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.limits?.max_guests || 'Unbegrenzt'}

    +

    {pkg.limits?.max_guests || t('unlimited')}

    ))}
    - Galerie Tage {getFeatureIcon('gallery_days')} + {t('packages.gallery_days_label')} {getFeatureIcon('gallery_days')}
    {endcustomerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.limits?.gallery_days || 'Unbegrenzt'}

    +

    {pkg.limits?.gallery_days || t('unlimited')}

    ))}
    - Watermark {getFeatureIcon('no_watermark')} + {t('packages.watermark_label')} {getFeatureIcon('no_watermark')}
    {endcustomerPackages.map((pkg) => ( @@ -252,7 +254,7 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag - Feature + {t('packages.feature')} {endcustomerPackages.map((pkg) => ( {pkg.name} @@ -262,39 +264,39 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag - Preis + {t('packages.price')} {endcustomerPackages.map((pkg) => ( - {pkg.price === 0 ? 'Kostenlos' : `${pkg.price} €`} + {pkg.price === 0 ? t('free') : `${pkg.price} ${t('currency.euro')}`} ))} - Max. Fotos {getFeatureIcon('max_photos')} + {t('packages.max_photos_label')} {getFeatureIcon('max_photos')} {endcustomerPackages.map((pkg) => ( - {pkg.limits?.max_photos || 'Unbegrenzt'} + {pkg.limits?.max_photos || t('unlimited')} ))} - Max. Gäste {getFeatureIcon('max_guests')} + {t('packages.max_guests_label')} {getFeatureIcon('max_guests')} {endcustomerPackages.map((pkg) => ( - {pkg.limits?.max_guests || 'Unbegrenzt'} + {pkg.limits?.max_guests || t('unlimited')} ))} - Galerie Tage {getFeatureIcon('gallery_days')} + {t('packages.gallery_days_label')} {getFeatureIcon('gallery_days')} {endcustomerPackages.map((pkg) => ( - {pkg.limits?.gallery_days || 'Unbegrenzt'} + {pkg.limits?.gallery_days || t('unlimited')} ))} - Watermark {getFeatureIcon('no_watermark')} + {t('packages.watermark_label')} {getFeatureIcon('no_watermark')} {endcustomerPackages.map((pkg) => ( {pkg.watermark_allowed === false ? : } @@ -307,9 +309,9 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag -
    +
    -

    Für Reseller

    +

    {t('packages.section_reseller')}

    {/* Mobile Carousel for Reseller Packages */}
    @@ -318,32 +320,32 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag {resellerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.description}

    +

    {pkg.description}

    - {pkg.price} € / Jahr + {pkg.price} {t('packages.currency.euro')} / {t('packages.year')}

    -
      +
        {pkg.features.map((feature, index) => (
      • - {getFeatureIcon(feature)} {feature} + {getFeatureIcon(feature)} {t(`packages.feature_${feature}`)}
      • ))} - {pkg.limits?.max_tenants &&
      • • Max. {pkg.limits.max_tenants} Tenants
      • } - {pkg.limits?.max_events &&
      • • Max. {pkg.limits.max_events} Events/Jahr
      • } - {pkg.branding_allowed &&
      • Custom Branding
      • } + {pkg.limits?.max_tenants &&
      • • {t('packages.max_tenants')} {pkg.limits.max_tenants}
      • } + {pkg.limits?.max_events &&
      • • {t('packages.max_events_year')} {pkg.limits.max_events}
      • } + {pkg.branding_allowed &&
      • {t('packages.custom_branding')}
      • }
    @@ -360,32 +362,32 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag {resellerPackages.map((pkg) => (

    {pkg.name}

    -

    {pkg.description}

    +

    {pkg.description}

    - {pkg.price} € / Jahr + {pkg.price} {t('packages.currency.euro')} / {t('packages.year')}

    -
      +
        {pkg.features.map((feature, index) => (
      • - {getFeatureIcon(feature)} {feature} + {getFeatureIcon(feature)} {t(`packages.feature_${feature}`)}
      • ))} - {pkg.limits?.max_tenants &&
      • • Max. {pkg.limits.max_tenants} Tenants
      • } - {pkg.limits?.max_events &&
      • • Max. {pkg.limits.max_events} Events/Jahr
      • } - {pkg.branding_allowed &&
      • Custom Branding
      • } + {pkg.limits?.max_tenants &&
      • • {t('packages.max_tenants')} {pkg.limits.max_tenants}
      • } + {pkg.limits?.max_events &&
      • • {t('packages.max_events_year')} {pkg.limits.max_events}
      • } + {pkg.branding_allowed &&
      • {t('packages.custom_branding')}
      • }
    ))} @@ -395,25 +397,25 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag
    {/* FAQ Section */} -
    +
    -

    Häufige Fragen

    +

    {t('packages.faq_title')}

    -
    -

    Was ist das Free-Paket?

    -

    Ideal für Tests: 30 Fotos, 7 Tage Galerie, mit Watermark.

    +
    +

    {t('packages.faq_free')}

    +

    {t('packages.faq_free_desc')}

    -
    -

    Kann ich upgraden?

    -

    Ja, jederzeit im Dashboard – Limits werden sofort erweitert.

    +
    +

    {t('packages.faq_upgrade')}

    +

    {t('packages.faq_upgrade_desc')}

    -
    -

    Was für Reseller?

    -

    Jährliche Subscriptions mit Dashboard, Branding und Support.

    +
    +

    {t('packages.faq_reseller')}

    +

    {t('packages.faq_reseller_desc')}

    -
    -

    Zahlungssicher?

    -

    Sichere Zahlung via Stripe/PayPal, 14 Tage Rückgaberecht.

    +
    +

    {t('packages.faq_payment')}

    +

    {t('packages.faq_payment_desc')}

    @@ -424,51 +426,51 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag - {selectedPackage.name} - Details + {selectedPackage.name} - {t('packages.details')} - Details - Kundenmeinungen + {t('packages.details')} + {t('packages.customer_opinions')}

    {selectedPackage.name}

    - {selectedPackage.price === 0 ? 'Kostenlos' : `${selectedPackage.price} €`} + {selectedPackage.price === 0 ? t('packages.free') : `${selectedPackage.price} ${t('packages.currency.euro')}`}

    -

    {selectedPackage.description}

    +

    {selectedPackage.description}

    {selectedPackage.features.map((feature, index) => ( - {getFeatureIcon(feature)} {feature} + {getFeatureIcon(feature)} {t(`packages.feature_${feature}`)} ))} {selectedPackage.limits?.max_photos && ( - Max. {selectedPackage.limits.max_photos} Fotos + {t('packages.max_photos')} {selectedPackage.limits.max_photos} )} {selectedPackage.limits?.max_guests && ( - Max. {selectedPackage.limits.max_guests} Gäste + {t('packages.max_guests')} {selectedPackage.limits.max_guests} )} {selectedPackage.limits?.gallery_days && ( - {selectedPackage.limits.gallery_days} Tage Galerie + {t('packages.gallery_days')} {selectedPackage.limits.gallery_days} )} {selectedPackage.watermark_allowed === false && ( - Kein Watermark + {t('packages.no_watermark')} )} {selectedPackage.branding_allowed && ( - Custom Branding + {t('packages.custom_branding')} )}
    @@ -478,7 +480,7 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag href={`/buy-packages/${selectedPackage.id}`} className="w-full block bg-[#FFB6C1] text-white py-3 rounded-md font-semibold font-sans-marketing hover:bg-[#FF69B4] transition text-center" > - Zur Bestellung + {t('packages.to_order')} ) : ( = ({ endcustomerPackages, resellerPackag localStorage.setItem('preferred_package', JSON.stringify(selectedPackage)); }} > - Zur Bestellung + {t('packages.to_order')} )}
    @@ -496,11 +498,11 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag
    -

    Was Kunden sagen

    +

    {t('packages.what_customers_say')}

    {testimonials.map((testimonial, index) => ( -
    -

    "{testimonial.text}"

    +
    +

    "{testimonial.text}"

    {testimonial.name}

    {[...Array(testimonial.rating)].map((_, i) => )} @@ -508,8 +510,8 @@ const Packages: React.FC = ({ endcustomerPackages, resellerPackag
    ))}
    -
    diff --git a/resources/js/pages/marketing/Success.tsx b/resources/js/pages/marketing/Success.tsx index 4d402dd..af2e407 100644 --- a/resources/js/pages/marketing/Success.tsx +++ b/resources/js/pages/marketing/Success.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { usePage, router } from '@inertiajs/react'; import { Head } from '@inertiajs/react'; +import { useTranslation } from 'react-i18next'; import MarketingLayout from '@/layouts/marketing/MarketingLayout'; import { Loader } from 'lucide-react'; const Success: React.FC = () => { const { auth, flash } = usePage().props as any; + const { t } = useTranslation('success'); if (auth.user && auth.user.email_verified_at) { // Redirect to admin @@ -14,7 +16,7 @@ const Success: React.FC = () => {
    -

    Wird weitergeleitet...

    +

    {t('redirecting')}

    ); @@ -22,26 +24,26 @@ const Success: React.FC = () => { if (auth.user && !auth.user.email_verified_at) { return ( - +

    - E-Mail verifizieren + {t('verify_email')}

    - Bitte überprüfen Sie Ihre E-Mail und klicken Sie auf den Verifizierungslink. + {t('check_email')}

    - Bereits registriert? Anmelden + {t('already_registered')} {t('login')}

    @@ -51,24 +53,24 @@ const Success: React.FC = () => { } return ( - +

    - Kauf abschließen + {t('complete_purchase')}

    - Melden Sie sich an, um fortzufahren. + {t('login_to_continue')}

    - Anmelden + {t('login')}

    - Kein Konto? Registrieren + {t('no_account')} {t('register')}

    diff --git a/resources/lang/de/marketing.json b/resources/lang/de/marketing.json new file mode 100644 index 0000000..d39b573 --- /dev/null +++ b/resources/lang/de/marketing.json @@ -0,0 +1,243 @@ +{ + "home": { + "title": "Startseite - Fotospiel", + "hero_title": "Fotospiel", + "hero_description": "Sammle Gastfotos für Events mit QR-Codes. Unsere sichere PWA-Plattform für Gäste und Organisatoren – einfach, mobil und datenschutzkonform. Besser als Konkurrenz, geliebt von Tausenden.", + "cta_explore": "Pakete entdecken", + "hero_image_alt": "Event-Fotos mit QR-Code", + "how_title": "So funktioniert es", + "step1_title": "Paket wählen", + "step1_desc": "Wähle das passende Paket für dein Event.", + "step2_title": "QR-Code teilen", + "step2_desc": "Teile den QR-Code mit deinen Gästen.", + "step3_title": "Fotos sammeln", + "step3_desc": "Gäste laden Fotos hoch – sicher und einfach.", + "features_title": "Warum Fotospiel?", + "feature1_title": "Sicher & Datenschutzkonform", + "feature1_desc": "GDPR-konform, keine PII-Speicherung.", + "feature2_title": "Mobil & PWA", + "feature2_desc": "Funktioniert offline, installierbar wie App.", + "feature3_title": "Einfach zu bedienen", + "feature3_desc": "Intuitive UI für Gäste und Organisatoren.", + "packages_title": "Unsere Pakete", + "view_details": "Details ansehen", + "all_packages": "Alle Pakete ansehen", + "contact_title": "Kontakt", + "name_label": "Name", + "email_label": "E-Mail", + "message_label": "Nachricht", + "sending": "Wird gesendet...", + "send": "Senden", + "testimonials_title": "Was unsere Kunden sagen", + "testimonial1": "Toll für Hochzeiten! Einfach und sicher.", + "testimonial2": "Beste App für Event-Fotos.", + "testimonial3": "Schnell und benutzerfreundlich.", + "faq_title": "Häufige Fragen", + "faq1_q": "Ist es kostenlos?", + "faq1_a": "Ja, es gibt ein kostenloses Paket für kleine Events.", + "faq2_q": "Wie funktioniert der QR-Code?", + "faq2_a": "Gäste scannen und laden Fotos hoch – einfach!" + }, + "packages": { + "title": "Unsere Packages", + "hero_title": "Entdecken Sie unsere flexiblen Packages", + "hero_description": "Von kostenlosem Einstieg bis Premium-Features: Passen Sie Ihr Event-Paket an Ihre Bedürfnisse an. Einfach, sicher und skalierbar.", + "cta_explore": "Pakete entdecken", + "tab_endcustomer": "Endkunden", + "tab_reseller": "Reseller & Agenturen", + "section_endcustomer": "Packages für Endkunden (Einmalkauf pro Event)", + "section_reseller": "Packages für Reseller (Jährliches Abo)", + "free": "Kostenlos", + "one_time": "Einmalkauf", + "subscription": "Abo", + "year": "Jahr", + "max_photos": "Fotos", + "max_guests": "Gäste", + "gallery_days": "Tage Galerie", + "max_events_year": "Events/Jahr", + "buy_now": "Jetzt kaufen", + "subscribe_now": "Jetzt abonnieren", + "register_buy": "Registrieren und kaufen", + "register_subscribe": "Registrieren und abonnieren", + "faq_title": "Häufige Fragen zu Packages", + "faq_q1": "Was ist ein Package?", + "faq_a1": "Ein Package definiert Limits und Features für Ihr Event, z.B. Anzahl Fotos und Galerie-Dauer.", + "faq_q2": "Kann ich upgraden?", + "faq_a2": "Ja, wählen Sie bei Event-Erstellung ein höheres Package oder upgraden Sie später.", + "faq_q3": "Was passiert bei Ablauf?", + "faq_a3": "Die Galerie bleibt lesbar, aber Uploads sind blockiert. Verlängern Sie einfach.", + "faq_q4": "Zahlungssicher?", + "faq_a4": "Ja, via Stripe oder PayPal – sicher und GDPR-konform.", + "final_cta": "Bereit für Ihr nächstes Event?", + "contact_us": "Kontaktieren Sie uns", + "feature_live_slideshow": "Live-Slideshow", + "feature_analytics": "Analytics", + "feature_watermark": "Wasserzeichen", + "feature_branding": "Branding", + "feature_support": "Support", + "feature_basic_uploads": "Basis-Uploads", + "feature_unlimited_sharing": "Unbegrenztes Teilen", + "feature_no_watermark": "Kein Wasserzeichen", + "feature_custom_tasks": "Benutzerdefinierte Tasks", + "feature_advanced_analytics": "Erweiterte Analytics", + "feature_priority_support": "Priorisierter Support", + "feature_limited_sharing": "Begrenztes Teilen", + "feature_no_branding": "Kein Branding", + "feature_0": "Basis-Feature", + "feature_reseller_dashboard": "Reseller-Dashboard", + "feature_custom_branding": "Benutzerdefiniertes Branding", + "feature_advanced_reporting": "Erweiterte Berichterstattung", + "for_endcustomers": "Für Endkunden", + "for_resellers": "Für Reseller", + "details_show": "Details anzeigen", + "comparison_title": "Packages vergleichen", + "price": "Preis", + "max_photos_label": "Max. Fotos", + "max_guests_label": "Max. Gäste", + "gallery_days_label": "Galerie-Tage", + "watermark_label": "Wasserzeichen", + "no_watermark": "Kein Wasserzeichen", + "custom_branding": "Benutzerdefiniertes Branding", + "max_tenants": "Max. Tenants", + "max_events": "Max. Events/Jahr", + "faq_free": "Was ist das Free Package?", + "faq_upgrade": "Kann ich upgraden?", + "faq_reseller": "Was für Reseller?", + "faq_payment": "Zahlung sicher?" + }, + "blog": { + "title": "Fotospiel - Blog", + "hero_title": "Fotospiel Blog", + "hero_description": "Tipps, News und Guides für perfekte Event-Fotos mit QR-Codes, PWA und mehr. Bleiben Sie informiert!", + "hero_cta": "Mehr über Fotospiel", + "posts_title": "Aktuelle Blog-Beiträge", + "by": "Von", + "team": "Fotospiel Team", + "published_at": "Veröffentlicht am", + "read_more": "Lesen", + "back": "Zurück zum Blog", + "empty": "Noch keine Beiträge verfügbar. Bleiben Sie dran!", + "our_blog": "Unser Blog", + "latest_posts": "Neueste Beiträge", + "no_posts": "Keine Beiträge verfügbar.", + "read_more_link": "Mehr lesen" + }, + "kontakt": { + "title": "Kontakt - Fotospiel", + "description": "Haben Sie Fragen? Schreiben Sie uns!", + "name": "Name", + "email": "E-Mail", + "message": "Nachricht", + "sending": "Wird gesendet...", + "send": "Senden", + "back_home": "Zurück zur Startseite" + }, + "occasions": { + "title": "Fotospiel für :type", + "hero_title": "Fotospiel für :type", + "hero_description": "Sammeln Sie unvergessliche Fotos von Ihren Gästen mit QR-Codes. Perfekt für :type – einfach, mobil und datenschutzkonform.", + "cta": "Paket wählen", + "weddings": { + "title": "Hochzeiten mit Fotospiel", + "description": "Fangen Sie romantische Momente ein: Gäste teilen Fotos via QR, wählen Emotionen wie 'Romantisch' oder 'Freudig'. Besser als traditionelle Fotoboxen.", + "benefits_title": "Vorteile für Hochzeiten", + "benefit1": "QR-Code für Gäste: Einfaches Teilen ohne App-Download.", + "benefit2": "Emotion-Filter: Kategorisieren von Fotos (z.B. 'Tanz', 'Kuss').", + "benefit3": "Private Galerie: Nur genehmigte Fotos sichtbar.", + "benefit4": "Download: Hochauflösend für Album.", + "image_alt": "Hochzeitsfotos" + }, + "birthdays": { + "title": "Geburtstage feiern", + "description": "Lassen Sie Freunde und Familie spontane Fotos teilen. QR auf der Torte – Spaß garantiert!", + "benefits_title": "Vorteile für Geburtstage", + "benefit1": "Schnelle Uploads: Kamera oder Galerie.", + "benefit2": "Likes & Shares: Beliebte Momente hervorheben.", + "benefit3": "Offline-fähig: PWA funktioniert ohne Internet.", + "benefit4": "Anonym: Keine Registrierung erforderlich.", + "image_alt": "Geburtstagsfotos" + }, + "corporate": { + "title": "Firmenevents professionell", + "description": "Networking und Team-Building: Fotos zentral sammeln, Highlights intern teilen.", + "benefits_title": "Vorteile für Firmenevents", + "benefit1": "QR an Ständen: Gäste fotografieren sich selbst.", + "benefit2": "Kategorien: 'Team', 'Netzwerk', 'Präsentation'.", + "benefit3": "Export: Für Social Media oder Intranet.", + "benefit4": "GDPR-sicher: Keine PII gespeichert.", + "image_alt": "Firmenevent-Fotos" + }, + "family": { + "title": "Familienfeiern", + "description": "Von Taufen bis Jubiläen: Erinnerungen von allen Verwandten sammeln.", + "benefits_title": "Vorteile für Familienfeiern", + "benefit1": "Einfach für alle Altersgruppen: Große Schrift, touch-freundlich.", + "benefit2": "Emotionen: 'Familie', 'Glück', 'Einheit'.", + "benefit3": "Teilen: Via Link oder QR für Nachfeier.", + "benefit4": "Unbegrenzt: Im Premium-Plan.", + "image_alt": "Familienfotos" + }, + "not_found": "Anlass nicht gefunden.", + "hochzeit_title": "Hochzeit – Perfekte Gastfotos mit QR", + "hochzeit_desc": "Machen Sie Ihre Hochzeit unvergesslich mit Fotospiel. Gäste teilen Fotos einfach via QR-Code – sicher, privat und in Echtzeit. Von Zeremonie bis Party, alle Momente zentral gesammelt.", + "hochzeit_feature1": "Live-Slideshow für Gäste", + "hochzeit_feature2": "Emotion-basierte Foto-Filter", + "hochzeit_feature3": "Unbegrenzte Galerie für 30 Tage", + "hochzeit_cta": "Hochzeitspaket wählen", + "geburtstag_title": "Geburtstag – Feiern mit geteilten Erinnerungen", + "geburtstag_desc": "Feiern Sie Geburtstage mit Fotospiel! QR-Code für Gäste zum Hochladen von Fotos – von Kinder- bis Erwachsenen-Partys. Einfach teilen, liken und downloaden.", + "geburtstag_feature1": "Kostenloses Paket für kleine Partys", + "geburtstag_feature2": "Schnelle Uploads via PWA", + "geburtstag_feature3": "Privat und datenschutzkonform", + "geburtstag_cta": "Geburtstagspaket entdecken", + "firmenevent_title": "Firmenevent – Team-Events und Konferenzen", + "firmenevent_desc": "Für Firmenevents, Teambuildings und Konferenzen: Fotospiel sammelt alle Fotos zentral via QR. Branding, Analytics und sichere Galerie für Ihr Unternehmen.", + "firmenevent_feature1": "Benutzerdefiniertes Branding für Firmenlogo", + "firmenevent_feature2": "Erweiterte Analytics", + "firmenevent_feature3": "Priorisierter Support", + "firmenevent_cta": "Firmenpaket anfragen" + }, + "success": { + "title": "Erfolg", + "verify_email": "E-Mail verifizieren", + "check_email": "Überprüfen Sie Ihre E-Mail auf den Verifizierungslink.", + "redirecting": "Weiterleitung zum Admin-Bereich...", + "complete_purchase": "Kauf abschließen", + "login_to_continue": "Melden Sie sich an, um fortzufahren.", + "loading": "Laden...", + "email_verify_title": "E-Mail verifizieren", + "email_verify_desc": "Bitte überprüfen Sie Ihre E-Mail und klicken Sie auf den Verifizierungslink.", + "resend_verification": "Verifizierung erneut senden", + "already_registered": "Bereits registriert? Anmelden", + "purchase_complete_title": "Kauf abschließen", + "purchase_complete_desc": "Melden Sie sich an, um fortzufahren.", + "login": "Anmelden", + "no_account": "Kein Konto? Registrieren" + }, + "blog_show": { + "title_suffix": " - Fotospiel Blog", + "by_author": "Von", + "published_on": "Veröffentlicht am", + "back_to_blog": "Zurück zum Blog" + }, + "nav": { + "home": "Startseite", + "how_it_works": "So funktioniert es", + "features": "Features", + "occasions": "Anlässe", + "blog": "Blog", + "packages": "Pakete", + "contact": "Kontakt", + "discover_packages": "Pakete entdecken" + }, + "footer": { + "company": "Fotospiel GmbH", + "rights_reserved": "Alle Rechte vorbehalten" + }, + "register": { + "free": "Kostenlos" + }, + "currency": { + "euro": "€" + } +} \ No newline at end of file diff --git a/resources/lang/en/marketing.json b/resources/lang/en/marketing.json new file mode 100644 index 0000000..01b8695 --- /dev/null +++ b/resources/lang/en/marketing.json @@ -0,0 +1,243 @@ +{ + "home": { + "title": "Home - Fotospiel", + "hero_title": "Fotospiel", + "hero_description": "Collect guest photos for events with QR codes. Our secure PWA platform for guests and organizers – simple, mobile and privacy-compliant. Better than competitors, loved by thousands.", + "cta_explore": "Discover Packages", + "hero_image_alt": "Event Photos with QR Code", + "how_title": "How it works", + "step1_title": "Choose Package", + "step1_desc": "Choose the right package for your event.", + "step2_title": "Share QR Code", + "step2_desc": "Share the QR code with your guests.", + "step3_title": "Collect Photos", + "step3_desc": "Guests upload photos – secure and easy.", + "features_title": "Why Fotospiel?", + "feature1_title": "Secure & Privacy Compliant", + "feature1_desc": "GDPR compliant, no PII storage.", + "feature2_title": "Mobile & PWA", + "feature2_desc": "Works offline, installable like an app.", + "feature3_title": "Easy to Use", + "feature3_desc": "Intuitive UI for guests and organizers.", + "packages_title": "Our Packages", + "view_details": "View Details", + "all_packages": "View All Packages", + "contact_title": "Contact", + "name_label": "Name", + "email_label": "Email", + "message_label": "Message", + "sending": "Sending...", + "send": "Send", + "testimonials_title": "What Our Customers Say", + "testimonial1": "Great for weddings! Simple and secure.", + "testimonial2": "Best app for event photos.", + "testimonial3": "Fast and user-friendly.", + "faq_title": "Frequently Asked Questions", + "faq1_q": "Is it free?", + "faq1_a": "Yes, there's a free package for small events.", + "faq2_q": "How does the QR code work?", + "faq2_a": "Guests scan and upload photos – easy!" + }, + "packages": { + "title": "Our Packages", + "hero_title": "Discover our flexible Packages", + "hero_description": "From free entry to premium features: Tailor your event package to your needs. Simple, secure and scalable.", + "cta_explore": "Discover Packages", + "tab_endcustomer": "End Customers", + "tab_reseller": "Resellers & Agencies", + "section_endcustomer": "Packages for End Customers (One-time purchase per Event)", + "section_reseller": "Packages for Resellers (Annual Subscription)", + "free": "Free", + "one_time": "One-time purchase", + "subscription": "Subscription", + "year": "Year", + "max_photos": "Photos", + "max_guests": "Guests", + "gallery_days": "Gallery Days", + "max_events_year": "Events/Year", + "buy_now": "Buy Now", + "subscribe_now": "Subscribe Now", + "register_buy": "Register and Buy", + "register_subscribe": "Register and Subscribe", + "faq_title": "Frequently Asked Questions about Packages", + "faq_q1": "What is a Package?", + "faq_a1": "A Package defines limits and features for your event, e.g. number of photos and gallery duration.", + "faq_q2": "Can I upgrade?", + "faq_a2": "Yes, choose a higher package when creating the event or upgrade later.", + "faq_q3": "What happens when it expires?", + "faq_a3": "The gallery remains readable, but uploads are blocked. Simply extend it.", + "faq_q4": "Payment secure?", + "faq_a4": "Yes, via Stripe or PayPal – secure and GDPR-compliant.", + "final_cta": "Ready for your next event?", + "contact_us": "Contact Us", + "feature_live_slideshow": "Live Slideshow", + "feature_analytics": "Analytics", + "feature_watermark": "Watermark", + "feature_branding": "Branding", + "feature_support": "Support", + "feature_basic_uploads": "Basic Uploads", + "feature_unlimited_sharing": "Unlimited Sharing", + "feature_no_watermark": "No Watermark", + "feature_custom_tasks": "Custom Tasks", + "feature_advanced_analytics": "Advanced Analytics", + "feature_priority_support": "Priority Support", + "feature_limited_sharing": "Limited Sharing", + "feature_no_branding": "No Branding", + "feature_0": "Basic Feature", + "feature_reseller_dashboard": "Reseller Dashboard", + "feature_custom_branding": "Custom Branding", + "feature_advanced_reporting": "Advanced Reporting", + "for_endcustomers": "For End Customers", + "for_resellers": "For Resellers", + "details_show": "Show Details", + "comparison_title": "Compare Packages", + "price": "Price", + "max_photos_label": "Max. Photos", + "max_guests_label": "Max. Guests", + "gallery_days_label": "Gallery Days", + "watermark_label": "Watermark", + "no_watermark": "No Watermark", + "custom_branding": "Custom Branding", + "max_tenants": "Max. Tenants", + "max_events": "Max. Events/Year", + "faq_free": "What is the Free Package?", + "faq_upgrade": "Can I upgrade?", + "faq_reseller": "What for Resellers?", + "faq_payment": "Payment secure?" + }, + "blog": { + "title": "Fotospiel - Blog", + "hero_title": "Fotospiel Blog", + "hero_description": "Tips, News and Guides for perfect Event Photos with QR-Codes, PWA and more. Stay informed!", + "hero_cta": "More about Fotospiel", + "posts_title": "Current Blog Posts", + "by": "By", + "team": "Fotospiel Team", + "published_at": "Published on", + "read_more": "Read", + "back": "Back to Blog", + "empty": "No posts available yet. Stay tuned!", + "our_blog": "Our Blog", + "latest_posts": "Latest Posts", + "no_posts": "No posts available.", + "read_more_link": "Read More" + }, + "kontakt": { + "title": "Contact - Fotospiel", + "description": "Have questions? Write to us!", + "name": "Name", + "email": "E-Mail", + "message": "Message", + "sending": "Sending...", + "send": "Send", + "back_home": "Back to Home" + }, + "occasions": { + "title": "Fotospiel for :type", + "hero_title": "Fotospiel for :type", + "hero_description": "Collect unforgettable photos from your guests with QR-Codes. Perfect for :type – simple, mobile and privacy-compliant.", + "cta": "Choose Package", + "weddings": { + "title": "Weddings with Fotospiel", + "description": "Capture romantic moments: Guests share photos via QR, choose emotions like 'Romantic' or 'Joyful'. Better than traditional photo booths.", + "benefits_title": "Benefits for Weddings", + "benefit1": "QR-Code for Guests: Easy sharing without app download.", + "benefit2": "Emotion Filter: Categorize photos (e.g. 'Dance', 'Kiss').", + "benefit3": "Private Gallery: Only approved photos visible.", + "benefit4": "Download: High-resolution for album.", + "image_alt": "Wedding Photos" + }, + "birthdays": { + "title": "Celebrate Birthdays", + "description": "Let friends and family share spontaneous photos. QR on the cake – fun guaranteed!", + "benefits_title": "Benefits for Birthdays", + "benefit1": "Quick Uploads: Camera or Gallery.", + "benefit2": "Likes & Shares: Highlight popular moments.", + "benefit3": "Offline-capable: PWA works without internet.", + "benefit4": "Anonymous: No registration required.", + "image_alt": "Birthday Photos" + }, + "corporate": { + "title": "Corporate Events Professionally", + "description": "Networking and Team-Building: Collect photos centrally, share highlights internally.", + "benefits_title": "Benefits for Corporate Events", + "benefit1": "QR at Booths: Guests photograph themselves.", + "benefit2": "Categories: 'Team', 'Network', 'Presentation'.", + "benefit3": "Export: For Social Media or Intranet.", + "benefit4": "GDPR-secure: No PII stored.", + "image_alt": "Corporate Event Photos" + }, + "family": { + "title": "Family Celebrations", + "description": "From baptisms to anniversaries: Collect memories from all relatives.", + "benefits_title": "Benefits for Family Celebrations", + "benefit1": "Easy for all ages: Large letters, touch-friendly.", + "benefit2": "Emotions: 'Family', 'Happiness', 'Unity'.", + "benefit3": "Share: Via link or QR for after-party.", + "benefit4": "Unlimited: In premium plan.", + "image_alt": "Family Photos" + }, + "not_found": "Occasion not found.", + "hochzeit_title": "Wedding – Perfect Guest Photos with QR", + "hochzeit_desc": "Make your wedding unforgettable with Fotospiel. Guests share photos easily via QR code – secure, private and in real time. From ceremony to party, all moments collected centrally.", + "hochzeit_feature1": "Live Slideshow for Guests", + "hochzeit_feature2": "Emotion-based Photo Filters", + "hochzeit_feature3": "Unlimited Gallery for 30 Days", + "hochzeit_cta": "Choose Wedding Package", + "geburtstag_title": "Birthday – Celebrate with Shared Memories", + "geburtstag_desc": "Celebrate birthdays with Fotospiel! QR code for guests to upload photos – from kids to adult parties. Easy to share, like and download.", + "geburtstag_feature1": "Free Package for Small Parties", + "geburtstag_feature2": "Quick Uploads via PWA", + "geburtstag_feature3": "Private and Privacy Compliant", + "geburtstag_cta": "Discover Birthday Package", + "firmenevent_title": "Corporate Event – Team Events and Conferences", + "firmenevent_desc": "For corporate events, team buildings and conferences: Fotospiel collects all photos centrally via QR. Branding, analytics and secure gallery for your company.", + "firmenevent_feature1": "Custom Branding for Company Logo", + "firmenevent_feature2": "Advanced Analytics", + "firmenevent_feature3": "Priority Support", + "firmenevent_cta": "Request Corporate Package" + }, + "success": { + "title": "Success", + "verify_email": "Verify Email", + "check_email": "Check your email for the verification link.", + "redirecting": "Redirecting to admin area...", + "complete_purchase": "Complete Purchase", + "login_to_continue": "Log in to continue.", + "loading": "Loading...", + "email_verify_title": "Verify Email", + "email_verify_desc": "Please check your email and click the verification link.", + "resend_verification": "Resend Verification", + "already_registered": "Already registered? Login", + "purchase_complete_title": "Complete Purchase", + "purchase_complete_desc": "Log in to continue.", + "login": "Login", + "no_account": "No Account? Register" + }, + "blog_show": { + "title_suffix": " - Fotospiel Blog", + "by_author": "By", + "published_on": "Published on", + "back_to_blog": "Back to Blog" + }, + "nav": { + "home": "Home", + "how_it_works": "How it works", + "features": "Features", + "occasions": "Occasions", + "blog": "Blog", + "packages": "Packages", + "contact": "Contact", + "discover_packages": "Discover Packages" + }, + "footer": { + "company": "Fotospiel GmbH", + "rights_reserved": "All rights reserved" + }, + "register": { + "free": "Free" + }, + "currency": { + "euro": "€" + } +} \ No newline at end of file diff --git a/resources/views/layouts/marketing.blade.php b/resources/views/layouts/marketing.blade.php index 94cd84e..1a6afdd 100644 --- a/resources/views/layouts/marketing.blade.php +++ b/resources/views/layouts/marketing.blade.php @@ -22,7 +22,6 @@ - @include('partials.header')
    @yield('content') diff --git a/routes/auth.php b/routes/auth.php index 168a9f4..30e6b18 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -11,18 +11,6 @@ use App\Http\Controllers\Auth\VerifyEmailController; use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { - Route::get('register', [RegisteredUserController::class, 'create']) - ->name('register'); - - Route::post('register', [RegisteredUserController::class, 'store']) - ->name('register.store'); - - Route::get('login', [AuthenticatedSessionController::class, 'create']) - ->name('login'); - - Route::post('login', [AuthenticatedSessionController::class, 'store']) - ->name('login.store'); - Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) ->name('password.request'); @@ -54,7 +42,4 @@ Route::middleware('auth')->group(function () { Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']) ->middleware('throttle:6,1') ->name('password.confirm.store'); - - Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) - ->name('logout'); }); diff --git a/routes/web.php b/routes/web.php index e2cef1a..ab97ed2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,23 +4,52 @@ use Illuminate\Support\Facades\Route; use Inertia\Inertia; use Illuminate\Support\Facades\Log; - // Marketing-Seite mit Locale-Prefix +Route::get('/lang/{locale}/{namespace}', function ($locale, $namespace) { + Log::info('Lang route hit', ['locale' => $locale, 'namespace' => $namespace]); + $path = public_path("lang/{$locale}/{$namespace}.json"); + Log::info('Path checked', ['path' => $path, 'exists' => file_exists($path)]); + if (!file_exists($path)) { + abort(404); + } + $content = json_decode(file_get_contents($path), true); + Log::info('JSON loaded', ['keys' => array_keys($content ?? [])]); + return response()->json($content); +})->where(['locale' => 'de|en', 'namespace' => 'marketing|auth']); + Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () { Route::get('/', [\App\Http\Controllers\MarketingController::class, 'index'])->name('marketing'); Route::get('/packages', [\App\Http\Controllers\MarketingController::class, 'packagesIndex'])->name('packages'); - Route::get('/register/{package_id?}', [\App\Http\Controllers\Auth\MarketingRegisterController::class, 'create'])->name('marketing.register'); - Route::post('/register', [\App\Http\Controllers\Auth\MarketingRegisterController::class, 'store'])->name('marketing.register.store'); + Route::get('/login', [\App\Http\Controllers\Auth\AuthenticatedSessionController::class, 'create'])->name('login'); + Route::post('/login', [\App\Http\Controllers\Auth\AuthenticatedSessionController::class, 'store'])->name('login.store'); + Route::get('/register/{package_id?}', [\App\Http\Controllers\Auth\MarketingRegisterController::class, 'create'])->name('register'); + Route::post('/register', [\App\Http\Controllers\Auth\MarketingRegisterController::class, 'store'])->name('register.store'); + Route::post('/logout', [\App\Http\Controllers\Auth\AuthenticatedSessionController::class, 'destroy'])->name('logout'); }); + // Fallback for /login (redirect to default locale) + Route::get('/login', function () { + return redirect('/de/login'); + })->name('login.fallback'); + + // Fallback for /register (redirect to default locale) + Route::get('/register', function () { + return redirect('/de/register'); + })->name('register.fallback'); + + // Fallback for /logout (redirect to default locale) + Route::post('/logout', function () { + return redirect('/de/logout'); + })->name('logout.fallback'); + // Fallback for /packages (redirect to default locale) Route::get('/packages', function () { return redirect('/de/packages'); })->name('packages.fallback'); -// Fallback for /blog (redirect to default locale) -Route::get('/blog', function () { - return redirect('/de/blog'); -})->name('blog.fallback'); + // Fallback for /blog (redirect to default locale) + Route::get('/blog', function () { + return redirect('/de/blog'); + })->name('blog.fallback'); // Blog Routes (inside locale group for i18n support) Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () { @@ -58,6 +87,7 @@ Route::middleware(['auth', 'verified'])->group(function () { }); require __DIR__.'/settings.php'; +// Auth-Routes sind nun in web.php integriert, auth.php nur für andere Auth-Funktionen require __DIR__.'/auth.php'; // Guest PWA shell for /event and sub-routes @@ -76,6 +106,7 @@ Route::prefix('api/v1')->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken // PayPal IPN webhook Route::post('/webhooks/paypal', [\App\Http\Controllers\PayPalWebhookController::class, 'handle']); + // CSV templates for Super Admin imports Route::get('/super-admin/templates/emotions.csv', function () { $headers = [ @@ -110,15 +141,18 @@ Route::get('/super-admin/templates/tasks.csv', function () { Route::get('/buy-packages/{package_id}', [\App\Http\Controllers\MarketingController::class, 'buyPackages'])->name('buy.packages'); Route::middleware('auth')->group(function () { - Route::get('/profile', [\App\Http\Controllers\ProfileController::class, 'edit'])->name('user.profile.edit'); - Route::patch('/profile', [\App\Http\Controllers\ProfileController::class, 'update'])->name('user.profile.update'); + Route::get('/profile', [\App\Http\Controllers\ProfileController::class, 'index'])->name('profile'); + Route::get('/profile/account', [\App\Http\Controllers\ProfileController::class, 'account'])->name('profile.account'); + Route::patch('/profile/account', [\App\Http\Controllers\ProfileController::class, 'account'])->name('profile.account.update'); + Route::get('/profile/orders', [\App\Http\Controllers\ProfileController::class, 'orders'])->name('profile.orders'); }); Route::get('/marketing/success/{package_id?}', [\App\Http\Controllers\MarketingController::class, 'success'])->name('marketing.success'); -Route::get('{locale}/occasions/{type}', [\App\Http\Controllers\MarketingController::class, 'occasionsType']) - ->where([ - 'locale' => 'de|en', - 'type' => 'weddings|birthdays|corporate-events|family-celebrations' - ]) - ->name('occasions.type'); +Route::prefix('{locale?}')->where(['locale' => 'de|en'])->middleware('locale')->group(function () { + Route::get('/anlaesse/{type}', [\App\Http\Controllers\MarketingController::class, 'occasionsType']) + ->where([ + 'type' => 'hochzeit|geburtstag|firmenevent' + ]) + ->name('anlaesse.type'); +});