mirror of
https://github.com/massbug/judge4c.git
synced 2025-05-17 23:12:23 +00:00
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:
parent
2e2e0315a8
commit
2efdc21419
@ -65,7 +65,6 @@
|
|||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"react-world-flags": "^1.6.0",
|
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
@ -88,7 +87,6 @@
|
|||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@types/react-world-flags": "^1.6.0",
|
|
||||||
"@types/tar-stream": "^3.1.3",
|
"@types/tar-stream": "^3.1.3",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.7",
|
"eslint-config-next": "15.1.7",
|
||||||
|
1
public/flags/cn.svg
Normal file
1
public/flags/cn.svg
Normal 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
1
public/flags/us.svg
Normal 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 |
@ -2,9 +2,9 @@ import Link from "next/link";
|
|||||||
import { Logo } from "@/components/logo";
|
import { Logo } from "@/components/logo";
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { LanguageSettings } from "@/components/language-settings";
|
import { LocaleSwitcher } from "@/components/locale-switcher";
|
||||||
|
|
||||||
const Header = () => {
|
export const Header = () => {
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<nav>
|
<nav>
|
||||||
@ -15,7 +15,7 @@ const Header = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<LanguageSettings />
|
<LocaleSwitcher />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
@ -23,5 +23,3 @@ const Header = () => {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Header };
|
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
83
src/components/locale-switcher.tsx
Normal file
83
src/components/locale-switcher.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -28,11 +28,11 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useSettingsStore } from "@/stores/useSettingsStore";
|
import { useSettingsStore } from "@/stores/useSettingsStore";
|
||||||
|
import { LocaleSwitcher } from "@/components/locale-switcher";
|
||||||
import AppearanceSettings from "@/components/appearance-settings";
|
import AppearanceSettings from "@/components/appearance-settings";
|
||||||
import { LanguageSettings } from "@/components/language-settings";
|
|
||||||
import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react";
|
import { CodeXml, Globe, Paintbrush, Settings } from "lucide-react";
|
||||||
|
|
||||||
export function SettingsDialog() {
|
export const SettingsDialog = () => {
|
||||||
const t = useTranslations("SettingsDialog");
|
const t = useTranslations("SettingsDialog");
|
||||||
const data = {
|
const data = {
|
||||||
nav: [
|
nav: [
|
||||||
@ -42,7 +42,8 @@ export function SettingsDialog() {
|
|||||||
{ id: "Advanced", name: t("nav.Advanced"), icon: Settings },
|
{ id: "Advanced", name: t("nav.Advanced"), icon: Settings },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } = useSettingsStore();
|
const { isDialogOpen, activeSetting, setDialogOpen, setActiveSetting } =
|
||||||
|
useSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
||||||
@ -86,7 +87,9 @@ export function SettingsDialog() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator className="hidden md:block" />
|
<BreadcrumbSeparator className="hidden md:block" />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbPage>{t(`nav.${activeSetting}`)}</BreadcrumbPage>
|
<BreadcrumbPage>
|
||||||
|
{t(`nav.${activeSetting}`)}
|
||||||
|
</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
@ -95,7 +98,7 @@ export function SettingsDialog() {
|
|||||||
<ScrollArea className="flex-1 overflow-y-auto p-4 pt-0">
|
<ScrollArea className="flex-1 overflow-y-auto p-4 pt-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{activeSetting === "Appearance" && <AppearanceSettings />}
|
{activeSetting === "Appearance" && <AppearanceSettings />}
|
||||||
{activeSetting === "Language" && <LanguageSettings />}
|
{activeSetting === "Language" && <LocaleSwitcher />}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</main>
|
</main>
|
||||||
@ -103,4 +106,4 @@ export function SettingsDialog() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Locale } from "@/generated/client";
|
import { Locale } from "@/generated/client";
|
||||||
|
|
||||||
|
export const LOCALES = Object.values(Locale);
|
||||||
|
|
||||||
export const DEFAULT_LOCALE: Locale = Locale.en;
|
export const DEFAULT_LOCALE: Locale = Locale.en;
|
||||||
|
|
||||||
export const LOCALE_COOKIE_KEY = "judge4c_locale";
|
export const LOCALE_COOKIE_KEY = "judge4c_locale";
|
||||||
|
@ -1,29 +1,8 @@
|
|||||||
import "server-only";
|
"use server";
|
||||||
|
|
||||||
|
import { cookies } from "next/headers";
|
||||||
import { Locale } from "@/generated/client";
|
import { Locale } from "@/generated/client";
|
||||||
import { cookies, headers } from "next/headers";
|
import { LOCALE_COOKIE_KEY } from "@/config/i18n";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setUserLocale = async (locale: Locale) => {
|
export const setUserLocale = async (locale: Locale) => {
|
||||||
(await cookies()).set(LOCALE_COOKIE_KEY, locale);
|
(await cookies()).set(LOCALE_COOKIE_KEY, locale);
|
||||||
|
@ -1,7 +1,28 @@
|
|||||||
import "server-only";
|
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 { 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 () => {
|
export default getRequestConfig(async () => {
|
||||||
const locale = await getUserLocale();
|
const locale = await getUserLocale();
|
||||||
|
Loading…
Reference in New Issue
Block a user