mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 09:31:55 +08:00
feat: init i18n
This commit is contained in:
parent
97087fe67d
commit
26871521d8
@ -27,10 +27,12 @@
|
||||
"clsx": "^2.1.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"framer-motion": "^11.11.17",
|
||||
"i18next": "^24.0.0",
|
||||
"lucide-react": "^0.460.0",
|
||||
"luxon": "^3.5.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.1.1",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-use-websocket": "^4.11.1",
|
||||
"recharts": "^2.13.3",
|
||||
|
@ -1,4 +1,3 @@
|
||||
// import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||
import { ModeToggle } from "@/components/ThemeSwitcher";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@ -6,6 +5,8 @@ import { fetchLoginUser } from "@/lib/nezha-api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DateTime } from "luxon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
@ -32,7 +33,7 @@ function Header() {
|
||||
</section>
|
||||
<section className="flex items-center gap-2">
|
||||
<DashboardLink />
|
||||
{/* <LanguageSwitcher /> */}
|
||||
<LanguageSwitcher />
|
||||
<ModeToggle />
|
||||
</section>
|
||||
</section>
|
||||
@ -80,6 +81,7 @@ const useInterval = (callback: () => void, delay: number | null) => {
|
||||
}, [delay]);
|
||||
};
|
||||
function Overview() {
|
||||
const { t } = useTranslation();
|
||||
const [mouted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@ -94,7 +96,7 @@ function Overview() {
|
||||
}, 1000);
|
||||
return (
|
||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
||||
<p className="text-base font-semibold">👋 Overview</p>
|
||||
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-sm font-medium opacity-50">where the time is</p>
|
||||
{mouted ? (
|
||||
|
56
src/components/LanguageSwitcher.tsx
Normal file
56
src/components/LanguageSwitcher.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const locale = i18n.language;
|
||||
|
||||
const handleSelect = (e: Event, newLocale: string) => {
|
||||
e.preventDefault(); // 阻止默认的关闭行为
|
||||
i18n.changeLanguage(newLocale);
|
||||
};
|
||||
|
||||
const localeItems = [
|
||||
{ name: t("language.zh-CN"), code: "zh-CN" },
|
||||
{ name: t("language.zh-TW"), code: "zh-TW" },
|
||||
{ name: t("language.en"), code: "en" },
|
||||
];
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full px-[9px] bg-white dark:bg-black"
|
||||
>
|
||||
{localeItems.find((item) => item.code === locale)?.name}
|
||||
<span className="sr-only">Change language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||
{localeItems.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.code}
|
||||
onSelect={(e) => handleSelect(e, item.code)}
|
||||
className={locale === item.code ? "bg-muted gap-3" : ""}
|
||||
>
|
||||
{item.name}{" "}
|
||||
{locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
@ -9,8 +9,11 @@ import { cn } from "@/lib/utils";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { Theme } from "@/components/ThemeProvider";
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function ModeToggle() {
|
||||
const {t} = useTranslation();
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||
@ -36,19 +39,22 @@ export function ModeToggle() {
|
||||
className={cn({ "gap-3 bg-muted": theme === "light" })}
|
||||
onSelect={(e) => handleSelect(e, "light")}
|
||||
>
|
||||
Light
|
||||
{t("theme.light")}
|
||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
||||
onSelect={(e) => handleSelect(e, "dark")}
|
||||
>
|
||||
Dark
|
||||
{t("theme.dark")}
|
||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
||||
onSelect={(e) => handleSelect(e, "system")}
|
||||
>
|
||||
System
|
||||
{t("theme.system")}
|
||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
|
29
src/i18n.js
Normal file
29
src/i18n.js
Normal file
@ -0,0 +1,29 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import enTranslation from "./locales/en/translation.json";
|
||||
import zhCNTranslation from "./locales/zh-CN/translation.json";
|
||||
import zhTWTranslation from "./locales/zh-TW/translation.json";
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
"zh-CN": {
|
||||
translation: zhCNTranslation,
|
||||
},
|
||||
"zh-TW": {
|
||||
translation: zhTWTranslation,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: "zh-CN", // 默认语言
|
||||
fallbackLng: "en", // 当前语言的翻译没有找到时,使用的备选语言
|
||||
interpolation: {
|
||||
escapeValue: false, // react已经安全地转义
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
13
src/locales/en/translation.json
Normal file
13
src/locales/en/translation.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"overview": "Overview",
|
||||
"language": {
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"en": "English"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
}
|
||||
}
|
13
src/locales/zh-CN/translation.json
Normal file
13
src/locales/zh-CN/translation.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"overview": "概览",
|
||||
"language": {
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"en": "English"
|
||||
},
|
||||
"theme": {
|
||||
"light": "亮色",
|
||||
"dark": "暗色",
|
||||
"system": "跟随系统"
|
||||
}
|
||||
}
|
13
src/locales/zh-TW/translation.json
Normal file
13
src/locales/zh-TW/translation.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"overview": "概覽",
|
||||
"language": {
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"en": "English"
|
||||
},
|
||||
"theme": {
|
||||
"light": "亮色",
|
||||
"dark": "暗色",
|
||||
"system": "跟随系統"
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
import "./i18n";
|
||||
import { ThemeProvider } from "./components/ThemeProvider";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
Loading…
x
Reference in New Issue
Block a user