mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +08:00
feat: init i18n
This commit is contained in:
parent
97087fe67d
commit
26871521d8
@ -27,10 +27,12 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"country-flag-icons": "^1.5.13",
|
"country-flag-icons": "^1.5.13",
|
||||||
"framer-motion": "^11.11.17",
|
"framer-motion": "^11.11.17",
|
||||||
|
"i18next": "^24.0.0",
|
||||||
"lucide-react": "^0.460.0",
|
"lucide-react": "^0.460.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-i18next": "^15.1.1",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-use-websocket": "^4.11.1",
|
"react-use-websocket": "^4.11.1",
|
||||||
"recharts": "^2.13.3",
|
"recharts": "^2.13.3",
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
|
||||||
import { ModeToggle } from "@/components/ThemeSwitcher";
|
import { ModeToggle } from "@/components/ThemeSwitcher";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@ -6,6 +5,8 @@ import { fetchLoginUser } from "@/lib/nezha-api";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
return (
|
return (
|
||||||
@ -32,7 +33,7 @@ function Header() {
|
|||||||
</section>
|
</section>
|
||||||
<section className="flex items-center gap-2">
|
<section className="flex items-center gap-2">
|
||||||
<DashboardLink />
|
<DashboardLink />
|
||||||
{/* <LanguageSwitcher /> */}
|
<LanguageSwitcher />
|
||||||
<ModeToggle />
|
<ModeToggle />
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@ -80,6 +81,7 @@ const useInterval = (callback: () => void, delay: number | null) => {
|
|||||||
}, [delay]);
|
}, [delay]);
|
||||||
};
|
};
|
||||||
function Overview() {
|
function Overview() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [mouted, setMounted] = useState(false);
|
const [mouted, setMounted] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
@ -94,7 +96,7 @@ function Overview() {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
return (
|
return (
|
||||||
<section className={"mt-10 flex flex-col md:mt-16"}>
|
<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">
|
<div className="flex items-center gap-1.5">
|
||||||
<p className="text-sm font-medium opacity-50">where the time is</p>
|
<p className="text-sm font-medium opacity-50">where the time is</p>
|
||||||
{mouted ? (
|
{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 { Moon, Sun } from "lucide-react";
|
||||||
import { Theme } from "@/components/ThemeProvider";
|
import { Theme } from "@/components/ThemeProvider";
|
||||||
import { useTheme } from "../hooks/use-theme";
|
import { useTheme } from "../hooks/use-theme";
|
||||||
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
|
const {t} = useTranslation();
|
||||||
const { setTheme, theme } = useTheme();
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||||
@ -36,19 +39,22 @@ export function ModeToggle() {
|
|||||||
className={cn({ "gap-3 bg-muted": theme === "light" })}
|
className={cn({ "gap-3 bg-muted": theme === "light" })}
|
||||||
onSelect={(e) => handleSelect(e, "light")}
|
onSelect={(e) => handleSelect(e, "light")}
|
||||||
>
|
>
|
||||||
Light
|
{t("theme.light")}
|
||||||
|
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
className={cn({ "gap-3 bg-muted": theme === "dark" })}
|
||||||
onSelect={(e) => handleSelect(e, "dark")}
|
onSelect={(e) => handleSelect(e, "dark")}
|
||||||
>
|
>
|
||||||
Dark
|
{t("theme.dark")}
|
||||||
|
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
className={cn({ "gap-3 bg-muted": theme === "system" })}
|
||||||
onSelect={(e) => handleSelect(e, "system")}
|
onSelect={(e) => handleSelect(e, "system")}
|
||||||
>
|
>
|
||||||
System
|
{t("theme.system")}
|
||||||
|
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
@ -63,7 +63,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@ -81,7 +81,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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",
|
inset && "pl-8",
|
||||||
className,
|
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 ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import "./i18n";
|
||||||
import { ThemeProvider } from "./components/ThemeProvider";
|
import { ThemeProvider } from "./components/ThemeProvider";
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user