fixed language switching in the frontend
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Support\LocaleConfig;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
@@ -11,23 +12,13 @@ class LocaleController extends Controller
|
|||||||
public function set(Request $request)
|
public function set(Request $request)
|
||||||
{
|
{
|
||||||
$locale = $request->input('locale');
|
$locale = $request->input('locale');
|
||||||
$supportedLocales = array_values(array_unique(array_filter([
|
$supportedLocales = LocaleConfig::normalized();
|
||||||
config('app.locale'),
|
$canonical = LocaleConfig::canonicalize($locale);
|
||||||
config('app.fallback_locale'),
|
|
||||||
...array_filter(array_map(
|
|
||||||
static fn ($value) => trim((string) $value),
|
|
||||||
explode(',', (string) env('APP_SUPPORTED_LOCALES', ''))
|
|
||||||
)),
|
|
||||||
])));
|
|
||||||
|
|
||||||
if (empty($supportedLocales)) {
|
if (in_array($canonical, $supportedLocales, true)) {
|
||||||
$supportedLocales = ['de', 'en'];
|
App::setLocale($canonical);
|
||||||
}
|
Session::put('locale', $canonical);
|
||||||
|
Session::put('preferred_locale', $canonical);
|
||||||
if (in_array($locale, $supportedLocales)) {
|
|
||||||
App::setLocale($locale);
|
|
||||||
Session::put('locale', $locale);
|
|
||||||
Session::put('preferred_locale', $locale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->expectsJson()) {
|
if ($request->expectsJson()) {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class MarketingController extends Controller
|
|||||||
|
|
||||||
public function contactView(Request $request)
|
public function contactView(Request $request)
|
||||||
{
|
{
|
||||||
$locale = app()->getLocale();
|
$locale = \App\Support\LocaleConfig::canonicalize((string) ($request->route('locale') ?? app()->getLocale()));
|
||||||
$secondSegment = $request->segment(2);
|
$secondSegment = $request->segment(2);
|
||||||
$slug = $secondSegment ? '/'.trim((string) $secondSegment, '/') : '/';
|
$slug = $secondSegment ? '/'.trim((string) $secondSegment, '/') : '/';
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Kernel extends HttpKernel
|
|||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
\App\Http\Middleware\SetLocaleFromRequest::class,
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
|
|||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Middleware;
|
use Inertia\Middleware;
|
||||||
|
use App\Support\LocaleConfig;
|
||||||
|
|
||||||
class HandleInertiaRequests extends Middleware
|
class HandleInertiaRequests extends Middleware
|
||||||
{
|
{
|
||||||
@@ -38,19 +39,10 @@ class HandleInertiaRequests extends Middleware
|
|||||||
{
|
{
|
||||||
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
[$message, $author] = str(Inspiring::quotes()->random())->explode('-');
|
||||||
|
|
||||||
$supportedLocales = collect(explode(',', (string) env('APP_SUPPORTED_LOCALES', 'de,en')))
|
$supportedLocales = LocaleConfig::normalized();
|
||||||
->map(fn ($l) => trim((string) $l))
|
$defaultLocale = LocaleConfig::canonicalize($supportedLocales[0] ?? null);
|
||||||
->filter()
|
|
||||||
->unique()
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
|
|
||||||
if (empty($supportedLocales)) {
|
$currentLocale = LocaleConfig::canonicalize($request->route('locale') ?? $request->segment(1) ?? app()->getLocale());
|
||||||
$supportedLocales = array_values(array_unique(array_filter([
|
|
||||||
config('app.locale'),
|
|
||||||
config('app.fallback_locale'),
|
|
||||||
])));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...parent::share($request),
|
...parent::share($request),
|
||||||
@@ -60,9 +52,10 @@ class HandleInertiaRequests extends Middleware
|
|||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
],
|
],
|
||||||
'supportedLocales' => $supportedLocales,
|
'supportedLocales' => $supportedLocales,
|
||||||
|
'defaultLocale' => $defaultLocale,
|
||||||
'appUrl' => rtrim(config('app.url'), '/'),
|
'appUrl' => rtrim(config('app.url'), '/'),
|
||||||
'sidebarOpen' => $request->cookie('sidebar_state', 'false') === 'true',
|
'sidebarOpen' => $request->cookie('sidebar_state', 'false') === 'true',
|
||||||
'locale' => app()->getLocale(),
|
'locale' => $currentLocale,
|
||||||
'translations' => [
|
'translations' => [
|
||||||
'marketing' => __('marketing'),
|
'marketing' => __('marketing'),
|
||||||
'auth' => __('auth'),
|
'auth' => __('auth'),
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ use Closure;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
|
use App\Support\LocaleConfig;
|
||||||
|
|
||||||
class SetLocaleFromRequest
|
class SetLocaleFromRequest
|
||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next)
|
||||||
{
|
{
|
||||||
$supportedLocales = $this->supportedLocales();
|
$supportedLocales = LocaleConfig::normalized();
|
||||||
|
|
||||||
$locale = (string) $request->route('locale');
|
$locale = (string) ($request->route('locale') ?? $request->segment(1));
|
||||||
|
$normalizedLocale = LocaleConfig::canonicalize($locale);
|
||||||
|
|
||||||
if (! $locale || ! in_array($locale, $supportedLocales, true)) {
|
if (! $locale || ! in_array($normalizedLocale, $supportedLocales, true)) {
|
||||||
$preferred = Session::get('preferred_locale');
|
$preferred = Session::get('preferred_locale');
|
||||||
|
|
||||||
if ($preferred && in_array($preferred, $supportedLocales, true)) {
|
if ($preferred && in_array($preferred, $supportedLocales, true)) {
|
||||||
@@ -27,35 +29,11 @@ class SetLocaleFromRequest
|
|||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
App::setLocale($locale);
|
App::setLocale($normalizedLocale);
|
||||||
Session::put('preferred_locale', $locale);
|
Session::put('preferred_locale', $normalizedLocale);
|
||||||
Session::put('locale', $locale);
|
Session::put('locale', $normalizedLocale);
|
||||||
$request->attributes->set('preferred_locale', $locale);
|
$request->attributes->set('preferred_locale', $normalizedLocale);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
private function supportedLocales(): array
|
|
||||||
{
|
|
||||||
$configured = array_filter(array_map(
|
|
||||||
static fn ($value) => trim((string) $value),
|
|
||||||
explode(',', (string) env('APP_SUPPORTED_LOCALES', ''))
|
|
||||||
));
|
|
||||||
|
|
||||||
if (empty($configured)) {
|
|
||||||
$configured = array_filter([
|
|
||||||
config('app.locale'),
|
|
||||||
config('app.fallback_locale'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($configured)) {
|
|
||||||
$configured = ['de', 'en'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_values(array_unique($configured));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
79
app/Support/LocaleConfig.php
Normal file
79
app/Support/LocaleConfig.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class LocaleConfig
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return configured locales from env/config as provided (may include region codes).
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function configured(): array
|
||||||
|
{
|
||||||
|
$configured = array_filter(array_map(
|
||||||
|
static fn ($value) => trim((string) $value),
|
||||||
|
explode(',', (string) env('APP_SUPPORTED_LOCALES', ''))
|
||||||
|
));
|
||||||
|
|
||||||
|
$baseLocales = array_filter([
|
||||||
|
config('app.locale'),
|
||||||
|
config('app.fallback_locale'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fallback = ['de', 'en'];
|
||||||
|
|
||||||
|
return array_values(array_unique([
|
||||||
|
...$configured,
|
||||||
|
...$baseLocales,
|
||||||
|
...$fallback,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return normalized short codes (language only, lowercase).
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function normalized(): array
|
||||||
|
{
|
||||||
|
return collect(static::configured())
|
||||||
|
->map(fn (string $code) => Str::of($code)->lower()->before('-')->before('_')->toString())
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern for route constraints: accept both configured and normalized variants.
|
||||||
|
*/
|
||||||
|
public static function routePattern(): string
|
||||||
|
{
|
||||||
|
$all = collect(array_merge(static::configured(), static::normalized()))
|
||||||
|
->map(fn (string $code) => preg_quote($code, '/'))
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return implode('|', $all);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalize any incoming locale to a normalized short code if supported, otherwise fallback.
|
||||||
|
*/
|
||||||
|
public static function canonicalize(?string $locale): string
|
||||||
|
{
|
||||||
|
$normalized = static::normalized();
|
||||||
|
$fallback = Arr::first($normalized, default: 'de');
|
||||||
|
|
||||||
|
if (! $locale) {
|
||||||
|
return $fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
$short = Str::of($locale)->lower()->before('-')->before('_')->toString();
|
||||||
|
|
||||||
|
return in_array($short, $normalized, true) ? $short : $fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"impressum": "Impressum",
|
"impressum": "Impressum",
|
||||||
|
"headline": "Rechtliches",
|
||||||
"datenschutz": "Datenschutzerklärung",
|
"datenschutz": "Datenschutzerklärung",
|
||||||
"impressum_title": "Impressum - Fotospiel",
|
"impressum_title": "Impressum - Fotospiel",
|
||||||
"datenschutz_title": "Datenschutzerklärung - Fotospiel",
|
"datenschutz_title": "Datenschutzerklärung - Fotospiel",
|
||||||
|
|||||||
@@ -402,7 +402,8 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"company": "S.E.B. Fotografie",
|
"company": "S.E.B. Fotografie",
|
||||||
"rights_reserved": "Alle Rechte vorbehalten"
|
"rights_reserved": "Alle Rechte vorbehalten",
|
||||||
|
"social": "Social"
|
||||||
},
|
},
|
||||||
"register": {
|
"register": {
|
||||||
"free": "Kostenlos"
|
"free": "Kostenlos"
|
||||||
|
|||||||
@@ -11,9 +11,28 @@ import { Toaster } from 'react-hot-toast';
|
|||||||
import { ConsentProvider } from './contexts/consent';
|
import { ConsentProvider } from './contexts/consent';
|
||||||
import CookieBanner from '@/components/consent/CookieBanner';
|
import CookieBanner from '@/components/consent/CookieBanner';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { usePage } from '@inertiajs/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
|
||||||
|
const LocaleSync: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
// usePage is only available inside Inertia-provided tree; guard for SSR/raw mounts
|
||||||
|
try {
|
||||||
|
const { props } = usePage<{ locale?: string }>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.locale && i18n.language !== props.locale) {
|
||||||
|
i18n.changeLanguage(props.locale);
|
||||||
|
}
|
||||||
|
}, [props.locale]);
|
||||||
|
} catch (error) {
|
||||||
|
// noop – will be hydrated once Inertia provides context
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => title ? `${title} - ${appName}` : appName,
|
title: (title) => title ? `${title} - ${appName}` : appName,
|
||||||
resolve: (name) =>
|
resolve: (name) =>
|
||||||
@@ -46,7 +65,9 @@ createInertiaApp({
|
|||||||
root.render(
|
root.render(
|
||||||
<ConsentProvider>
|
<ConsentProvider>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<App {...props} />
|
<LocaleSync>
|
||||||
|
<App {...props} />
|
||||||
|
</LocaleSync>
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
<Toaster position="top-right" toastOptions={{ duration: 4000 }} />
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
|
|||||||
@@ -1,75 +1,21 @@
|
|||||||
import { usePage } from '@inertiajs/react';
|
import { usePage } from '@inertiajs/react';
|
||||||
|
import { buildLocalizedPath, defaultLocaleRewrites } from '@/lib/localizedPath';
|
||||||
import { useLocale } from './useLocale';
|
import { useLocale } from './useLocale';
|
||||||
|
|
||||||
type LocalizedPathInput = string | null | undefined;
|
|
||||||
|
|
||||||
export const useLocalizedRoutes = () => {
|
export const useLocalizedRoutes = () => {
|
||||||
const { props } = usePage<{ supportedLocales?: string[] }>();
|
const { props } = usePage<{ supportedLocales?: string[]; defaultLocale?: string }>();
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const supportedLocales = props.supportedLocales ?? [];
|
const supportedLocales = props.supportedLocales ?? [];
|
||||||
|
const defaultLocale = props.defaultLocale ?? 'de';
|
||||||
|
|
||||||
const fallbackLocale = (() => {
|
const localizedPath = (path: string | null | undefined, targetLocale?: string) =>
|
||||||
if (locale && supportedLocales.includes(locale)) {
|
buildLocalizedPath(
|
||||||
return locale;
|
path,
|
||||||
}
|
targetLocale ?? locale,
|
||||||
|
supportedLocales,
|
||||||
if (supportedLocales.length > 0) {
|
defaultLocale,
|
||||||
return supportedLocales[0];
|
defaultLocaleRewrites,
|
||||||
}
|
);
|
||||||
|
|
||||||
return 'de';
|
|
||||||
})();
|
|
||||||
|
|
||||||
const pathRewrites: Record<string, Record<string, string>> = {
|
|
||||||
'/kontakt': { en: '/contact' },
|
|
||||||
'/contact': { de: '/kontakt' },
|
|
||||||
'/so-funktionierts': { en: '/how-it-works' },
|
|
||||||
'/how-it-works': { de: '/so-funktionierts' },
|
|
||||||
'/anlaesse': { en: '/occasions' },
|
|
||||||
'/anlaesse/hochzeit': { en: '/occasions/wedding' },
|
|
||||||
'/anlaesse/geburtstag': { en: '/occasions/birthday' },
|
|
||||||
'/anlaesse/firmenevent': { en: '/occasions/corporate-event' },
|
|
||||||
'/anlaesse/konfirmation': { en: '/occasions/confirmation' },
|
|
||||||
'/occasions/wedding': { de: '/anlaesse/hochzeit' },
|
|
||||||
'/occasions/birthday': { de: '/anlaesse/geburtstag' },
|
|
||||||
'/occasions/corporate-event': { de: '/anlaesse/firmenevent' },
|
|
||||||
'/occasions/confirmation': { de: '/anlaesse/konfirmation' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const rewriteForLocale = (path: string, targetLocale: string): string => {
|
|
||||||
const key = path === '' ? '/' : path;
|
|
||||||
const normalizedKey = key.startsWith('/') ? key : `/${key}`;
|
|
||||||
const rewrites = pathRewrites[normalizedKey] ?? {};
|
|
||||||
|
|
||||||
return rewrites[targetLocale] ?? normalizedKey;
|
|
||||||
};
|
|
||||||
|
|
||||||
const localizedPath = (path: LocalizedPathInput, targetLocale?: string) => {
|
|
||||||
if (typeof path !== 'string' || path.trim().length === 0) {
|
|
||||||
console.error('[useLocalizedRoutes] Invalid path input detected', {
|
|
||||||
path,
|
|
||||||
locale,
|
|
||||||
stack: new Error().stack,
|
|
||||||
});
|
|
||||||
|
|
||||||
return `/${fallbackLocale}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextLocale = targetLocale && supportedLocales.includes(targetLocale)
|
|
||||||
? targetLocale
|
|
||||||
: fallbackLocale;
|
|
||||||
|
|
||||||
const trimmed = path.trim();
|
|
||||||
const [rawPath, rawQuery] = trimmed.split('?');
|
|
||||||
const normalizedPath = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
|
||||||
const rewritten = rewriteForLocale(normalizedPath, nextLocale);
|
|
||||||
|
|
||||||
const base = rewritten === '/' ? `/${nextLocale}` : `/${nextLocale}${rewritten}`;
|
|
||||||
const sanitisedBase = base.replace(/\/{2,}/g, '/');
|
|
||||||
const query = rawQuery ? `?${rawQuery}` : '';
|
|
||||||
|
|
||||||
return `${sanitisedBase}${query}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return { localizedPath };
|
return { localizedPath };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import i18n from 'i18next';
|
|
||||||
import { initReactI18next } from 'react-i18next';
|
|
||||||
import Backend from 'i18next-http-backend';
|
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
||||||
|
|
||||||
const isDev = typeof import.meta !== 'undefined' && Boolean(import.meta.env?.DEV);
|
|
||||||
|
|
||||||
i18n
|
|
||||||
.use(Backend)
|
|
||||||
.use(LanguageDetector)
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
fallbackLng: 'de',
|
|
||||||
debug: isDev,
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
backend: {
|
|
||||||
loadPath: '/lang/{{lng}}/{{ns}}.json',
|
|
||||||
},
|
|
||||||
ns: ['marketing', 'auth', 'profile', 'common', 'legal'],
|
|
||||||
defaultNS: 'marketing',
|
|
||||||
supportedLngs: ['de', 'en'],
|
|
||||||
detection: {
|
|
||||||
order: ['path', 'cookie', 'localStorage', 'htmlTag', 'subdomain'],
|
|
||||||
lookupFromPathIndex: 0,
|
|
||||||
caches: ['cookie'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
import Backend from 'i18next-http-backend';
|
import Backend from 'i18next-http-backend';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
|
||||||
i18n.on('languageChanged', (lng) => {
|
const supportedLngs = ['de', 'en'];
|
||||||
console.log('i18n languageChanged event:', lng);
|
const fallbackLng = 'de';
|
||||||
console.trace('languageChanged trace for', lng);
|
|
||||||
});
|
const detection = {
|
||||||
|
order: ['path', 'localStorage', 'cookie', 'htmlTag', 'navigator'],
|
||||||
|
lookupFromPathIndex: 0,
|
||||||
|
caches: ['localStorage'],
|
||||||
|
};
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
.use(Backend)
|
.use(Backend)
|
||||||
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
lng: localStorage.getItem('i18nextLng') || 'de',
|
fallbackLng,
|
||||||
fallbackLng: 'de',
|
supportedLngs,
|
||||||
supportedLngs: ['de', 'en'],
|
|
||||||
ns: ['marketing', 'auth', 'common', 'legal'],
|
ns: ['marketing', 'auth', 'common', 'legal'],
|
||||||
defaultNS: 'marketing',
|
defaultNS: 'marketing',
|
||||||
debug: import.meta.env.DEV,
|
debug: import.meta.env.DEV,
|
||||||
|
load: 'languageOnly',
|
||||||
|
detection,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
@@ -32,4 +39,4 @@ i18n
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ type PageProps = {
|
|||||||
const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) => {
|
const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) => {
|
||||||
const page = usePage<PageProps>();
|
const page = usePage<PageProps>();
|
||||||
const { url } = page;
|
const { url } = page;
|
||||||
const { t } = useTranslation('marketing');
|
const { t, i18n, ready } = useTranslation(['marketing', 'common', 'legal', 'auth']);
|
||||||
const i18n = useTranslation();
|
|
||||||
const { locale, analytics, supportedLocales = ['de', 'en'], appUrl, auth } = page.props;
|
const { locale, analytics, supportedLocales = ['de', 'en'], appUrl, auth } = page.props;
|
||||||
const user = auth?.user ?? null;
|
const user = auth?.user ?? null;
|
||||||
const { localizedPath } = useLocalizedRoutes();
|
const { localizedPath } = useLocalizedRoutes();
|
||||||
@@ -94,11 +93,20 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (locale && i18n.i18n.language !== locale) {
|
if (locale && i18n.language !== locale) {
|
||||||
i18n.i18n.changeLanguage(locale);
|
i18n.changeLanguage(locale);
|
||||||
}
|
}
|
||||||
}, [locale, i18n]);
|
}, [locale, i18n]);
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white text-gray-900 dark:bg-gray-950 dark:text-gray-50 flex items-center justify-center">
|
||||||
|
<Head title="Fotospiel" />
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">Lade Inhalte …</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const marketing = page.props.translations?.marketing ?? {};
|
const marketing = page.props.translations?.marketing ?? {};
|
||||||
|
|
||||||
const getString = (key: string, fallback: string) => {
|
const getString = (key: string, fallback: string) => {
|
||||||
@@ -132,11 +140,13 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
const targetPath = localizedPath(relativePath, nextLocale);
|
const targetPath = localizedPath(relativePath, nextLocale);
|
||||||
const targetUrl = `${targetPath}${rawQuery ? `?${rawQuery}` : ''}`;
|
const targetUrl = `${targetPath}${rawQuery ? `?${rawQuery}` : ''}`;
|
||||||
|
|
||||||
i18n.i18n.changeLanguage(nextLocale);
|
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
router.visit(targetUrl, {
|
router.visit(targetUrl, {
|
||||||
replace: true,
|
replace: true,
|
||||||
preserveState: false,
|
preserveState: false,
|
||||||
|
onSuccess: () => {
|
||||||
|
i18n.changeLanguage(nextLocale);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
29
resources/js/lib/__tests__/localizedPath.test.ts
Normal file
29
resources/js/lib/__tests__/localizedPath.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { buildLocalizedPath, defaultLocaleRewrites } from '../localizedPath';
|
||||||
|
|
||||||
|
describe('buildLocalizedPath', () => {
|
||||||
|
const supported = ['de', 'en'];
|
||||||
|
|
||||||
|
it('prefixes path with locale', () => {
|
||||||
|
expect(buildLocalizedPath('/packages', 'en', supported)).toBe('/en/packages');
|
||||||
|
expect(buildLocalizedPath('/packages', 'de', supported)).toBe('/de/packages');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies rewrite rules between locales', () => {
|
||||||
|
expect(buildLocalizedPath('/kontakt', 'en', supported, 'de', defaultLocaleRewrites)).toBe('/en/contact');
|
||||||
|
expect(buildLocalizedPath('/contact', 'de', supported, 'de', defaultLocaleRewrites)).toBe('/de/kontakt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves query strings', () => {
|
||||||
|
expect(buildLocalizedPath('/contact?ref=ad', 'de', supported)).toBe('/de/kontakt?ref=ad');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to default locale when target not supported', () => {
|
||||||
|
expect(buildLocalizedPath('/demo', 'fr', supported)).toBe('/de/demo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty or invalid paths gracefully', () => {
|
||||||
|
expect(buildLocalizedPath('', 'en', supported)).toBe('/de');
|
||||||
|
expect(buildLocalizedPath(undefined, 'en', supported)).toBe('/de');
|
||||||
|
});
|
||||||
|
});
|
||||||
57
resources/js/lib/localizedPath.ts
Normal file
57
resources/js/lib/localizedPath.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export type LocaleRewriteMap = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
|
export const defaultLocaleRewrites: LocaleRewriteMap = {
|
||||||
|
'/': {},
|
||||||
|
'/kontakt': { en: '/contact' },
|
||||||
|
'/contact': { de: '/kontakt' },
|
||||||
|
'/so-funktionierts': { en: '/how-it-works' },
|
||||||
|
'/how-it-works': { de: '/so-funktionierts' },
|
||||||
|
'/anlaesse': { en: '/occasions' },
|
||||||
|
'/anlaesse/hochzeit': { en: '/occasions/wedding' },
|
||||||
|
'/anlaesse/geburtstag': { en: '/occasions/birthday' },
|
||||||
|
'/anlaesse/firmenevent': { en: '/occasions/corporate-event' },
|
||||||
|
'/anlaesse/konfirmation': { en: '/occasions/confirmation' },
|
||||||
|
'/occasions/wedding': { de: '/anlaesse/hochzeit' },
|
||||||
|
'/occasions/birthday': { de: '/anlaesse/geburtstag' },
|
||||||
|
'/occasions/corporate-event': { de: '/anlaesse/firmenevent' },
|
||||||
|
'/occasions/confirmation': { de: '/anlaesse/konfirmation' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizePath = (input: string): string => {
|
||||||
|
if (!input || input.trim().length === 0) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
const withLeading = input.startsWith('/') ? input : `/${input}`;
|
||||||
|
const withoutTrailing = withLeading.replace(/\/{2,}/g, '/');
|
||||||
|
|
||||||
|
return withoutTrailing;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildLocalizedPath = (
|
||||||
|
path: string | null | undefined,
|
||||||
|
targetLocale: string | undefined,
|
||||||
|
supportedLocales: string[],
|
||||||
|
defaultLocale = 'de',
|
||||||
|
rewrites: LocaleRewriteMap = defaultLocaleRewrites,
|
||||||
|
): string => {
|
||||||
|
const fallbackLocale = supportedLocales.length > 0 ? supportedLocales[0] : defaultLocale;
|
||||||
|
const nextLocale = targetLocale && supportedLocales.includes(targetLocale)
|
||||||
|
? targetLocale
|
||||||
|
: fallbackLocale;
|
||||||
|
|
||||||
|
if (typeof path !== 'string' || path.trim().length === 0) {
|
||||||
|
return `/${fallbackLocale}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = path.trim();
|
||||||
|
const [rawPath, rawQuery] = trimmed.split('?');
|
||||||
|
const normalizedPath = sanitizePath(rawPath);
|
||||||
|
const rewritesForPath = rewrites[normalizedPath] ?? {};
|
||||||
|
const rewrittenPath = rewritesForPath[nextLocale] ?? normalizedPath;
|
||||||
|
const base = rewrittenPath === '/' ? `/${nextLocale}` : `/${nextLocale}${rewrittenPath}`;
|
||||||
|
const sanitisedBase = base.replace(/\/{2,}/g, '/');
|
||||||
|
const query = rawQuery ? `?${rawQuery}` : '';
|
||||||
|
|
||||||
|
return `${sanitisedBase}${query}`;
|
||||||
|
};
|
||||||
@@ -38,10 +38,30 @@ const iconByUseCase: Record<string, React.ReactNode> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const HowItWorks: React.FC = () => {
|
const HowItWorks: React.FC = () => {
|
||||||
const { t } = useTranslation('marketing');
|
const { t, ready } = useTranslation('marketing');
|
||||||
const { localizedPath } = useLocalizedRoutes();
|
const { localizedPath } = useLocalizedRoutes();
|
||||||
|
|
||||||
const hero = t('how_it_works_page.hero', { returnObjects: true }) as {
|
if (!ready) {
|
||||||
|
return (
|
||||||
|
<MarketingLayout title="Fotospiel">
|
||||||
|
<Head title="Fotospiel" />
|
||||||
|
<div className="container mx-auto px-4 py-16 text-center text-gray-600 dark:text-gray-300">
|
||||||
|
<p className="text-lg">Lade Inhalte …</p>
|
||||||
|
</div>
|
||||||
|
</MarketingLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hero = t('how_it_works_page.hero', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: {
|
||||||
|
title: 'So funktioniert die Fotospiel App',
|
||||||
|
subtitle: '',
|
||||||
|
primaryCta: '',
|
||||||
|
secondaryCta: '',
|
||||||
|
stats: [],
|
||||||
|
},
|
||||||
|
}) as {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
primaryCta: string;
|
primaryCta: string;
|
||||||
@@ -49,41 +69,67 @@ const HowItWorks: React.FC = () => {
|
|||||||
stats: HeroStat[];
|
stats: HeroStat[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const experience = t('how_it_works_page.experience', { returnObjects: true }) as {
|
const experience = t('how_it_works_page.experience', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: {
|
||||||
|
host: { label: '', intro: '', steps: [], callouts: [] },
|
||||||
|
guest: { label: '', intro: '', steps: [], callouts: [] },
|
||||||
|
},
|
||||||
|
}) as {
|
||||||
host: ExperienceGroup;
|
host: ExperienceGroup;
|
||||||
guest: ExperienceGroup;
|
guest: ExperienceGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
const pillars = t('how_it_works_page.pillars', { returnObjects: true }) as Array<{
|
const pillars = t('how_it_works_page.pillars', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: [],
|
||||||
|
}) as Array<{
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const timeline = t('how_it_works_page.timeline', { returnObjects: true }) as TimelineItem[];
|
const timeline = t('how_it_works_page.timeline', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: [],
|
||||||
|
}) as TimelineItem[];
|
||||||
|
|
||||||
const useCases = t('how_it_works_page.use_cases', { returnObjects: true }) as {
|
const useCases = t('how_it_works_page.use_cases', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: { title: '', description: '', tabs: [] },
|
||||||
|
}) as {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
tabs: UseCase[];
|
tabs: UseCase[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const checklist = t('how_it_works_page.checklist', { returnObjects: true }) as {
|
const checklist = t('how_it_works_page.checklist', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: { title: '', items: [], cta: '' },
|
||||||
|
}) as {
|
||||||
title: string;
|
title: string;
|
||||||
items: string[];
|
items: string[];
|
||||||
cta: string;
|
cta: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const faq = t('how_it_works_page.faq', { returnObjects: true }) as {
|
const faq = t('how_it_works_page.faq', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: { title: '', items: [] },
|
||||||
|
}) as {
|
||||||
title: string;
|
title: string;
|
||||||
items: FaqItem[];
|
items: FaqItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const support = t('how_it_works_page.support', { returnObjects: true }) as {
|
const support = t('how_it_works_page.support', {
|
||||||
|
returnObjects: true,
|
||||||
|
defaultValue: { title: '', description: '', cta: '' },
|
||||||
|
}) as {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
cta: string;
|
cta: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const heroStats = Array.isArray(hero.stats) ? hero.stats : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MarketingLayout title={hero.title}>
|
<MarketingLayout title={hero.title}>
|
||||||
<Head title={hero.title} />
|
<Head title={hero.title} />
|
||||||
@@ -116,7 +162,7 @@ const HowItWorks: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
{hero.stats.map((stat) => (
|
{heroStats.map((stat) => (
|
||||||
<Card key={stat.label} className="border-pink-100/70 shadow-none dark:border-pink-900/40">
|
<Card key={stat.label} className="border-pink-100/70 shadow-none dark:border-pink-900/40">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-2xl font-semibold text-pink-600 dark:text-pink-300">
|
<CardTitle className="text-2xl font-semibold text-pink-600 dark:text-pink-300">
|
||||||
|
|||||||
@@ -1016,7 +1016,7 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Details overlay */}
|
{/* Details overlay */}
|
||||||
{selectedPackage && (
|
{selectedPackage && (
|
||||||
isMobile ? (
|
isMobile ? (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
@@ -1047,8 +1047,15 @@ const PackageDetailGrid: React.FC<PackageDetailGridProps> = ({
|
|||||||
|
|
||||||
const handleDetailAutoFocus = (event: Event) => {
|
const handleDetailAutoFocus = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
dialogScrollRef.current?.scrollTo({ top: 0 });
|
|
||||||
dialogHeadingRef.current?.focus();
|
// Guard in case refs are not in scope when autofocusing
|
||||||
|
if (typeof dialogScrollRef !== 'undefined') {
|
||||||
|
dialogScrollRef.current?.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dialogHeadingRef !== 'undefined') {
|
||||||
|
dialogHeadingRef.current?.focus();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Packages.layout = (page: React.ReactNode) => page;
|
Packages.layout = (page: React.ReactNode) => page;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { createInertiaApp } from '@inertiajs/react';
|
|||||||
import createServer from '@inertiajs/react/server';
|
import createServer from '@inertiajs/react/server';
|
||||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import i18n from './i18n';
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
|
||||||
@@ -12,7 +14,17 @@ createServer((page) =>
|
|||||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
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')),
|
||||||
setup: ({ App, props }) => {
|
setup: ({ App, props }) => {
|
||||||
return <App {...props} />;
|
const locale = (props.initialPage?.props as Record<string, unknown> | undefined)?.locale as string | undefined;
|
||||||
|
|
||||||
|
if (locale && i18n.language !== locale) {
|
||||||
|
i18n.changeLanguage(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<App {...props} />
|
||||||
|
</I18nextProvider>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use App\Http\Controllers\Tenant\EventPhotoArchiveController;
|
|||||||
use App\Http\Controllers\TenantAdminAuthController;
|
use App\Http\Controllers\TenantAdminAuthController;
|
||||||
use App\Http\Controllers\TenantAdminGoogleController;
|
use App\Http\Controllers\TenantAdminGoogleController;
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
|
use App\Support\LocaleConfig;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -24,20 +25,8 @@ use Inertia\Inertia;
|
|||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
|
|
||||||
$configuredLocales = array_filter(array_map(
|
$supportedLocales = LocaleConfig::normalized();
|
||||||
static fn ($value) => trim((string) $value),
|
$localePattern = LocaleConfig::routePattern();
|
||||||
explode(',', (string) env('APP_SUPPORTED_LOCALES', ''))
|
|
||||||
));
|
|
||||||
|
|
||||||
if (empty($configuredLocales)) {
|
|
||||||
$configuredLocales = array_filter([
|
|
||||||
config('app.locale'),
|
|
||||||
config('app.fallback_locale'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$supportedLocales = array_values(array_unique($configuredLocales ?: ['de', 'en']));
|
|
||||||
$localePattern = implode('|', $supportedLocales);
|
|
||||||
|
|
||||||
$rewritePath = static function (string $path, string $locale): string {
|
$rewritePath = static function (string $path, string $locale): string {
|
||||||
$normalized = '/'.ltrim($path, '/');
|
$normalized = '/'.ltrim($path, '/');
|
||||||
@@ -82,9 +71,6 @@ $determinePreferredLocale = static function (Request $request) use ($supportedLo
|
|||||||
|
|
||||||
Route::prefix('{locale}')
|
Route::prefix('{locale}')
|
||||||
->where(['locale' => $localePattern])
|
->where(['locale' => $localePattern])
|
||||||
->middleware([
|
|
||||||
\App\Http\Middleware\SetLocaleFromRequest::class,
|
|
||||||
])
|
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Route::get('/', [MarketingController::class, 'index'])->name('marketing.home');
|
Route::get('/', [MarketingController::class, 'index'])->name('marketing.home');
|
||||||
|
|
||||||
|
|||||||
58
tests/Feature/Marketing/MarketingLocaleRoutingTest.php
Normal file
58
tests/Feature/Marketing/MarketingLocaleRoutingTest.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Marketing;
|
||||||
|
|
||||||
|
use Inertia\Testing\AssertableInertia as Assert;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
class MarketingLocaleRoutingTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
public function test_home_route_accepts_german_locale_prefix(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/de');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->component('marketing/Home')
|
||||||
|
->where('locale', 'de')
|
||||||
|
->where('supportedLocales.0', 'de')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_home_route_accepts_english_locale_prefix(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/en');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->component('marketing/Home')
|
||||||
|
->where('locale', 'en')
|
||||||
|
->where('supportedLocales.1', 'en')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_contact_route_respects_locale_and_component(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/en/contact');
|
||||||
|
|
||||||
|
// Debug response headers for redirect source during development
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
// var_dump($response->headers->all());
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertInertia(fn (Assert $page) => $page
|
||||||
|
->component('marketing/Kontakt')
|
||||||
|
->where('locale', 'en')
|
||||||
|
);
|
||||||
|
|
||||||
|
$responseDe = $this->get('/de/kontakt');
|
||||||
|
|
||||||
|
$responseDe->assertOk();
|
||||||
|
$responseDe->assertInertia(fn (Assert $page) => $page
|
||||||
|
->component('marketing/Kontakt')
|
||||||
|
->where('locale', 'de')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user