refactor(i18n): replace language-settings with locale-switcher

- Replace react-world-flags with next/image for better optimization
- Simplify locale handling logic and remove unused getUserLocale
- Rename component to be more descriptive (language-settings -> locale-switcher)
- Update all references to use the new component
- Add proper SVG flag assets for supported locales
- Remove react-world-flags dependency from package.json
This commit is contained in:
cfngc4594 2025-05-12 23:40:32 +08:00
parent 2e2e0315a8
commit 2efdc21419
11 changed files with 442 additions and 347 deletions

562
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -65,7 +65,6 @@
"react-hook-form": "^7.54.2",
"react-icons": "^5.5.0",
"react-resizable-panels": "^2.1.7",
"react-world-flags": "^1.6.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
@ -88,7 +87,6 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-world-flags": "^1.6.0",
"@types/tar-stream": "^3.1.3",
"eslint": "^9",
"eslint-config-next": "15.1.7",

1
public/flags/cn.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 20"><defs><path id="a" d="M0-1L.588.809-.952-.309H.952L-.588.809z" fill="#FF0"/></defs><path fill="#EE1C25" d="M0 0h30v20H0z"/><use xlink:href="#a" transform="matrix(3 0 0 3 5 5)"/><use xlink:href="#a" transform="rotate(23.036 .093 25.536)"/><use xlink:href="#a" transform="rotate(45.87 1.273 16.18)"/><use xlink:href="#a" transform="rotate(69.945 .996 12.078)"/><use xlink:href="#a" transform="rotate(20.66 -19.689 31.932)"/></svg>

After

Width:  |  Height:  |  Size: 531 B

1
public/flags/us.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>

After

Width:  |  Height:  |  Size: 741 B

View File

