feat(web): language switcher in dashboard header + shared component

Extract the footer's TR/EN toggle into a reusable LanguageSwitcher with
two variants: `inline` (segmented row, used by the footer) and
`dropdown` (round Globe icon + menu with active check). Add the dropdown
variant to the dashboard header next to the theme toggle so app users
can switch language without the marketing footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 23:08:12 +03:00
parent 70b530c020
commit 681a25ec2e
3 changed files with 74 additions and 31 deletions

View File

@@ -0,0 +1,67 @@
import { type Locale, useTranslation } from "@/lib/i18n";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@sase/ui";
import { Check, Globe } from "lucide-react";
const LOCALES: { code: Locale; label: string; name: string }[] = [
{ code: "tr", label: "TR", name: "Türkçe" },
{ code: "en", label: "EN", name: "English" },
];
/**
* Language switcher wired to the i18n store.
* - `inline`: segmented TR/EN row (footer)
* - `dropdown`: round Globe icon button + menu (app header)
*/
export function LanguageSwitcher({ variant = "inline" }: { variant?: "inline" | "dropdown" }) {
const { t, locale, setLocale } = useTranslation();
if (variant === "dropdown") {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={t("footer.language")}
className="flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<Globe className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
{LOCALES.map(({ code, name }) => (
<DropdownMenuItem
key={code}
onSelect={() => setLocale(code)}
className="justify-between"
>
{name}
{locale === code && <Check className="size-4 text-brand" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
return (
// biome-ignore lint/a11y/useSemanticElements: a button group, not a form fieldset; role="group" is the correct ARIA here
<div className="flex items-center gap-1 text-sm" role="group" aria-label={t("footer.language")}>
<Globe className="mr-1 size-4 text-muted-foreground" aria-hidden="true" />
{LOCALES.map(({ code, label }) => (
<button
key={code}
type="button"
onClick={() => setLocale(code)}
aria-pressed={locale === code}
className={`rounded-md px-2 py-1 transition-colors ${
locale === code
? "font-semibold text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{label}
</button>
))}
</div>
);
}

View File

@@ -1,12 +1,8 @@
import { type Locale, useTranslation } from "@/lib/i18n"; import { LanguageSwitcher } from "@/components/language-switcher";
import { useTranslation } from "@/lib/i18n";
import { Separator } from "@sase/ui"; import { Separator } from "@sase/ui";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Facebook, Globe, Instagram, Linkedin } from "lucide-react"; import { Facebook, Instagram, Linkedin } from "lucide-react";
const LOCALES: { code: Locale; label: string }[] = [
{ code: "tr", label: "TR" },
{ code: "en", label: "EN" },
];
const SOCIALS: { const SOCIALS: {
label: string; label: string;
@@ -23,29 +19,7 @@ const SOCIALS: {
]; ];
export function SiteFooter({ variant = "full" }: { variant?: "full" | "compact" }) { export function SiteFooter({ variant = "full" }: { variant?: "full" | "compact" }) {
const { t, locale, setLocale } = useTranslation(); const { t } = useTranslation();
const languageSwitcher = (
// biome-ignore lint/a11y/useSemanticElements: a button group, not a form fieldset; role="group" is the correct ARIA here
<div className="flex items-center gap-1 text-sm" role="group" aria-label={t("footer.language")}>
<Globe className="mr-1 size-4 text-muted-foreground" aria-hidden="true" />
{LOCALES.map(({ code, label }) => (
<button
key={code}
type="button"
onClick={() => setLocale(code)}
aria-pressed={locale === code}
className={`rounded-md px-2 py-1 transition-colors ${
locale === code
? "font-semibold text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{label}
</button>
))}
</div>
);
// Compact variant — single line for app/dashboard layouts. // Compact variant — single line for app/dashboard layouts.
if (variant === "compact") { if (variant === "compact") {
@@ -183,7 +157,7 @@ export function SiteFooter({ variant = "full" }: { variant?: "full" | "compact"
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
&copy; {new Date().getFullYear()} Sase.tr. {t("footer.rights")} &copy; {new Date().getFullYear()} Sase.tr. {t("footer.rights")}
</p> </p>
{languageSwitcher} <LanguageSwitcher variant="inline" />
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -1,3 +1,4 @@
import { LanguageSwitcher } from "@/components/language-switcher";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import { TrialUrgencyBanner } from "@/components/trial-urgency-banner"; import { TrialUrgencyBanner } from "@/components/trial-urgency-banner";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -493,6 +494,7 @@ function DashboardLayout() {
</Button> </Button>
</Link> </Link>
)} )}
<LanguageSwitcher variant="dropdown" />
<button <button
type="button" type="button"
onClick={toggleTheme} onClick={toggleTheme}