wip: i18n chinese

This commit is contained in:
elvis liao 2024-09-26 17:57:30 +08:00
parent 85df8eb09d
commit 0abb030889
18 changed files with 289 additions and 184 deletions

View File

@ -1,4 +1,5 @@
import { Moon, Sun } from "lucide-react";
import { useTranslation } from 'react-i18next'
import { Button } from "@/components/ui/button";
import {
@ -11,6 +12,8 @@ import { useTheme } from "./ThemeProvider";
export function ThemeToggle() {
const { setTheme } = useTheme();
const { t } = useTranslation();
return (
<DropdownMenu>
@ -23,13 +26,13 @@ export function ThemeToggle() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
{t('theme.light')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
{t('theme.dark')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
{t('theme.system')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { Separator } from "../ui/separator";
type DeployProgressProps = {
@ -6,26 +8,40 @@ type DeployProgressProps = {
};
const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
const { t } = useTranslation();
let rs = <> </>;
if (phase === "check") {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-red-600"> </div>
<div className="text-xs text-nowrap text-red-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div>
);
}
@ -35,21 +51,33 @@ const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600"></div>
<div className="text-xs text-nowrap text-red-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow" />
<div className="text-xs text-nowrap text-muted-foreground"></div>
<div className="text-xs text-nowrap text-muted-foreground">
{t('deploy.progress.deploy')}
</div>
</div>
);
}
@ -59,21 +87,33 @@ const DeployProgress = ({ phase, phaseSuccess }: DeployProgressProps) => {
if (phaseSuccess) {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.deploy')}
</div>
</div>
);
} else {
rs = (
<div className="flex items-center">
<div className="text-xs text-nowrap text-green-600"> </div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.check')}
</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-green-600"></div>
<div className="text-xs text-nowrap text-green-600">
{t('deploy.progress.apply')}
</div>
<Separator className="h-1 grow bg-green-600" />
<div className="text-xs text-nowrap text-red-600"></div>
<div className="text-xs text-nowrap text-red-600">
{t('deploy.progress.deploy')}
</div>
</div>
);
}

View File

@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { useTranslation } from 'react-i18next'
type DingTalkSetting = {
id: string;
@ -17,6 +18,7 @@ type DingTalkSetting = {
const DingTalk = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [dingtalk, setDingtalk] = useState<DingTalkSetting>({
id: config.id ?? "",
@ -70,15 +72,15 @@ const DingTalk = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t('save.succeed'),
description: t('setting.notify.config.save.succeed'),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t('save.failed'),
description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive",
});
}
@ -127,7 +129,7 @@ const DingTalk = () => {
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
<Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div>
<div className="flex justify-end mt-2">
@ -136,7 +138,7 @@ const DingTalk = () => {
handleSaveClick();
}}
>
{t('save')}
</Button>
</div>
</div>

View File

@ -9,6 +9,7 @@ import {
} from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { useToast } from "../ui/use-toast";
import { useTranslation } from 'react-i18next'
const NotifyTemplate = () => {
const [id, setId] = useState("");
@ -17,6 +18,7 @@ const NotifyTemplate = () => {
]);
const { toast } = useToast();
const { t } = useTranslation();
useEffect(() => {
const featchData = async () => {
@ -66,8 +68,8 @@ const NotifyTemplate = () => {
}
toast({
title: "保存成功",
description: "通知模板保存成功",
title: t('save.succeed'),
description: t('setting.notify.template.save.succeed'),
});
};
@ -81,7 +83,7 @@ const NotifyTemplate = () => {
/>
<div className="text-muted-foreground text-sm mt-1">
, COUNT:即将过期张数
{t('setting.notify.template.variables.tips.title')}
</div>
<Textarea
@ -92,10 +94,10 @@ const NotifyTemplate = () => {
}}
></Textarea>
<div className="text-muted-foreground text-sm mt-1">
, COUNT:即将过期张数DOMAINS:域名列表
{t('setting.notify.template.variables.tips.content')}
</div>
<div className="flex justify-end mt-2">
<Button onClick={handleSaveClick}></Button>
<Button onClick={handleSaveClick}>{t('save')}</Button>
</div>
</div>
);

View File

@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { useTranslation } from "react-i18next";
type TelegramSetting = {
id: string;
@ -17,6 +18,7 @@ type TelegramSetting = {
const Telegram = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [telegram, setTelegram] = useState<TelegramSetting>({
id: config.id ?? "",
@ -70,15 +72,15 @@ const Telegram = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t('save.succeed'),
description: t('setting.notify.config.save.succeed'),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t('save.failed'),
description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive",
});
}
@ -128,7 +130,7 @@ const Telegram = () => {
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
<Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div>
<div className="flex justify-end mt-2">
@ -137,7 +139,7 @@ const Telegram = () => {
handleSaveClick();
}}
>
{t('save')}
</Button>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { update } from "@/repository/settings";
import { getErrMessage } from "@/lib/error";
import { useToast } from "../ui/use-toast";
import { isValidURL } from "@/lib/url";
import { useTranslation } from 'react-i18next'
type WebhookSetting = {
id: string;
@ -18,6 +19,7 @@ type WebhookSetting = {
const Webhook = () => {
const { config, setChannels } = useNotify();
const { t } = useTranslation();
const [webhook, setWebhook] = useState<WebhookSetting>({
id: config.id ?? "",
@ -59,8 +61,8 @@ const Webhook = () => {
webhook.data.url = webhook.data.url.trim();
if (!isValidURL(webhook.data.url)) {
toast({
title: "保存失败",
description: "Url格式不正确",
title: t('save.failed'),
description: t('setting.notify.config.save.failed.url.not.valid'),
variant: "destructive",
});
return;
@ -79,15 +81,15 @@ const Webhook = () => {
setChannels(resp);
toast({
title: "保存成功",
description: "配置保存成功",
title: t('save.succeed'),
description: t('setting.notify.config.save.succeed'),
});
} catch (e) {
const msg = getErrMessage(e);
toast({
title: "保存失败",
description: "配置保存失败:" + msg,
title: t('save.failed'),
description: `${t('setting.notify.config.save.failed')}: ${msg}`,
variant: "destructive",
});
}
@ -123,7 +125,7 @@ const Webhook = () => {
});
}}
/>
<Label htmlFor="airplane-mode"></Label>
<Label htmlFor="airplane-mode">{t('setting.notify.config.enable')}</Label>
</div>
<div className="flex justify-end mt-2">
@ -132,7 +134,7 @@ const Webhook = () => {
handleSaveClick();
}}
>
{t('save')}
</Button>
</div>
</div>

View File

@ -12,6 +12,7 @@ import {
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { useTranslation } from "react-i18next"
const Form = FormProvider
@ -145,7 +146,9 @@ const FormMessage = React.forwardRef<
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
const { t } = useTranslation()
const body = error ? t(String(error?.message)) : children
if (!body) {
return null

View File

@ -3,6 +3,7 @@ import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
@ -62,33 +63,41 @@ PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span></span>
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
const { t } = useTranslation()
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>{t('pagination.prev')}</span>
</PaginationLink>
)
};
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span></span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
}: React.ComponentProps<typeof PaginationLink>) => {
const { t } = useTranslation()
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>{t('pagination.next')}</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
};
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({

View File

@ -6,6 +6,7 @@ import {
useNavigate,
} from "react-router-dom";
import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
@ -22,12 +23,14 @@ import { cn } from "@/lib/utils";
import { ConfigProvider } from "@/providers/config";
import { getPb } from "@/repository/api";
import { ThemeToggle } from "@/components/ThemeToggle";
import LocaleToggle from "@/components/LocaleToggle";
import Version from "@/components/certimate/Version";
export default function Dashboard() {
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation()
if (!getPb().authStore.isValid || !getPb().authStore.isAdmin) {
return <Navigate to="/login" />;
@ -70,7 +73,7 @@ export default function Dashboard() {
)}
>
<Home className="h-4 w-4" />
{t('dashboard')}
</Link>
<Link
to="/domains"
@ -80,7 +83,7 @@ export default function Dashboard() {
)}
>
<Earth className="h-4 w-4" />
{t('domain.management.name')}
</Link>
<Link
to="/access"
@ -90,7 +93,7 @@ export default function Dashboard() {
)}
>
<Server className="h-4 w-4" />
{t('menu.auth.management')}
</Link>
<Link
@ -101,7 +104,7 @@ export default function Dashboard() {
)}
>
<History className="h-4 w-4" />
{t('deployment.log.name')}
</Link>
</nav>
</div>
@ -138,7 +141,7 @@ export default function Dashboard() {
)}
>
<Home className="h-5 w-5" />
{t('dashboard')}
</Link>
<Link
to="/domains"
@ -148,7 +151,7 @@ export default function Dashboard() {
)}
>
<Earth className="h-5 w-5" />
{t('domain.management.name')}
</Link>
<Link
to="/access"
@ -158,7 +161,7 @@ export default function Dashboard() {
)}
>
<Server className="h-5 w-5" />
{t('menu.auth.management')}
</Link>
<Link
@ -169,13 +172,14 @@ export default function Dashboard() {
)}
>
<History className="h-5 w-5" />
{t('deployment.log.name')}
</Link>
</nav>
</SheetContent>
</Sheet>
<div className="w-full flex-1"></div>
<ThemeToggle />
<LocaleToggle />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@ -188,15 +192,15 @@ export default function Dashboard() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuLabel>{t('account')}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSettingClick}>
{t('setting')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogoutClick}>
退
{t('logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -4,11 +4,13 @@ import { KeyRound, Megaphone, UserRound } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const SettingLayout = () => {
const location = useLocation();
const [tabValue, setTabValue] = useState("account");
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const pathname = location.pathname;
@ -20,7 +22,7 @@ const SettingLayout = () => {
<div>
<Toaster />
<div className="text-muted-foreground border-b dark:border-stone-500 py-5">
{t('setting')}
</div>
<div className="w-full mt-5 p-0 md:p-3 flex justify-center">
<Tabs defaultValue="account" className="w-full" value={tabValue}>
@ -33,7 +35,7 @@ const SettingLayout = () => {
className="px-5"
>
<UserRound size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t('account')}</div>
</TabsTrigger>
<TabsTrigger
value="password"
@ -43,7 +45,7 @@ const SettingLayout = () => {
className="px-5"
>
<KeyRound size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t('password')}</div>
</TabsTrigger>
<TabsTrigger
@ -54,7 +56,7 @@ const SettingLayout = () => {
className="px-5"
>
<Megaphone size={14} />
<div className="ml-1"></div>
<div className="ml-1">{t('setting.notify.menu')}</div>
</TabsTrigger>
</TabsList>
<TabsContent value={tabValue}>

View File

@ -24,12 +24,14 @@ import {
} from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
const Dashboard = () => {
const [statistic, setStatistic] = useState<Statistic>();
const [deployments, setDeployments] = useState<Deployment[]>();
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const fetchStatistic = async () => {
@ -55,7 +57,7 @@ const Dashboard = () => {
return (
<div className="flex flex-col">
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">{t('dashboard')}</div>
</div>
<div className="flex mt-10 gap-5 flex-col flex-wrap md:flex-row">
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
@ -63,7 +65,9 @@ const Dashboard = () => {
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t('dashboard.all')}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.total ? (
@ -74,7 +78,9 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div>
</div>
</div>
@ -84,7 +90,9 @@ const Dashboard = () => {
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t('dashboard.near.expired')}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.expired ? (
@ -95,7 +103,9 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div>
</div>
</div>
@ -109,7 +119,9 @@ const Dashboard = () => {
/>
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">
{t('dashboard.enabled')}
</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.enabled ? (
@ -120,7 +132,9 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div>
</div>
</div>
@ -130,7 +144,7 @@ const Dashboard = () => {
<Ban size={48} strokeWidth={1} className="text-gray-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="text-muted-foreground font-semibold">{t('dashboard.not.enabled')}</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? (
@ -144,19 +158,23 @@ const Dashboard = () => {
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200"></div>
<div className="ml-1 text-stone-700 dark:text-stone-200">
{t("dashboard.unit")}
</div>
</div>
</div>
</div>
</div>
<div>
<div className="text-muted-foreground mt-5 text-sm"></div>
<div className="text-muted-foreground mt-5 text-sm">
{t('deployment.log.name')}
</div>
{deployments?.length == 0 ? (
<>
<Alert className="max-w-[40em] mt-10">
<AlertTitle></AlertTitle>
<AlertTitle>{t('no.data')}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
@ -164,7 +182,7 @@ const Dashboard = () => {
</div>
<div className="ml-2">
{" "}
{t('deployment.log.empty')}
</div>
</div>
<div className="mt-2 flex justify-end">
@ -173,7 +191,7 @@ const Dashboard = () => {
navigate("/edit");
}}
>
{t('domain.add')}
</Button>
</div>
</AlertDescription>
@ -182,16 +200,16 @@ const Dashboard = () => {
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div>
<div className="w-48">{t('domain')}</div>
<div className="w-24"></div>
<div className="w-56"></div>
<div className="w-56 sm:ml-2 text-center"></div>
<div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56">{t('deployment.log.stage')}</div>
<div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
<div className="grow"></div>
<div className="grow">{t('operation')}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
{t('deployment.log.name')}
</div>
{deployments?.map((deployment) => (
@ -218,14 +236,14 @@ const Dashboard = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t('deployment.log.detail.button.text')}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t('deployment.log.detail')}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@ -38,6 +38,7 @@ import EmailsEdit from "@/components/certimate/EmailsEdit";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { EmailsSetting } from "@/domain/settings";
import { useTranslation } from "react-i18next";
const Edit = () => {
const {
@ -47,6 +48,7 @@ const Edit = () => {
const [domain, setDomain] = useState<Domain>();
const location = useLocation();
const { t } = useTranslation();
const [tab, setTab] = useState<"base" | "advance">("base");
@ -69,15 +71,15 @@ const Edit = () => {
const formSchema = z.object({
id: z.string().optional(),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: "请输入正确的域名",
message: t('domain.management.edit.domain.verify.tips'),
}),
email: z.string().email().optional(),
access: z.string().regex(/^[a-zA-Z0-9]+$/, {
message: "请选择DNS服务商授权配置",
message: t('domain.management.edit.dns.verify.tips'),
}),
targetAccess: z.string().optional(),
targetType: z.string().regex(/^[a-zA-Z0-9-]+$/, {
message: "请选择部署服务类型",
message: t('domain.management.edit.target.type.verify.tips'),
}),
variables: z.string().optional(),
group: z.string().optional(),

View File

@ -35,11 +35,13 @@ import { TooltipContent, TooltipProvider } from "@radix-ui/react-tooltip";
import { Earth } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useTranslation, Trans } from "react-i18next";
const Home = () => {
const toast = useToast();
const navigate = useNavigate();
const { t } = useTranslation()
const location = useLocation();
const query = new URLSearchParams(location.search);
@ -127,23 +129,22 @@ const Home = () => {
await save(domain);
toast.toast({
title: "操作成功",
description: "已发起部署,请稍后查看部署日志。",
title: t('operation.succeed'),
description: t('domain.management.start.deploy.succeed.tips'),
});
} catch (e) {
toast.toast({
title: "执行失败",
title: t('domain.management.execution.failed'),
description: (
<>
// 这里的 text 只是占位作用,实际文案在 src/i18n/locales/[lang].json
<Trans i18nKey="domain.management.execution.failed.tips">
text1
<Link
to={`/history?domain=${domain.id}`}
className="underline text-blue-500"
>
</Link>
</>
>text2</Link>
text3
</Trans>
),
variant: "destructive",
});
@ -175,8 +176,10 @@ const Home = () => {
<div className="">
<Toaster />
<div className="flex justify-between items-center">
<div className="text-muted-foreground"></div>
<Button onClick={handleCreateClick}></Button>
<div className="text-muted-foreground">{t('domain.management.name')}</div>
<Button onClick={handleCreateClick}>
{t('domain.add')}
</Button>
</div>
{!domains.length ? (
@ -187,26 +190,26 @@ const Home = () => {
</span>
<div className="text-center text-sm text-muted-foreground mt-3">
{t('domain.management.empty')}
</div>
<Button onClick={handleCreateClick} className="mt-3">
{t('domain.add')}
</Button>
</div>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-36"></div>
<div className="w-40"></div>
<div className="w-32"></div>
<div className="w-64"></div>
<div className="w-40 sm:ml-2"></div>
<div className="w-24"></div>
<div className="grow"></div>
<div className="w-36">{t('domain')}</div>
<div className="w-40">{t('domain.management.expiry.date')}</div>
<div className="w-32">{t('domain.management.last.execution.status')}</div>
<div className="w-64">{t('domain.management.last.execution.stage')}</div>
<div className="w-40 sm:ml-2">{t('domain.management.last.execution.time')}</div>
<div className="w-24">{t('domain.management.enable')}</div>
<div className="grow">{t('operation')}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
{t('domain')}
</div>
{domains.map((domain) => (
@ -221,8 +224,8 @@ const Home = () => {
<div>
{domain.expiredAt ? (
<>
<div>90</div>
<div>{getDate(domain.expiredAt)}</div>
<div>{t('domain.management.expiry.date1', { date: 90 })}</div>
<div>{t('domain.management.expiry.date2', { date: getDate(domain.expiredAt) })}</div>
</>
) : (
"---"
@ -266,7 +269,7 @@ const Home = () => {
</TooltipTrigger>
<TooltipContent>
<div className="border rounded-sm px-3 bg-background text-muted-foreground text-xs">
{domain.enabled ? "禁用" : "启用"}
{domain.enabled ? t('disable') : t('enable')}
</div>
</TooltipContent>
</Tooltip>
@ -278,7 +281,7 @@ const Home = () => {
className="p-0"
onClick={() => handleHistoryClick(domain.id)}
>
{t('deployment.log.name')}
</Button>
<Show when={domain.enabled ? true : false}>
<Separator orientation="vertical" className="h-4 mx-2" />
@ -287,7 +290,7 @@ const Home = () => {
className="p-0"
onClick={() => handleRightNowClick(domain)}
>
{t('domain.management.start.deploying')}
</Button>
</Show>
@ -304,7 +307,7 @@ const Home = () => {
className="p-0"
onClick={() => handleForceClick(domain)}
>
{t('domain.management.forced.deployment')}
</Button>
</Show>
@ -315,7 +318,7 @@ const Home = () => {
className="p-0"
onClick={() => handleDownloadClick(domain)}
>
{t('download')}
</Button>
</Show>
@ -325,24 +328,24 @@ const Home = () => {
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant={"link"} className="p-0">
{t('delete')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogTitle>{t('domain.delete')}</AlertDialogTitle>
<AlertDialogDescription>
{t('domain.management.delete.confirm')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDeleteClick(domain.id);
}}
>
{t('confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -354,7 +357,7 @@ const Home = () => {
className="p-0"
onClick={() => handleEditClick(domain.id)}
>
{t('edit')}
</Button>
</>
)}

View File

@ -17,11 +17,13 @@ import { list } from "@/repository/deployment";
import { Smile } from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
const History = () => {
const navigate = useNavigate();
const [deployments, setDeployments] = useState<Deployment[]>();
const [searchParams] = useSearchParams();
const { t } = useTranslation();
const domain = searchParams.get("domain");
useEffect(() => {
@ -38,11 +40,11 @@ const History = () => {
return (
<ScrollArea className="h-[80vh] overflow-hidden">
<div className="text-muted-foreground"></div>
<div className="text-muted-foreground">{t('deployment.log.name')}</div>
{!deployments?.length ? (
<>
<Alert className="max-w-[40em] mx-auto mt-20">
<AlertTitle></AlertTitle>
<AlertTitle>{t('no.data')}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
@ -50,7 +52,7 @@ const History = () => {
</div>
<div className="ml-2">
{" "}
{t('deployment.log.empty')}
</div>
</div>
<div className="mt-2 flex justify-end">
@ -59,7 +61,7 @@ const History = () => {
navigate("/");
}}
>
{t('domain.add')}
</Button>
</div>
</AlertDescription>
@ -68,16 +70,16 @@ const History = () => {
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48"></div>
<div className="w-48">{t('domain')}</div>
<div className="w-24"></div>
<div className="w-56"></div>
<div className="w-56 sm:ml-2 text-center"></div>
<div className="w-24">{t('deployment.log.status')}</div>
<div className="w-56">{t('deployment.log.stage')}</div>
<div className="w-56 sm:ml-2 text-center">{t('deployment.log.last.execution.time')}</div>
<div className="grow"></div>
<div className="grow">{t('operation')}</div>
</div>
<div className="sm:hidden flex text-sm text-muted-foreground">
{t('deployment.log.name')}
</div>
{deployments?.map((deployment) => (
@ -104,14 +106,14 @@ const History = () => {
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t('deployment.log.detail.button.text')}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t('deployment.log.detail')}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">

View File

@ -1,3 +1,8 @@
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
import { useTranslation } from 'react-i18next'
import { Button } from "@/components/ui/button";
import {
Form,
@ -11,20 +16,19 @@ import { Input } from "@/components/ui/input";
import { getErrMessage } from "@/lib/error";
import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { z } from "zod";
const formSchema = z.object({
username: z.string().email({
message: "请输入正确的邮箱地址",
message: "login.username.no.empty.message",
}),
password: z.string().min(10, {
message: "密码至少10个字符",
message: "login.password.length.message",
}),
});
const Login = () => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@ -61,7 +65,7 @@ const Login = () => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('username')}</FormLabel>
<FormControl>
<Input placeholder="email" {...field} />
</FormControl>
@ -76,7 +80,7 @@ const Login = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} type="password" />
</FormControl>
@ -86,7 +90,7 @@ const Login = () => {
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t('login.submit')}</Button>
</div>
</form>
</Form>

View File

@ -15,16 +15,18 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod";
const formSchema = z.object({
email: z.string().email("请输入正确的邮箱"),
email: z.string().email("setting.account.email.valid.message"),
});
const Account = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const [changed, setChanged] = useState(false);
@ -43,8 +45,8 @@ const Account = () => {
getPb().authStore.clear();
toast({
title: "修改账户邮箱功",
description: "请重新登录",
title: t("setting.account.email.change.succeed"),
description: t("setting.account.log.back.in"),
});
setTimeout(() => {
navigate("/login");
@ -52,7 +54,7 @@ const Account = () => {
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改账户邮箱失败",
title: t("setting.account.email.change.failed"),
description: message,
variant: "destructive",
});
@ -72,10 +74,10 @@ const Account = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('email')}</FormLabel>
<FormControl>
<Input
placeholder="请输入邮箱"
placeholder={t('setting.email.placeholder')}
{...field}
type="email"
onChange={(e) => {
@ -92,10 +94,10 @@ const Account = () => {
<div className="flex justify-end">
{changed ? (
<Button type="submit"></Button>
<Button type="submit">{t('setting.submit')}</Button>
) : (
<Button type="submit" disabled variant={"secondary"}>
{t('setting.submit')}
</Button>
)}
</div>

View File

@ -9,15 +9,18 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { NotifyProvider } from "@/providers/notify";
import { useTranslation } from "react-i18next";
const Notify = () => {
const { t } = useTranslation();
return (
<>
<NotifyProvider>
<div className="border rounded-sm p-5 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger></AccordionTrigger>
<AccordionTrigger>{t('template')}</AccordionTrigger>
<AccordionContent>
<NotifyTemplate />
</AccordionContent>

View File

@ -14,29 +14,31 @@ import { getPb } from "@/repository/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod";
const formSchema = z
.object({
oldPassword: z.string().min(10, {
message: "密码至少10个字符",
message: "setting.password.length.message",
}),
newPassword: z.string().min(10, {
message: "密码至少10个字符",
message: "setting.password.length.message",
}),
confirmPassword: z.string().min(10, {
message: "密码至少10个字符",
message: "setting.password.length.message",
}),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: "两次密码不一致",
message: "setting.password.not.match",
path: ["confirmPassword"],
});
const Password = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@ -66,8 +68,8 @@ const Password = () => {
getPb().authStore.clear();
toast({
title: "修改密码成功",
description: "请重新登录",
title: t('setting.password.change.succeed'),
description: t("setting.account.log.back.in"),
});
setTimeout(() => {
navigate("/login");
@ -75,7 +77,7 @@ const Password = () => {
} catch (e) {
const message = getErrMessage(e);
toast({
title: "修改密码失败",
title: t('setting.password.change.failed'),
description: message,
variant: "destructive",
});
@ -95,9 +97,9 @@ const Password = () => {
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('setting.password.current.password')}</FormLabel>
<FormControl>
<Input placeholder="当前密码" {...field} type="password" />
<Input placeholder={t('setting.password.current.password')} {...field} type="password" />
</FormControl>
<FormMessage />
@ -110,7 +112,7 @@ const Password = () => {
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('setting.password.new.password')}</FormLabel>
<FormControl>
<Input
placeholder="newPassword"
@ -129,7 +131,7 @@ const Password = () => {
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormLabel>{t('setting.password.confirm.password')}</FormLabel>
<FormControl>
<Input
placeholder="confirmPassword"
@ -143,7 +145,7 @@ const Password = () => {
)}
/>
<div className="flex justify-end">
<Button type="submit"></Button>
<Button type="submit">{t('setting.submit')}</Button>
</div>
</form>
</Form>