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:
67
apps/web/src/components/language-switcher.tsx
Normal file
67
apps/web/src/components/language-switcher.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
© {new Date().getFullYear()} Sase.tr. {t("footer.rights")}
|
© {new Date().getFullYear()} Sase.tr. {t("footer.rights")}
|
||||||
</p>
|
</p>
|
||||||
{languageSwitcher}
|
<LanguageSwitcher variant="inline" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user