@ -2,9 +2,9 @@ import Link from "next/link";
import { Logo } from "@/components/logo";
import { Container } from "@/components/container";
import { ThemeToggle } from "@/components/theme-toggle";
import { LanguageSettings } from "@/components/language-settings";
import { LocaleSwitcher } from "@/components/locale-switcher";
const Header = () => {
export const Header = () => {
return (
<header>
<nav>
@ -15,7 +15,7 @@ const Header = () => {
</Link>
</div>
<div className="flex items-center gap-6">
<LanguageSettings />
<LocaleSwitcher />
<ThemeToggle />
</div>
</Container>
@ -23,5 +23,3 @@ const Header = () => {
</header>
);
};
export { Header };

View File

@ -1,65 +0,0 @@
"use client";
import Flag from "react-world-flags";
import { Globe } from "lucide-react";
import { useTranslations } from "next-intl";
import { Locale, locales } from "@/config/i18n";
import { useState, useMemo, useEffect } from "react";
import { getUserLocale, setUserLocale } from "@/i18n/locale";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
export function LanguageSettings() {
const t = useTranslations();
const [selectedOption, setSelectedOption] = useState<Locale>();
useEffect(() => {
const fetchLocale = async () => {
const userLocale = await getUserLocale();
if (!userLocale) return;
setSelectedOption(userLocale);
};
fetchLocale();
}, []);
const localeOptions = useMemo(() => {
const options = locales.map((locale) => ({
value: locale,
label: `${t(`LanguageSettings.${locale}.name`)}`,
}));
return options.sort((a, b) => a.value.localeCompare(b.value));
}, [t]);
const handleValueChange = async (value: Locale) => {
setSelectedOption(value);
await setUserLocale(value);
};
const getIconForLocale = (locale: Locale) => {
switch (locale) {
case "en":
return <Flag code="US" className="h-4 w-4 mr-2" />;
case "zh":
return <Flag code="CN" className="h-4 w-4 mr-2" />;
default:
return <Globe size={16} className="mr-2" />;
}
};
return (
<Select value={selectedOption} onValueChange={handleValueChange}>
<SelectTrigger className="w-[200px] shadow-none focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="w-[200px]">
{localeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex items-center">
{getIconForLocale(option.value)}
<span className="truncate">{option.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@ -0,0 +1,83 @@
"use client";
import Image from "next/image";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useLocale } from "next-intl";
import { useTransition } from "react";
import { LOCALES } from "@/config/i18n";
import { Locale } from "@/generated/client";
import { setUserLocale } from "@/i18n/locale";
const getIconForLocale = (locale: Locale) => {
switch (locale) {
case Locale.en:
return (
<Image
src="/flags/us.svg"
alt="English"
className="mr-2 h-4 w-4"
width={16}
height={16}
/>
);
case Locale.zh:
return (
<Image
src="/flags/cn.svg"
alt="中文"
className="mr-2 h-4 w-4"
width={16}
height={16}
/>
);
}
};
const getLabelForLocale = (locale: Locale) => {
switch (locale) {
case Locale.en:
return "English";
case Locale.zh:
return "中文";
}
};
export const LocaleSwitcher = () => {
const locale = useLocale();
const [isPending, startTransition] = useTransition();
const handleValueChange = (value: Locale) => {
const locale = value as Locale;
startTransition(() => {
setUserLocale(locale);
});
};
return (
<Select
value={locale}
onValueChange={handleValueChange}
disabled={isPending}
>
<SelectTrigger className="w-[150px] focus:ring-0 shadow-none">
<SelectValue placeholder="Select Locale" />
</SelectTrigger>
<SelectContent>
{LOCALES.map((locale) => (
<SelectItem key={locale} value={locale}>
<div className="flex items-center">
{getIconForLocale(locale)}
<span className="truncate">{getLabelForLocale(locale)}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
};

View File

@ -28,11 +28,11 @@ import {
import { useTranslations } from "next-intl";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useSettingsStore } from "@/stores/useSettingsStore";
import { LocaleSwitcher } from "@/components/locale-switcher";
import AppearanceSettings from "@/components/appearance-settings";
import { LanguageSettings } from "@/components/language-settings";
import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react";
export function SettingsDialog() {
export const SettingsDialog = () => {
const t = useTranslations("SettingsDialog");
const data = {
nav: [
@ -42,7 +42,8 @@ export function SettingsDialog() {
{ id: "Advanced", name: t("nav.Advanced"), icon: Settings },
],
};
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } = useSettingsStore();
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } =
useSettingsStore();
return (
<Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
@ -86,7 +87,9 @@ export function SettingsDialog() {
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>{t(`nav.${activeSetting}`)}</BreadcrumbPage>
<BreadcrumbPage>
{t(`nav.${activeSetting}`)}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
@ -95,7 +98,7 @@ export function SettingsDialog() {
<ScrollArea className="flex-1 overflow-y-auto p-4 pt-0">
<div className="flex flex-col gap-4">
{activeSetting === "Appearance" && <AppearanceSettings />}
{activeSetting === "Language" && <LanguageSettings />}
{activeSetting === "Language" && <LocaleSwitcher />}
</div>
</ScrollArea>
</main>
@ -103,4 +106,4 @@ export function SettingsDialog() {
</DialogContent>
</Dialog>
);
}
};

View File

@ -1,5 +1,7 @@
import { Locale } from "@/generated/client";
export const LOCALES = Object.values(Locale);
export const DEFAULT_LOCALE: Locale = Locale.en;
export const LOCALE_COOKIE_KEY = "judge4c_locale";

View File

@ -1,29 +1,8 @@
import "server-only";
"use server";
import { cookies } from "next/headers";
import { Locale } from "@/generated/client";
import { cookies, headers } from "next/headers";
import { DEFAULT_LOCALE, LOCALE_COOKIE_KEY } from "@/config/i18n";
const validLocales = Object.values(Locale);
export const getUserLocale = async () => {
const cookieLocale = (await cookies()).get(LOCALE_COOKIE_KEY)?.value;
if (validLocales.includes(cookieLocale as Locale)) {
return cookieLocale as Locale;
}
const acceptLanguage = (await headers())
.get("accept-language")
?.split(",")[0]
?.trim()
.toLowerCase();
const langPrefix = acceptLanguage?.slice(0, 2);
if (validLocales.includes(langPrefix as Locale)) {
return langPrefix as Locale;
}
return DEFAULT_LOCALE;
};
import { LOCALE_COOKIE_KEY } from "@/config/i18n";
export const setUserLocale = async (locale: Locale) => {
(await cookies()).set(LOCALE_COOKIE_KEY, locale);

View File

@ -1,7 +1,28 @@
import "server-only";
import { getUserLocale } from "@/i18n/locale";
import { Locale } from "@/generated/client";
import { cookies, headers } from "next/headers";
import { getRequestConfig } from "next-intl/server";
import { DEFAULT_LOCALE, LOCALE_COOKIE_KEY, LOCALES } from "@/config/i18n";
const getUserLocale = async () => {
const cookieLocale = (await cookies()).get(LOCALE_COOKIE_KEY)?.value;
if (LOCALES.includes(cookieLocale as Locale)) {
return cookieLocale as Locale;
}
const acceptLanguage = (await headers())
.get("accept-language")
?.split(",")[0]
?.trim()
.toLowerCase();
const langPrefix = acceptLanguage?.slice(0, 2);
if (LOCALES.includes(langPrefix as Locale)) {
return langPrefix as Locale;
}
return DEFAULT_LOCALE;
};
export default getRequestConfig(async () => {
const locale = await getUserLocale();