返回 CodeWhale
locale-switcher.tsx
根目录 / web / components / locale-switcher.tsx
1 "use client";
2
3 import { useRouter, usePathname } from "next/navigation";
4 import { ALL_LOCALES } from "@/lib/i18n/config";
5 import { fill, getChrome } from "@/lib/i18n/dictionaries";
6 import { replacePathLocale } from "@/lib/i18n/path";
7 import { Icon } from "./icon";
8
9 /** Labels for the dropdown. Keyed by locale code, displayed in native script. */
10 const LOCALE_LABELS: Record<string, string> = {};
11 for (const l of ALL_LOCALES) {
12 LOCALE_LABELS[l.code] = l.label;
13 }
14
15 /** Routed locales that appear in the switcher (shipped + partial). */
16 const ROUTED = ALL_LOCALES.filter((l) => l.status === "shipped" || l.status === "partial");
17
18 export function LocaleSwitcher({ current }: { current: string }) {
19 const router = useRouter();
20 const pathname = usePathname();
21 const chrome = getChrome(current);
22
23 const switchLocale = (code: string) => {
24 if (code === current) return;
25 document.cookie = `NEXT_LOCALE=${code};path=/;max-age=${60 * 60 * 24 * 365}`;
26 router.push(replacePathLocale(pathname, code));
27 };
28
29 // If only 1 routed locale, no switcher needed.
30 if (ROUTED.length <= 1) return null;
31
32 // If exactly 2 routed locales, show a simple toggle.
33 if (ROUTED.length === 2) {
34 const other = ROUTED.find((l) => l.code !== current);
35 if (!other) return null;
36 return (
37 <button
38 type="button"
39 onClick={() => switchLocale(other.code)}
40 className="nav-icon-button"
41 aria-label={fill(chrome.switcherSwitchTo, { label: other.label })}
42 title={other.label}
43 >
44 <Icon name="globe" className="nav-icon" />
45 </button>
46 );
47 }
48
49 // 3+ routed locales: a globe icon over a native <select>, which keeps the
50 // platform picker, keyboard and screen-reader behavior. The select is
51 // transparent and covers the icon, so the whole icon is the hit target.
52 // Partial packs sit under a visible group label so the incomplete scope is
53 // honest at the point of selection. Each option names its language in its
54 // own script, so it carries that language's `lang` (and `dir`); the badge
55 // stays in the page language on the <optgroup> instead of being mixed into
56 // the option text.
57 const option = (l: (typeof ROUTED)[number]) => (
58 <option key={l.code} value={l.code} lang={l.code} dir={l.dir === "rtl" ? "rtl" : undefined}>
59 {l.label}
60 </option>
61 );
62 const partial = ROUTED.filter((l) => l.status === "partial");
63 // The badge is written as an inline "(partial)"; as a group heading it reads
64 // better without its (ASCII or full-width) brackets.
65 const partialHeading = chrome.partialBadge.replace(/^[((]\s*|\s*[))]$/g, "");
66 return (
67 <span className="nav-icon-button nav-locale">
68 <Icon name="globe" className="nav-icon" />
69 <select
70 value={current}
71 onChange={(e) => switchLocale(e.target.value)}
72 aria-label={chrome.switcherLabel}
73 >
74 {ROUTED.filter((l) => l.status !== "partial").map(option)}
75 {partial.length > 0 && <optgroup label={partialHeading}>{partial.map(option)}</optgroup>}
76 </select>
77 </span>
78 );
79 }
80
80 lines Plain Text