feat: init i18n

This commit is contained in:
hamster1963 2024-11-24 20:51:43 +08:00
parent 97087fe67d
commit 26871521d8
11 changed files with 143 additions and 8 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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",

View File

@ -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 ? (

View 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>
);
}

View File

@ -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>

View File

@ -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
View 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;

View File

@ -0,0 +1,13 @@
{
"overview": "Overview",
"language": {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"en": "English"
},
"theme": {
"light": "Light",
"dark": "Dark",
"system": "System"
}
}

View File

@ -0,0 +1,13 @@
{
"overview": "概览",
"language": {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"en": "English"
},
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟随系统"
}
}

View File

@ -0,0 +1,13 @@
{
"overview": "概覽",
"language": {
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"en": "English"
},
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟随系統"
}
}

View File

@ -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";