Improve marketing language switcher
This commit is contained in:
@@ -504,6 +504,10 @@
|
|||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"discover_packages": "Packages entdecken",
|
"discover_packages": "Packages entdecken",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
|
"language_de": "Deutsch",
|
||||||
|
"language_en": "English",
|
||||||
|
"language_changed": "{{language}} ausgewählt",
|
||||||
|
"language": "Sprache",
|
||||||
"open_menu": "Menü öffnen",
|
"open_menu": "Menü öffnen",
|
||||||
"close_menu": "Menü schließen",
|
"close_menu": "Menü schließen",
|
||||||
"cta_demo": "Jetzt ausprobieren",
|
"cta_demo": "Jetzt ausprobieren",
|
||||||
|
|||||||
@@ -494,6 +494,10 @@
|
|||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"discover_packages": "Discover Packages",
|
"discover_packages": "Discover Packages",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"language_de": "Deutsch",
|
||||||
|
"language_en": "English",
|
||||||
|
"language_changed": "{{language}} selected",
|
||||||
|
"language": "Language",
|
||||||
"open_menu": "Open menu",
|
"open_menu": "Open menu",
|
||||||
"close_menu": "Close menu",
|
"close_menu": "Close menu",
|
||||||
"cta_demo": "Try it now",
|
"cta_demo": "Try it now",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAppearance } from '@/hooks/use-appearance';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
import { MoreHorizontal, Sun, Moon, Languages, LayoutDashboard, LogOut, LogIn } from 'lucide-react';
|
import { MoreHorizontal, Sun, Moon, Languages, LayoutDashboard, LogOut, LogIn } from 'lucide-react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface MarketingLayoutProps {
|
interface MarketingLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -136,9 +137,44 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const orderedLocales = useMemo(() => {
|
||||||
|
const preferredOrder = ['de', 'en'];
|
||||||
|
return [...supportedLocales].sort((a, b) => {
|
||||||
|
const aIndex = preferredOrder.indexOf(a);
|
||||||
|
const bIndex = preferredOrder.indexOf(b);
|
||||||
|
if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
|
||||||
|
if (aIndex === -1) return 1;
|
||||||
|
if (bIndex === -1) return -1;
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
}, [supportedLocales]);
|
||||||
|
|
||||||
|
const resolveLanguageLabel = (code: string) => {
|
||||||
|
if (code === 'de') {
|
||||||
|
return t('nav.language_de', 'Deutsch');
|
||||||
|
}
|
||||||
|
if (code === 'en') {
|
||||||
|
return t('nav.language_en', 'English');
|
||||||
|
}
|
||||||
|
return code.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const languageOptions = useMemo(() => orderedLocales.map((code) => ({
|
||||||
|
code,
|
||||||
|
label: resolveLanguageLabel(code),
|
||||||
|
})), [orderedLocales, t]);
|
||||||
|
|
||||||
|
const useInlineLocaleToggle = supportedLocales.length === 2;
|
||||||
|
|
||||||
const handleLocaleChange = (nextLocale: string) => {
|
const handleLocaleChange = (nextLocale: string) => {
|
||||||
|
if (nextLocale === activeLocale) {
|
||||||
|
setMobileMenuOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const targetPath = localizedPath(relativePath, nextLocale);
|
const targetPath = localizedPath(relativePath, nextLocale);
|
||||||
const targetUrl = `${targetPath}${rawQuery ? `?${rawQuery}` : ''}`;
|
const targetUrl = `${targetPath}${rawQuery ? `?${rawQuery}` : ''}`;
|
||||||
|
const nextLabel = resolveLanguageLabel(nextLocale);
|
||||||
|
|
||||||
setMobileMenuOpen(false);
|
setMobileMenuOpen(false);
|
||||||
router.visit(targetUrl, {
|
router.visit(targetUrl, {
|
||||||
@@ -146,10 +182,47 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
preserveState: false,
|
preserveState: false,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
i18n.changeLanguage(nextLocale);
|
i18n.changeLanguage(nextLocale);
|
||||||
|
toast.success(t('nav.language_changed', '{{language}} selected', { language: nextLabel }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FlagDe = () => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-3.5 w-5 shrink-0 rounded-[3px] shadow-sm ring-1 ring-black/10"
|
||||||
|
viewBox="0 0 5 3"
|
||||||
|
>
|
||||||
|
<rect width="5" height="1" y="0" fill="#000000" />
|
||||||
|
<rect width="5" height="1" y="1" fill="#dd0000" />
|
||||||
|
<rect width="5" height="1" y="2" fill="#ffce00" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FlagEn = () => (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-3.5 w-5 shrink-0 rounded-[3px] shadow-sm ring-1 ring-black/10"
|
||||||
|
viewBox="0 0 60 30"
|
||||||
|
>
|
||||||
|
<rect width="60" height="30" fill="#012169" />
|
||||||
|
<path d="M0 0L60 30M60 0L0 30" stroke="#ffffff" strokeWidth="6" />
|
||||||
|
<path d="M0 0L60 30M60 0L0 30" stroke="#c8102e" strokeWidth="3" />
|
||||||
|
<path d="M30 0V30M0 15H60" stroke="#ffffff" strokeWidth="10" />
|
||||||
|
<path d="M30 0V30M0 15H60" stroke="#c8102e" strokeWidth="6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolveFlag = (code: string) => {
|
||||||
|
if (code === 'de') {
|
||||||
|
return <FlagDe />;
|
||||||
|
}
|
||||||
|
if (code === 'en') {
|
||||||
|
return <FlagEn />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -232,6 +305,36 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{useInlineLocaleToggle && (
|
||||||
|
<div
|
||||||
|
className="hidden items-center rounded-full border border-gray-200 bg-white p-1 shadow-sm md:flex dark:border-gray-700 dark:bg-gray-900"
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={t('nav.language', 'Sprache')}
|
||||||
|
>
|
||||||
|
{languageOptions.map((option) => {
|
||||||
|
const isActive = option.code === activeLocale;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.code}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={isActive}
|
||||||
|
aria-label={`${t('nav.language', 'Sprache')}: ${option.label}`}
|
||||||
|
onClick={() => handleLocaleChange(option.code)}
|
||||||
|
className={[
|
||||||
|
'inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pink-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
|
||||||
|
isActive
|
||||||
|
? 'bg-pink-500 text-white shadow-sm'
|
||||||
|
: 'text-gray-600 hover:bg-rose-50 hover:text-pink-600 dark:text-gray-100 dark:hover:bg-gray-800 dark:hover:text-pink-300',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{resolveFlag(option.code)}
|
||||||
|
<span className="sr-only">{option.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
className="hidden rounded-full bg-pink-500 px-5 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-pink-600 font-sans-marketing md:inline-flex"
|
className="hidden rounded-full bg-pink-500 px-5 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-pink-600 font-sans-marketing md:inline-flex"
|
||||||
@@ -266,6 +369,8 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
<span>{themeLabel}</span>
|
<span>{themeLabel}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
{!useInlineLocaleToggle && (
|
||||||
|
<>
|
||||||
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
||||||
{t('nav.language', 'Sprache')}
|
{t('nav.language', 'Sprache')}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
@@ -277,11 +382,13 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
className="flex items-center gap-2 font-sans-marketing dark:text-gray-100"
|
className="flex items-center gap-2 font-sans-marketing dark:text-gray-100"
|
||||||
>
|
>
|
||||||
<Languages className="h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
<span>{code.toUpperCase()}</span>
|
<span>{resolveLanguageLabel(code)}</span>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
<DropdownMenuLabel className="font-sans-marketing text-xs uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
||||||
@@ -394,6 +501,42 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
|
{useInlineLocaleToggle ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
|
||||||
|
{t('nav.language', 'Sprache')}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={t('nav.language', 'Sprache')}
|
||||||
|
className="grid grid-cols-2 gap-2"
|
||||||
|
>
|
||||||
|
{languageOptions.map((option) => {
|
||||||
|
const isActive = option.code === activeLocale;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.code}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={isActive}
|
||||||
|
aria-label={`${t('nav.language', 'Sprache')}: ${option.label}`}
|
||||||
|
onClick={() => handleLocaleChange(option.code)}
|
||||||
|
className={[
|
||||||
|
'flex items-center justify-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-pink-300 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900',
|
||||||
|
isActive
|
||||||
|
? 'border-pink-500 bg-pink-500 text-white shadow-sm'
|
||||||
|
: 'border-gray-200 bg-white text-gray-700 hover:border-pink-200 hover:bg-rose-50 hover:text-pink-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:hover:border-pink-400 dark:hover:bg-gray-800 dark:hover:text-pink-300',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{resolveFlag(option.code)}
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<label htmlFor="marketing-language-select-mobile" className="sr-only">
|
<label htmlFor="marketing-language-select-mobile" className="sr-only">
|
||||||
{t('nav.language', 'Sprache')}
|
{t('nav.language', 'Sprache')}
|
||||||
</label>
|
</label>
|
||||||
@@ -405,10 +548,12 @@ const MarketingLayout: React.FC<MarketingLayoutProps> = ({ children, title }) =>
|
|||||||
>
|
>
|
||||||
{supportedLocales.map((code) => (
|
{supportedLocales.map((code) => (
|
||||||
<option key={code} value={code}>
|
<option key={code} value={code}>
|
||||||
{code.toUpperCase()}
|
{resolveLanguageLabel(code)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -421,7 +421,11 @@
|
|||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"packages": "Pakete",
|
"packages": "Pakete",
|
||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"discover_packages": "Pakete entdecken"
|
"discover_packages": "Pakete entdecken",
|
||||||
|
"language": "Sprache",
|
||||||
|
"language_de": "Deutsch",
|
||||||
|
"language_en": "English",
|
||||||
|
"language_changed": "{{language}} ausgewählt"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"company": "S.E.B. Fotografie",
|
"company": "S.E.B. Fotografie",
|
||||||
|
|||||||
@@ -421,7 +421,11 @@
|
|||||||
"blog": "Blog",
|
"blog": "Blog",
|
||||||
"packages": "Packages",
|
"packages": "Packages",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"discover_packages": "Discover Packages"
|
"discover_packages": "Discover Packages",
|
||||||
|
"language": "Language",
|
||||||
|
"language_de": "Deutsch",
|
||||||
|
"language_en": "English",
|
||||||
|
"language_changed": "{{language}} selected"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"company": "S.E.B. Fotografie",
|
"company": "S.E.B. Fotografie",
|
||||||
|
|||||||
Reference in New Issue
Block a user