feat(I18n): Add multiple languages ​​(zh-CN/zh-TW/en/it) (#8)

This commit is contained in:
GuGuGu
2024-11-29 13:47:09 +01:00
committed by GitHub
parent 5850fe7fca
commit 47f092918e
44 changed files with 1138 additions and 366 deletions

View File

@@ -32,12 +32,14 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"i18next": "^24.0.2",
"jotai-zustand": "^0.6.0",
"lucide-react": "^0.454.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.1",
"react-i18next": "^15.1.2",
"react-router-dom": "^6.27.0",
"react-use-websocket": "^4.10.1",
"react-virtuoso": "^4.12.0",

View File

@@ -13,6 +13,8 @@ import {
import { KeyedMutator } from "swr";
import { buttonVariants } from "@/components/ui/button"
import { useTranslation } from "react-i18next";
interface ButtonGroupProps<E, U> {
className?: string;
children: React.ReactNode;
@@ -20,6 +22,7 @@ interface ButtonGroupProps<E, U> {
}
export function ActionButtonGroup<E, U>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<E, U>) {
const { t } = useTranslation();
const handleDelete = async () => {
await fn([id]);
await mutate();
@@ -30,18 +33,18 @@ export function ActionButtonGroup<E, U>({ className, children, delete: { fn, id,
{children}
<AlertDialog>
<AlertDialogTrigger asChild>
<IconButton variant="outline" icon="trash" />
<IconButton variant="destructive" icon="trash" />
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
<AlertDialogTitle>Confirm Deletion?</AlertDialogTitle>
<AlertDialogTitle>{t("ConfirmDeletion")}</AlertDialogTitle>
<AlertDialogDescription>
This operation is unrecoverable!
{t("Results.ThisOperationIsUnrecoverable")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })} onClick={handleDelete}>Confirm</AlertDialogAction>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })} onClick={handleDelete}>{t("Confirm")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -43,6 +43,8 @@ import { Textarea } from "./ui/textarea"
import { useNotification } from "@/hooks/useNotfication"
import { Combobox } from "./ui/combobox"
import { useTranslation } from "react-i18next";
interface AlertRuleCardProps {
data?: ModelAlertRule;
mutate: KeyedMutator<ModelAlertRule[]>;
@@ -83,6 +85,7 @@ const alertRuleFormSchema = z.object({
});
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof alertRuleFormSchema>>({
resolver: zodResolver(alertRuleFormSchema),
defaultValues: data ? {
@@ -133,7 +136,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New Alert Rule</DialogTitle>
<DialogTitle>{data ? t("EditAlertRule") : t("CreateAlertRule")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -143,7 +146,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
{...field}
@@ -158,7 +161,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="rules_raw"
render={({ field }) => (
<FormItem>
<FormLabel>Rules</FormLabel>
<FormLabel>{t("Rules")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
@@ -174,7 +177,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>Notifier Group</FormLabel>
<FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl>
<Combobox
placeholder="Search..."
@@ -192,7 +195,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="trigger_mode"
render={({ field }) => (
<FormItem>
<FormLabel>Trigger Mode</FormLabel>
<FormLabel>{t("TriggerMode")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -214,7 +217,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="fail_trigger_tasks"
render={({ field }) => (
<FormItem>
<FormLabel>Tasks to trigger on an alarm (Separate with comma)</FormLabel>
<FormLabel>{t("TasksToTriggerOnAlert") + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
@@ -235,7 +238,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
name="recover_trigger_tasks"
render={({ field }) => (
<FormItem>
<FormLabel>Tasks to trigger after recovery (Separate with comma)</FormLabel>
<FormLabel>{t("TasksToTriggerAfterRecovery") + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
@@ -262,7 +265,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable</Label>
<Label className="text-sm">{t("Enable")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -272,10 +275,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -42,6 +42,8 @@ import { useNotification } from "@/hooks/useNotfication"
import { MultiSelect } from "./xui/multi-select"
import { Combobox } from "./ui/combobox"
import { useTranslation } from "react-i18next";
interface CronCardProps {
data?: ModelCron;
mutate: KeyedMutator<ModelCron[]>;
@@ -59,6 +61,7 @@ const cronFormSchema = z.object({
});
export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof cronFormSchema>>({
resolver: zodResolver(cronFormSchema),
defaultValues: data ? data : {
@@ -109,7 +112,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New Task</DialogTitle>
<DialogTitle>{data?t("EditTask"):t("CreateTask")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -119,7 +122,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="My Task"
@@ -135,7 +138,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="task_type"
render={({ field }) => (
<FormItem>
<FormLabel>Task Type</FormLabel>
<FormLabel>{t("Type")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -157,7 +160,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="scheduler"
render={({ field }) => (
<FormItem>
<FormLabel>Cron expression</FormLabel>
<FormLabel>{t("CronExpression") }</FormLabel>
<FormControl>
<Input
placeholder="0 0 0 3 * * (At 3 AM)"
@@ -173,7 +176,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormLabel>{t("Command")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
@@ -189,7 +192,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="cover"
render={({ field }) => (
<FormItem>
<FormLabel>Coverage</FormLabel>
<FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -211,7 +214,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="servers"
render={({ field }) => (
<FormItem>
<FormLabel>Specific Servers</FormLabel>
<FormLabel>{t("SpecificServers")}</FormLabel>
<FormControl>
<MultiSelect
options={serverList}
@@ -231,7 +234,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
name="notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>Notifier Group ID</FormLabel>
<FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl>
<Combobox
placeholder="Search..."
@@ -247,10 +250,10 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -41,6 +41,8 @@ import { ddnsTypes, ddnsRequestTypes } from "@/types"
import { createDDNSProfile, updateDDNSProfile } from "@/api/ddns"
import { Textarea } from "./ui/textarea"
import { useTranslation } from "react-i18next";
interface DDNSCardProps {
data?: ModelDDNSProfile;
providers: string[];
@@ -64,6 +66,7 @@ const ddnsFormSchema = z.object({
});
export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof ddnsFormSchema>>({
resolver: zodResolver(ddnsFormSchema),
defaultValues: data ? data : {
@@ -100,7 +103,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New DDNS Profile</DialogTitle>
<DialogTitle>{data?t("EditDDNS"):t("CreateDDNS")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -110,7 +113,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="My DDNS Profile"
@@ -126,7 +129,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="provider"
render={({ field }) => (
<FormItem>
<FormLabel>Provider</FormLabel>
<FormLabel>{t("Provider")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -148,7 +151,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="domains"
render={({ field }) => (
<FormItem>
<FormLabel>Domains (separate with comma)</FormLabel>
<FormLabel>{t("Domains") + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input
placeholder="www.example.com"
@@ -169,7 +172,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="access_id"
render={({ field }) => (
<FormItem>
<FormLabel>Credential 1</FormLabel>
<FormLabel>{t("Credential")} 1</FormLabel>
<FormControl>
<Input
placeholder="Token ID"
@@ -185,7 +188,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="access_secret"
render={({ field }) => (
<FormItem>
<FormLabel>Credential 2</FormLabel>
<FormLabel>{t("Credential")} 2</FormLabel>
<FormControl>
<Input
placeholder="Token Secret"
@@ -201,7 +204,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="max_retries"
render={({ field }) => (
<FormItem>
<FormLabel>Maximum retry attempts</FormLabel>
<FormLabel>{t("MaximumRetryAttempts")}</FormLabel>
<FormControl>
<Input
type="number"
@@ -234,7 +237,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="webhook_method"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Request Method</FormLabel>
<FormLabel>Webhook {t("RequestMethod")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -256,7 +259,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="webhook_request_type"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Request Type</FormLabel>
<FormLabel>Webhook {t("RequestType")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -278,7 +281,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="webhook_headers"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Request Headers</FormLabel>
<FormLabel>Webhook {t("RequestHeader")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
@@ -295,7 +298,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
name="webhook_request_body"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook Request Body</FormLabel>
<FormLabel>Webhook {t("RequestBody")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
@@ -318,7 +321,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable IPv4</Label>
<Label className="text-sm">{t("Enable")} IPv4</Label>
</div>
</FormControl>
<FormMessage />
@@ -336,7 +339,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable IPv6</Label>
<Label className="text-sm">{t("Enable")} IPv6</Label>
</div>
</FormControl>
<FormMessage />
@@ -346,10 +349,10 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -46,6 +46,9 @@ import {
DrawerTrigger,
} from "@/components/ui/drawer"
import { useTranslation } from "react-i18next";
interface FMProps {
wsUrl: string;
}
@@ -59,6 +62,7 @@ const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
}
const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
const { t } = useTranslation();
const fmRef = useRef<HTMLDivElement>(null);
const [dOpen, setdOpen] = useState(false);
@@ -67,14 +71,14 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
const columns: ColumnDef<FMEntry>[] = [
{
id: "type",
header: () => <span>Type</span>,
header: () => <span>{t("Type")}</span>,
accessorFn: row => row.type,
cell: ({ row }) => (
row.original.type == 0 ? <File size={24} /> : <Folder size={24} />
),
},
{
header: () => <span>Name</span>,
header: () => <span>{t("Name")}</span>,
id: "name",
accessorFn: row => row.name,
cell: ({ row }) => (
@@ -85,7 +89,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
size: 5000,
},
{
header: () => <span>Action</span>,
header: () => <span>{t("Actions")}</span>,
id: "download",
cell: ({ row }) => {
return (
@@ -143,30 +147,30 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
switch (event.data.type) {
case FMWorkerOpcode.Error: {
console.error('Error from worker', event.data.error);
break;
}
case FMWorkerOpcode.Progress: {
handleReady.current = true;
break;
}
case FMWorkerOpcode.Result: {
handleReady.current = false;
case FMWorkerOpcode.Error: {
console.error('Error from worker', event.data.error);
break;
}
case FMWorkerOpcode.Progress: {
handleReady.current = true;
break;
}
case FMWorkerOpcode.Result: {
handleReady.current = false;
if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = event.data.fileName;
anchor.click();
URL.revokeObjectURL(url);
}
firstChunk.current = true;
if (dOpen) setdOpen(false);
break;
if (event.data.blob && event.data.fileName) {
const url = URL.createObjectURL(event.data.blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = event.data.fileName;
anchor.click();
URL.revokeObjectURL(url);
}
firstChunk.current = true;
if (dOpen) setdOpen(false);
break;
}
}
}
@@ -180,8 +184,8 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
},
onError: (e) => {
console.error(e);
toast("Websocket error", {
description: "View console for details.",
toast("Websocket" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
})
},
onMessage: async (e) => {
@@ -214,8 +218,8 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
} catch (error) {
console.error('Error processing received data:', error);
toast("FM error", {
description: "View console for details.",
toast("FM" + " " + t("Error"), {
description: t("Results.UnExpectedError"),
})
if (dOpen) setdOpen(false);
if (uOpen) setuOpen(false);
@@ -286,30 +290,30 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
<IconButton variant="ghost" icon="menu" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={listFile}>Refresh</DropdownMenuItem>
<DropdownMenuItem onClick={listFile}>{t('Refresh')}</DropdownMenuItem>
<DropdownMenuItem onClick={
async () => {
await navigator.clipboard.writeText(formatPath(currentPath));
}
}>Copy path</DropdownMenuItem>
}>{t("CopyPath")}</DropdownMenuItem>
<AlertDialogTrigger asChild>
<DropdownMenuItem>Goto</DropdownMenuItem>
<DropdownMenuItem>{t('Goto')}</DropdownMenuItem>
</AlertDialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Goto</AlertDialogTitle>
<AlertDialogTitle>{t('Goto')}</AlertDialogTitle>
<AlertDialogDescription />
</AlertDialogHeader>
<Input className="mb-1" placeholder="Path" value={gotoPath} onChange={(e) => { setGotoPath(e.target.value) }} />
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => { setPath(gotoPath) }}>Confirm</AlertDialogAction>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction onClick={() => { setPath(gotoPath) }}>{t("Confirm")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<h1 className="text-base">Pseudo File Manager</h1>
<h1 className="text-base">{t("FileManager")}</h1>
<div className="ml-auto">
<input ref={fileInputRef} type="file" className="hidden" onChange={
async (e) => {
@@ -331,7 +335,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
<AlertDialog open={dOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Downloading...</AlertDialogTitle>
<AlertDialogTitle>{t("Downloading")}...</AlertDialogTitle>
<AlertDialogDescription />
</AlertDialogHeader>
</AlertDialogContent>
@@ -339,7 +343,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
<AlertDialog open={uOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Uploading...</AlertDialogTitle>
<AlertDialogTitle>{t("Uploading")}...</AlertDialogTitle>
<AlertDialogDescription />
</AlertDialogHeader>
</AlertDialogContent>
@@ -350,6 +354,7 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
}
export const FMCard = ({ id }: { id?: string }) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [fm, setFM] = useState<ModelCreateFMResponse | null>(null);
const [init, setInit] = useState(false);
@@ -363,8 +368,8 @@ export const FMCard = ({ id }: { id?: string }) => {
const createdFM = await createFM(id);
setFM(createdFM);
} catch (e) {
toast("FM API Error", {
description: "View console for details.",
toast(t("Error"), {
description: t("Results.UnExpectedError"),
})
console.error("fetch error", e);
return;
@@ -396,7 +401,7 @@ export const FMCard = ({ id }: { id?: string }) => {
?
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
:
<p>The server does not exist, or have not been connected yet.</p>
<p>{t("Results.TheServerDoesNotOnline")}</p>
}
</div>
</SheetContent>
@@ -417,7 +422,7 @@ export const FMCard = ({ id }: { id?: string }) => {
?
<FMComponent className="p-1 space-y-5" wsUrl={`/api/v1/ws/file/${fm.session_id}`} />
:
<p>The server does not exist, or have not been connected yet.</p>
<p>{t("Results.TheServerDoesNotOnline")}</p>
}
</div>
</DrawerContent>

View File

@@ -5,17 +5,20 @@ import {
} from "@/components/ui/tabs"
import { Link, useLocation } from "react-router-dom"
import { useTranslation } from "react-i18next";
export const GroupTab = ({ className }: { className?: string }) => {
const { t } = useTranslation();
const location = useLocation();
return (
<Tabs defaultValue={location.pathname} className={className}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="/dashboard/server-group" asChild>
<Link to="/dashboard/server-group">Server</Link>
<Link to="/dashboard/server-group">{t("Server")}</Link>
</TabsTrigger>
<TabsTrigger value="/dashboard/notification-group" asChild>
<Link to="/dashboard/notification-group">Notification</Link>
<Link to="/dashboard/notification-group">{t("Notification")}</Link>
</TabsTrigger>
</TabsList>
</Tabs>

View File

@@ -14,6 +14,8 @@ import {
import { KeyedMutator } from "swr";
import { toast } from "sonner"
import { useTranslation } from "react-i18next";
interface ButtonGroupProps<E, U> {
className?: string;
children?: React.ReactNode;
@@ -26,13 +28,15 @@ export function HeaderButtonGroup<E, U>({ className, children, delete: { fn, id,
await mutate();
}
const { t } = useTranslation();
return (
<div className={className}>
{id.length < 1 ? (
<>
<IconButton variant="destructive" icon="trash" onClick={() => {
toast("Error", {
description: "No rows are selected."
toast(t("Error"), {
description: t("Results.NoRowsAreSelected")
});
}} />
{children}
@@ -45,14 +49,14 @@ export function HeaderButtonGroup<E, U>({ className, children, delete: { fn, id,
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader>
<AlertDialogTitle>Confirm Deletion?</AlertDialogTitle>
<AlertDialogTitle>{t("ConfirmDeletion")}</AlertDialogTitle>
<AlertDialogDescription>
This operation is unrecoverable!
{t("Results.ThisOperationIsUnrecoverable")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })} onClick={handleDelete}>Confirm</AlertDialogAction>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })} onClick={handleDelete}>{t("Confirm")}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@@ -29,17 +29,21 @@ import { Button } from "./ui/button";
import { IconButton } from "./xui/icon-button";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import i18next from "i18next";
const pages = [
{ href: "/dashboard", label: "Server" },
{ href: "/dashboard/service", label: "Service" },
{ href: "/dashboard/cron", label: "Task" },
{ href: "/dashboard/notification", label: "Notification" },
{ href: "/dashboard/ddns", label: "Dynamic DNS" },
{ href: "/dashboard/nat", label: "NAT Traversal" },
{ href: "/dashboard/server-group", label: "Group" },
{ href: "/dashboard", label: i18next.t("Server") },
{ href: "/dashboard/service", label: i18next.t("Service") },
{ href: "/dashboard/cron", label: i18next.t("Task") },
{ href: "/dashboard/notification", label: i18next.t("Notification") },
{ href: "/dashboard/ddns", label: i18next.t("DDNS") },
{ href: "/dashboard/nat", label: i18next.t("NATT") },
{ href: "/dashboard/server-group", label: i18next.t("Group") },
]
export default function Header() {
const { t } = useTranslation();
const { logout } = useAuth();
const profile = useMainStore(store => store.profile);
@@ -56,7 +60,7 @@ export default function Header() {
<NavigationMenuList>
<Card className="mr-1">
<NavigationMenuLink asChild className={navigationMenuTriggerStyle() + ' !text-foreground'}>
<Link to={profile ? "/dashboard" : '#'}><img className="h-7 mr-1" src='/dashboard/logo.svg' /> NEZHA</Link>
<Link to={profile ? "/dashboard" : '#'}><img className="h-7 mr-1" src='/dashboard/logo.svg' /> {t("nezha")}</Link>
</NavigationMenuLink>
</Card>
@@ -65,37 +69,37 @@ export default function Header() {
<>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard">Server</Link>
<Link to="/dashboard">{t("Server")}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard/service">Service</Link>
<Link to="/dashboard/service">{t("Service")}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard/cron">Task</Link>
<Link to="/dashboard/cron">{t('Task')}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard/notification">Notification</Link>
<Link to="/dashboard/notification">{t('Notification')}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard/ddns">Dynamic DNS</Link>
<Link to="/dashboard/ddns">{t('DDNS')}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard/nat">NAT Traversal</Link>
<Link to="/dashboard/nat">{t('NATT')}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}>
<Link to="/dashboard/server-group">Group</Link>
<Link to="/dashboard/server-group">{t('Group')}</Link>
</NzNavigationMenuLink>
</NavigationMenuItem>
</>
@@ -120,14 +124,14 @@ export default function Header() {
<DropdownMenuItem onClick={() => { setDropdownOpen(false) }}>
<Link to="/dashboard/profile" className="flex items-center gap-2 w-full">
<User2 />
Profile
{t('Profile')}
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setDropdownOpen(false) }}>
<Link to="/dashboard/settings" className="flex items-center gap-2 w-full">
<Settings />
Settings
{t('Settings')}
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
@@ -135,7 +139,7 @@ export default function Header() {
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-pointer">
<LogOut />
<span>Log out</span>
{t('Logout')}
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
@@ -156,8 +160,8 @@ export default function Header() {
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Navigate to</DrawerTitle>
<DrawerDescription>Select a page to navigate to.</DrawerDescription>
<DrawerTitle>{t('NavigateTo')}</DrawerTitle>
<DrawerDescription>{t('SelectAPageToNavigateTo')}</DrawerDescription>
</DrawerHeader>
<div className="grid gap-1 px-4">
{pages.slice(0).map((item, index) => (
@@ -173,7 +177,7 @@ export default function Header() {
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
<Button variant="outline">{t('Close')}</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
@@ -201,14 +205,14 @@ export default function Header() {
<DropdownMenuItem onClick={() => { setDropdownOpen(false) }}>
<Link to="/dashboard/profile" className="flex items-center gap-2 w-full">
<User2 />
Profile
{t('Profile')}
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setDropdownOpen(false) }}>
<Link to="/dashboard/settings" className="flex items-center gap-2 w-full">
<Settings />
Settings
{t('Settings')}
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</Link>
</DropdownMenuItem>
@@ -216,7 +220,7 @@ export default function Header() {
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-pointer">
<LogOut />
<span>Log out</span>
{t('Logout')}
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -9,7 +9,10 @@ import {
} from "@/components/ui/dropdown-menu"
import { Theme, useTheme } from "@/components/theme-provider"
import { useTranslation } from "react-i18next";
export function ModeToggle() {
const { t } = useTranslation();
const { setTheme } = useTheme()
const toggleTheme = (theme: Theme) => {
@@ -27,13 +30,13 @@ export function ModeToggle() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => toggleTheme("light")}>
Light
{t("theme.light")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleTheme("dark")}>
Dark
{t("theme.dark")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toggleTheme("system")}>
System
{t("theme.system")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -28,6 +28,8 @@ import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button"
import { createNAT, updateNAT } from "@/api/nat"
import { useTranslation } from "react-i18next";
interface NATCardProps {
data?: ModelNAT;
mutate: KeyedMutator<ModelNAT[]>;
@@ -41,6 +43,7 @@ const natFormSchema = z.object({
});
export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof natFormSchema>>({
resolver: zodResolver(natFormSchema),
defaultValues: data ? data : {
@@ -77,7 +80,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New NAT Profile</DialogTitle>
<DialogTitle>{data?t("EditNAT"):t("CreateNAT")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -87,7 +90,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="My NAT Profile"
@@ -103,7 +106,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
name="server_id"
render={({ field }) => (
<FormItem>
<FormLabel>Server ID</FormLabel>
<FormLabel>{t("Server")} ID</FormLabel>
<FormControl>
<Input
type="number"
@@ -120,7 +123,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Local Service</FormLabel>
<FormLabel>{t("LocalService")}</FormLabel>
<FormControl>
<Input
placeholder="192.168.1.1:80 (with port)"
@@ -136,7 +139,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
name="domain"
render={({ field }) => (
<FormItem>
<FormLabel>Bind hostname</FormLabel>
<FormLabel>{t("BindHostname")}</FormLabel>
<FormControl>
<Input
placeholder="router.app.yourdomain.com"
@@ -150,10 +153,10 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -9,11 +9,14 @@ import { forwardRef, useState } from "react"
import { IconButton } from "./xui/icon-button"
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
interface NoteMenuProps extends ButtonProps {
note: { private?: string, public?: string };
}
export const NoteMenu = forwardRef<HTMLButtonElement, NoteMenuProps>((props, ref) => {
const { t } = useTranslation();
const [copy, setCopy] = useState(false);
const switchState = async (text?: string) => {
@@ -41,8 +44,8 @@ export const NoteMenu = forwardRef<HTMLButtonElement, NoteMenuProps>((props, ref
} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => { switchState(props.note.private) }}>Private</DropdownMenuItem>
<DropdownMenuItem onClick={() => { switchState(props.note.public) }}>Public</DropdownMenuItem>
<DropdownMenuItem onClick={() => { switchState(props.note.private) }}>{t("Private")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => { switchState(props.note.public) }}>{t("Public")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);

View File

@@ -30,6 +30,8 @@ import { createNotificationGroup, updateNotificationGroup } from "@/api/notifica
import { MultiSelect } from "@/components/xui/multi-select"
import { useNotification } from "@/hooks/useNotfication"
import { useTranslation } from "react-i18next";
interface NotificationGroupCardProps {
data?: ModelNotificationGroupResponseItem;
mutate: KeyedMutator<ModelNotificationGroupResponseItem[]>;
@@ -41,6 +43,7 @@ const notificationGroupFormSchema = z.object({
});
export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof notificationGroupFormSchema>>({
resolver: zodResolver(notificationGroupFormSchema),
defaultValues: data ? {
@@ -84,7 +87,7 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New Notifier Group</DialogTitle>
<DialogTitle>{data ? t("EditNotifierGroup") : t("CreateNotifierGroup")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -94,7 +97,7 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="Group Name"
@@ -110,7 +113,7 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
name="notifications"
render={({ field }) => (
<FormItem>
<FormLabel>Notifiers</FormLabel>
<FormLabel>{t("Notification")}</FormLabel>
<MultiSelect
options={notifierList}
onValueChange={e => {
@@ -126,10 +129,10 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -5,17 +5,21 @@ import {
} from "@/components/ui/tabs"
import { Link, useLocation } from "react-router-dom"
import { useTranslation } from "react-i18next";
export const NotificationTab = ({ className }: { className?: string }) => {
const { t } = useTranslation();
const location = useLocation();
return (
<Tabs defaultValue={location.pathname} className={className}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="/dashboard/notification" asChild>
<Link to="/dashboard/notification">Notifier</Link>
<Link to="/dashboard/notification">{t("Notifier")}</Link>
</TabsTrigger>
<TabsTrigger value="/dashboard/alert-rule" asChild>
<Link to="/dashboard/alert-rule">Alert Rule</Link>
<Link to="/dashboard/alert-rule">{t("AlertRule")}</Link>
</TabsTrigger>
</TabsList>
</Tabs>

View File

@@ -40,6 +40,8 @@ import { nrequestTypes, nrequestMethods } from "@/types"
import { createNotification, updateNotification } from "@/api/notification"
import { Textarea } from "./ui/textarea"
import { useTranslation } from "react-i18next";
interface NotifierCardProps {
data?: ModelNotification;
mutate: KeyedMutator<ModelNotification[]>;
@@ -57,6 +59,7 @@ const notificationFormSchema = z.object({
});
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof notificationFormSchema>>({
resolver: zodResolver(notificationFormSchema),
defaultValues: data ? data : {
@@ -95,7 +98,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New Notifier</DialogTitle>
<DialogTitle>{data?t("EditNotifier"):t("CreateNotifier")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -105,7 +108,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="My Notifier"
@@ -136,7 +139,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
name="request_method"
render={({ field }) => (
<FormItem>
<FormLabel>Request Method</FormLabel>
<FormLabel>{t("RequestMethod")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -158,7 +161,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
name="request_type"
render={({ field }) => (
<FormItem>
<FormLabel>Request Type</FormLabel>
<FormLabel>{t("Type")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -180,7 +183,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
name="request_header"
render={({ field }) => (
<FormItem>
<FormLabel>Header</FormLabel>
<FormLabel>{t("RequestHeader")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
@@ -197,7 +200,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
name="request_body"
render={({ field }) => (
<FormItem>
<FormLabel>Body</FormLabel>
<FormLabel>{t("RequestBody")}</FormLabel>
<FormControl>
<Textarea
className="resize-y h-[240px]"
@@ -220,7 +223,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Verify TLS</Label>
<Label className="text-sm">{t("VerifyTLS")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -238,7 +241,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Do Not Send Test Message</Label>
<Label className="text-sm">{t("DoNotSendTestMessage")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -248,10 +251,10 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -27,12 +27,15 @@ import { useState } from "react"
import { useMainStore } from "@/hooks/useMainStore"
import { toast } from "sonner"
import { useTranslation } from "react-i18next";
const profileFormSchema = z.object({
original_password: z.string().min(5).max(72),
new_password: z.string().min(8).max(72),
});
export const ProfileCard = ({ className }: { className: string }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof profileFormSchema>>({
resolver: zodResolver(profileFormSchema),
defaultValues: {
@@ -51,7 +54,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
try {
await updateProfile(values);
} catch (e) {
toast("Update failed", {
toast(t("Error"), {
description: `${e}`,
})
return;
@@ -66,14 +69,14 @@ export const ProfileCard = ({ className }: { className: string }) => {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" className={className}>
Update Password
{t("UpdatePassword")}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl">
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>Update Server</DialogTitle>
<DialogTitle>{t("UpdatePassword")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -83,7 +86,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
name="original_password"
render={({ field }) => (
<FormItem>
<FormLabel>Original Password</FormLabel>
<FormLabel>{t("OriginalPassword")}</FormLabel>
<FormControl>
<Input
{...field}
@@ -98,7 +101,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
name="new_password"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormLabel>{t("NewPassword")}</FormLabel>
<FormControl>
<Input
{...field}
@@ -112,10 +115,10 @@ export const ProfileCard = ({ className }: { className: string }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -30,6 +30,8 @@ import { createServerGroup, updateServerGroup } from "@/api/server-group"
import { MultiSelect } from "@/components/xui/multi-select"
import { useServer } from "@/hooks/useServer"
import { useTranslation } from "react-i18next";
interface ServerGroupCardProps {
data?: ModelServerGroupResponseItem;
mutate: KeyedMutator<ModelServerGroupResponseItem[]>;
@@ -41,6 +43,7 @@ const serverGroupFormSchema = z.object({
});
export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof serverGroupFormSchema>>({
resolver: zodResolver(serverGroupFormSchema),
defaultValues: data ? {
@@ -84,7 +87,7 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New Server Group</DialogTitle>
<DialogTitle>{data? t("EditServerGroup"):t("CreateServerGroup")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -94,7 +97,7 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="Group Name"
@@ -110,7 +113,7 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
name="servers"
render={({ field }) => (
<FormItem>
<FormLabel>Servers</FormLabel>
<FormLabel>{t("Server")}</FormLabel>
<FormControl>
<MultiSelect
options={serverList}
@@ -128,10 +131,10 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -32,6 +32,7 @@ import { KeyedMutator } from "swr"
import { asOptionalField } from "@/lib/utils"
import { IconButton } from "@/components/xui/icon-button"
import { Textarea } from "@/components/ui/textarea"
import { useTranslation } from "react-i18next"
interface ServerCardProps {
data: ModelServer;
@@ -49,6 +50,7 @@ const serverFormSchema = z.object({
});
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof serverFormSchema>>({
resolver: zodResolver(serverFormSchema),
defaultValues: data,
@@ -75,7 +77,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>Update Server</DialogTitle>
<DialogTitle>{t("EditServer") }</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -85,7 +87,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="My Server"
@@ -101,7 +103,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
name="display_index"
render={({ field }) => (
<FormItem>
<FormLabel>Display Index</FormLabel>
<FormLabel>{t("Weight")}</FormLabel>
<FormControl>
<Input
type="number"
@@ -118,7 +120,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
name="ddns_profiles"
render={({ field }) => (
<FormItem>
<FormLabel>DDNS Profile IDs (Separate with comma)</FormLabel>
<FormLabel>{t("DDNSProfiles") + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
@@ -146,7 +148,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable DDNS</Label>
<Label className="text-sm">{t("Enable") + t("DDNS") }</Label>
</div>
</FormControl>
<FormMessage />
@@ -164,7 +166,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Hide from Guest</Label>
<Label className="text-sm">{t("HideForGuest")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -176,7 +178,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
name="note"
render={({ field }) => (
<FormItem>
<FormLabel>Note</FormLabel>
<FormLabel>{t("Private") + t("Note")}</FormLabel>
<FormControl>
<Textarea
className="resize-none"
@@ -192,7 +194,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
name="public_note"
render={({ field }) => (
<FormItem>
<FormLabel>Public Note</FormLabel>
<FormLabel>{t("Public") + t("Note")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
@@ -206,10 +208,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Submit")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -44,6 +44,8 @@ import { Combobox } from "./ui/combobox"
import { useServer } from "@/hooks/useServer"
import { useNotification } from "@/hooks/useNotfication"
import { useTranslation } from "react-i18next";
interface ServiceCardProps {
data?: ModelService;
mutate: KeyedMutator<ModelServiceResponse>;
@@ -69,6 +71,7 @@ const serviceFormSchema = z.object({
});
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof serviceFormSchema>>({
resolver: zodResolver(serviceFormSchema),
defaultValues: data ? {
@@ -130,7 +133,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New Service</DialogTitle>
<DialogTitle>{data?t("EditService"):t("CreateService")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -140,7 +143,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Service Name</FormLabel>
<FormLabel>{t("Name")}</FormLabel>
<FormControl>
<Input
placeholder="My Service Monitor"
@@ -156,7 +159,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="target"
render={({ field }) => (
<FormItem>
<FormLabel>Target</FormLabel>
<FormLabel>{t("Target")}</FormLabel>
<FormControl>
<Input
placeholder="HTTP (https://t.tt)Ping (t.tt)TCP (t.tt:80)"
@@ -172,7 +175,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<FormLabel>{t("Type")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -200,7 +203,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Show in Service</Label>
<Label className="text-sm">{t("ShowInService")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -212,7 +215,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="duration"
render={({ field }) => (
<FormItem>
<FormLabel>Interval (s)</FormLabel>
<FormLabel>{t("Interval")} (s)</FormLabel>
<FormControl>
<Input
type="number"
@@ -229,7 +232,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="cover"
render={({ field }) => (
<FormItem>
<FormLabel>Coverage</FormLabel>
<FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -251,7 +254,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="skip_servers_raw"
render={({ field }) => (
<FormItem>
<FormLabel>Specific Servers</FormLabel>
<FormLabel>{t("SpecificServers")}</FormLabel>
<FormControl>
<MultiSelect
options={serverList}
@@ -268,7 +271,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="notification_group_id"
render={({ field }) => (
<FormItem>
<FormLabel>Notifier Group</FormLabel>
<FormLabel>{t("NotifierGroupID")}</FormLabel>
<FormControl>
<Combobox
placeholder="Search..."
@@ -292,7 +295,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable Failure Notification</Label>
<Label className="text-sm">{t("EnableFailureNotification")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -304,7 +307,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="max_latency"
render={({ field }) => (
<FormItem>
<FormLabel>Maximum Latency Time (ms)</FormLabel>
<FormLabel>{t("MaximumLatency")}</FormLabel>
<FormControl>
<Input
type="number"
@@ -321,7 +324,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="min_latency"
render={({ field }) => (
<FormItem>
<FormLabel>Minimum Latency Time (ms)</FormLabel>
<FormLabel>{t("MinimumLatency")}</FormLabel>
<FormControl>
<Input
type="number"
@@ -344,7 +347,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable Latency Notification</Label>
<Label className="text-sm">{t("EnableLatencyNotification")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -362,7 +365,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable Trigger Task</Label>
<Label className="text-sm">{t("EnableTriggerTask")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -374,7 +377,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="fail_trigger_tasks"
render={({ field }) => (
<FormItem>
<FormLabel>Tasks to trigger on an alarm (Separate with comma)</FormLabel>
<FormLabel>{t("TasksToTriggerOnAlert") + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
@@ -395,7 +398,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
name="recover_trigger_tasks"
render={({ field }) => (
<FormItem>
<FormLabel>Tasks to trigger after recovery (Separate with comma)</FormLabel>
<FormLabel>{t("TasksToTriggerAfterRecovery") + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
@@ -414,10 +417,10 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Submit")}</Button>
</DialogFooter>
</form>
</Form>

View File

@@ -5,20 +5,23 @@ import {
} from "@/components/ui/tabs"
import { Link, useLocation } from "react-router-dom"
import { useTranslation } from "react-i18next";
export const SettingsTab = ({ className }: { className?: string }) => {
const { t } = useTranslation();
const location = useLocation();
return (
<Tabs defaultValue={location.pathname} className={className}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="/dashboard/settings" asChild>
<Link to="/dashboard/settings">Config</Link>
<Link to="/dashboard/settings">{t("Settings")}</Link>
</TabsTrigger>
<TabsTrigger value="/dashboard/settings/user" asChild>
<Link to="/dashboard/settings/user">User</Link>
<Link to="/dashboard/settings/user">{t("User")}</Link>
</TabsTrigger>
<TabsTrigger value="/dashboard/settings/waf" asChild>
<Link to="/dashboard/settings/waf">WAF</Link>
<Link to="/dashboard/settings/waf">{t("WAF")}</Link>
</TabsTrigger>
</TabsList>
</Tabs>

View File

@@ -28,6 +28,8 @@ import { KeyedMutator } from "swr"
import { IconButton } from "@/components/xui/icon-button"
import { createUser } from "@/api/user"
import { useTranslation } from "react-i18next";
interface UserCardProps {
mutate: KeyedMutator<ModelUser[]>;
}
@@ -38,6 +40,7 @@ const userFormSchema = z.object({
});
export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
const { t } = useTranslation();
const form = useForm<z.infer<typeof userFormSchema>>({
resolver: zodResolver(userFormSchema),
defaultValues: {
@@ -67,7 +70,7 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1">
<DialogHeader>
<DialogTitle>New User</DialogTitle>
<DialogTitle>{t("NewUser")}</DialogTitle>
<DialogDescription />
</DialogHeader>
<Form {...form}>
@@ -77,7 +80,7 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormLabel>{t("Username")}</FormLabel>
<FormControl>
<Input
{...field}
@@ -92,7 +95,7 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("Password")}</FormLabel>
<FormControl>
<Input
{...field}
@@ -105,10 +108,10 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
<DialogFooter className="justify-end">
<DialogClose asChild>
<Button type="button" className="my-2" variant="secondary">
Close
{t("Close")}
</Button>
</DialogClose>
<Button type="submit" className="my-2">Submit</Button>
<Button type="submit" className="my-2">{t("Confirm")}</Button>
</DialogFooter>
</form>
</Form>

42
src/lib/i18n.ts Normal file
View File

@@ -0,0 +1,42 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import enTranslation from "../locales/en/translation.json";
import itTranslation from "../locales/it/translation.json";
import zhCNTranslation from "../locales/zh-CN/translation.json";
import zhTWTranslation from "../locales/zh-TW/translation.json";
const resources = {
en: {
translation: enTranslation,
},
it: {
translation: itTranslation,
},
"zh-CN": {
translation: zhCNTranslation,
},
"zh-TW": {
translation: zhTWTranslation,
},
};
const getStoredLanguage = () => {
return localStorage.getItem("language") || "zh-CN";
};
i18n.use(initReactI18next).init({
resources,
lng: getStoredLanguage(), // 使用localStorage中存储的语言或默认值
fallbackLng: "en", // 当前语言的翻译没有找到时,使用的备选语言
interpolation: {
escapeValue: false, // react已经安全地转义
},
});
// 添加语言改变时的处理函数
i18n.on("languageChanged", (lng) => {
localStorage.setItem("language", lng);
});
export default i18n;

View File

@@ -0,0 +1,152 @@
{
"nezha": "Nezha Monitoring",
"theme": {
"light": "Light",
"dark": "Dark",
"system": "Follow System"
},
"Username": "Username",
"Password": "Password",
"Results": {
"UsernameMin": "Username must be at least {{number}} characters.",
"PasswordRequired": "Password cannot be empty.",
"ErrorFetchingResource": "Error Fetching Resource : {{error}}",
"SelectAtLeastOneServer": "Please select at least one server.",
"UnExpectedError": "UnExpected Error, Please see the console for details.",
"ForceUpdate": "Forced upgrade:",
"NoRowsAreSelected": "No rows are selected",
"ThisOperationIsUnrecoverable": "This operation cannot be undone!",
"TaskTriggeredSuccessfully": "The task triggered successfully",
"TheServerDoesNotOnline": "The server does not exist or has not been connected yet"
},
"Login": "Log in",
"Server": "Server",
"Service": "Serve",
"Task": "Task",
"Notification": "Notification",
"DDNS": "Dynamic DNS",
"NATT": "NAT Traversal",
"Group": "Group",
"Profile": "personal information",
"Settings": "System settings",
"Logout": "Log out",
"NavigateTo": "Navigate to",
"SelectAPageToNavigateTo": "Choose a page to jump to",
"Close": "Close",
"Error": "Error",
"Name": "Name",
"Version": "Version",
"Unknown": "unknown",
"Enable": "Enable",
"HideForGuest": "Hidden from visitors",
"InstallCommands": "Installation command",
"Note": "Note",
"Success": "Success",
"Done": "Finish",
"Offline": "Offline",
"Failure": "Fail",
"Loading": "loading",
"NoResults": "no content",
"Actions": "Actions",
"EditServer": "Edit server",
"Weight": "Weight (the larger the number, the higher it is displayed)",
"DDNSProfiles": "DDNS Profile IDs",
"SeparateWithComma": "(Separate with comma)",
"Public": "Public",
"Private": "private",
"Submit": "submit",
"Target": "Target",
"Coverage": "Coverage",
"CoverAll": "Cover all",
"IgnoreAll": "Ignore all",
"SpecificServers": "Specific server",
"Type": "Type",
"Interval": "interval",
"NotifierGroupID": "Notification group ID",
"Trigger": "On Trigger",
"TasksToTriggerOnAlert": "The task that triggered the alert",
"TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery",
"Confirm": "Confirm",
"ConfirmDeletion": "Confirm deletion?",
"Services": "Services",
"ShowInService": "Show in Service",
"Coverages": {
"Excludes": "Excludes specific servers",
"Only": "Only specific servers",
"Alarmed": "Executed on the server that triggered the alarm"
},
"EnableFailureNotification": "Enable Failure Notification",
"MaximumLatency": "Maximum Latency Time (ms)",
"MinimumLatency": "Minimum delay time (milliseconds)",
"EnableLatencyNotification": "Enable delayed notifications",
"EnableTriggerTask": "Enable Trigger Task",
"CronExpression": "Cron expression",
"Command": "Order",
"NotifierGroup": "Notification group",
"SendSuccessNotification": "Send success notification",
"LastExecution": "Last Execution",
"Result": "Result",
"Scheduled": "Scheduled tasks",
"Notifier": "Notifier",
"AlertRule": "Alert rules",
"VerifyTLS": "Verify TLS",
"TriggerMode": "Trigger mode",
"Rules": "Rules",
"RequestMethod": "Request method",
"RequestHeader": "Request header",
"DoNotSendTestMessage": "Do Not Send Test Message",
"Always": "Always",
"Once": "Once",
"Provider": "Provider",
"Domains": "domain name",
"MaximumRetryAttempts": "Maximum number of retries",
"Refresh": "Refresh",
"CopyPath": "copy path",
"Goto": "Go to",
"UpdatePassword": "change password",
"OriginalPassword": "original password",
"NewPassword": "New Password",
"EditDDNS": "Edit DDNS",
"CreateDDNS": "Create DDNS",
"Credential": "Credential",
"RequestType": "Request type",
"RequestBody": "Request body",
"FileManager": "File manager",
"Downloading": "Downloading",
"Uploading": "Uploading",
"EditNAT": "Edit intranet penetration",
"CreateNAT": "Create intranet penetration",
"LocalService": "local service",
"BindHostname": "Bind domain name",
"EditServerGroup": "Edit server group",
"CreateServerGroup": "Create server group",
"User": "User",
"WAF": "Web application firewall",
"SiteName": "Site name",
"DashboardOriginalHost": "Dashboard Server Domain/IP without CDN",
"Auto": "automatic recognition",
"LoginFailed": "Login failed",
"BruteForceAttackingToken": "Brute Force Attacking Token",
"BruteForceAttackingAgentSecret": "Brute Force Attacking Agent Secret",
"Language": "language",
"CustomCodes": "Custom Codes (Style and Script)",
"CustomCodesDashboard": "Custom Codes for Dashboard",
"CustomPublicDNSNameserversforDDNS": "Custom Public DNS Nameservers for DDNS",
"RealIPHeader": "Real IP request header",
"IPChangeNotification": "IP Change notification",
"FullIPNotification": "Show Full IP Address in Notification Messages",
"EditService": "Editing services",
"CreateService": "Create service",
"EditTask": "Edit task",
"CreateTask": "Create task",
"CreateNotifier": "Create notification",
"EditNotifier": "Edit notification",
"EditAlertRule": "Edit alarm rules",
"CreateAlertRule": "Create alert rules",
"EditNotifierGroup": "Edit notification group",
"CreateNotifierGroup": "Create notification group",
"NewUser": "new user",
"Count": "Count",
"LastBlockReason": "Last Block Reason",
"LastBlockTime": "Last ban time"
}

View File

@@ -0,0 +1,152 @@
{
"nezha": "Monitoraggio Nezha",
"theme": {
"light": "Chiaro",
"dark": "Scuro",
"system": "Segui il sistema"
},
"Username": "Nome utente",
"Password": "Password",
"Results": {
"UsernameMin": "Il nome utente deve contenere almeno {{number}} caratteri.",
"PasswordRequired": "La password non può essere vuota.",
"ErrorFetchingResource": "Errore nel recupero della risorsa: {{error}}",
"SelectAtLeastOneServer": "Seleziona almeno un server.",
"UnExpectedError": "Errore imprevisto. Controlla la console per i dettagli.",
"ForceUpdate": "Aggiornamento forzato:",
"NoRowsAreSelected": "Nessuna riga selezionata",
"ThisOperationIsUnrecoverable": "Questa operazione non può essere annullata!",
"TaskTriggeredSuccessfully": "Attività avviata correttamente",
"TheServerDoesNotOnline": "Il server non esiste o non è stato ancora connesso"
},
"Login": "Accedi",
"Server": "Server",
"Service": "Servizio",
"Task": "Compito",
"Notification": "Notifica",
"DDNS": "DNS Dinamico",
"NATT": "Traversata NAT",
"Group": "Gruppo",
"Profile": "Informazioni personali",
"Settings": "Impostazioni di sistema",
"Logout": "Esci",
"NavigateTo": "Vai a",
"SelectAPageToNavigateTo": "Scegli una pagina da visitare",
"Close": "Chiudi",
"Error": "Errore",
"Name": "Nome",
"Version": "Versione",
"Unknown": "Sconosciuto",
"Enable": "Abilita",
"HideForGuest": "Nascosto ai visitatori",
"InstallCommands": "Comando di installazione",
"Note": "Osservazione",
"Success": "Successo",
"Done": "Fine",
"Offline": "Non in linea",
"Failure": "Fallire",
"Loading": "caricamento",
"NoResults": "nessun contenuto",
"Actions": "Azione",
"EditServer": "Modifica server",
"Weight": "Peso (più grande è il numero, più alto sarà visualizzato)",
"DDNSProfiles": "ID profilo DDNS",
"SeparateWithComma": "(separati da virgole)",
"Public": "Pubblico",
"Private": "privato",
"Submit": "invia",
"Target": "Bersaglio",
"Coverage": "Copertura",
"CoverAll": "Copri tutto",
"IgnoreAll": "Ignorare tutto",
"SpecificServers": "Server specifico",
"Type": "Tipo",
"Interval": "intervallo",
"NotifierGroupID": "ID del gruppo di notifiche",
"Trigger": "Grilletto",
"TasksToTriggerOnAlert": "L'attività che ha attivato l'avviso",
"TasksToTriggerAfterRecovery": "Attività da attivare dopo il ripristino",
"Confirm": "Confermo",
"ConfirmDeletion": "Confermi l'eliminazione?",
"NewService": "Nuovo servizio",
"Services": "Servizi",
"ShowInService": "Mostra in servizio",
"Coverages": {
"Only": "Solo server specifici",
"Excludes": "Escludi server specifici",
"Alarmed": "Eseguito sul server che ha attivato l'allarme"
},
"EnableFailureNotification": "Abilita la notifica di errore",
"MaximumLatency": "Tempo di ritardo massimo (millisecondi)",
"MinimumLatency": "Tempo di ritardo minimo (millisecondi)",
"EnableLatencyNotification": "Abilita le notifiche ritardate",
"EnableTriggerTask": "Abilita attività di attivazione",
"CronExpression": "Espressione cron",
"Command": "Ordine",
"NotifierGroup": "gruppo di notifica",
"SendSuccessNotification": "Invia notifica di successo",
"LastExecution": "Ultimo giustiziato",
"Result": "Risultato",
"Scheduled": "Attività pianificate",
"Notifier": "Notifica",
"AlertRule": "Regole di allerta",
"VerifyTLS": "Verifica TLS",
"TriggerMode": "Modalità di attivazione",
"Rules": "Regola",
"RequestMethod": "Metodo di richiesta",
"RequestHeader": "Intestazione della richiesta",
"DoNotSendTestMessage": "Non inviare messaggi di prova",
"Always": "Sempre",
"Once": "solo una volta",
"Provider": "fornitore",
"Domains": "nome di dominio",
"MaximumRetryAttempts": "Numero massimo di tentativi",
"Refresh": "aggiornare",
"CopyPath": "percorso di copia",
"Goto": "Vai a",
"UpdatePassword": "cambiare la password",
"OriginalPassword": "password originale",
"NewPassword": "Nuova parola d'ordine",
"EditDDNS": "Modifica DDNS",
"CreateDDNS": "Crea DDNS",
"Credential": "Credenziale",
"RequestType": "Tipo di richiesta",
"RequestBody": "Richiedi corpo",
"FileManager": "Gestore di file",
"Downloading": "Download in corso",
"Uploading": "Caricamento",
"EditNAT": "Modifica la penetrazione della intranet",
"CreateNAT": "Creare penetrazione intranet",
"LocalService": "servizio locale",
"BindHostname": "Associa il nome di dominio",
"EditServerGroup": "Modifica gruppo di server",
"CreateServerGroup": "Crea gruppo di server",
"EditService": "Servizi di editing",
"CreateService": "Crea servizio",
"EditTask": "Modifica attività",
"CreateTask": "Crea attività",
"CreateNotifier": "Crea notifica",
"EditNotifier": "Modifica notifica",
"EditAlertRule": "Modifica le regole degli allarmi",
"CreateAlertRule": "Crea regole di avviso",
"EditNotifierGroup": "Modifica gruppo di notifiche",
"CreateNotifierGroup": "Crea gruppo di notifica",
"User": "Utente",
"WAF": "Firewall dell'applicazione Web",
"SiteName": "Nome del sito",
"Language": "lingua",
"CustomCodes": "Codice personalizzato (stili e script)",
"CustomCodesDashboard": "Codice personalizzato per dashboard",
"DashboardOriginalHost": "Nome di dominio/IP del server Dashboard (no CDN)",
"CustomPublicDNSNameserversforDDNS": "Server dei nomi DNS pubblici personalizzati per DDNS",
"RealIPHeader": "Intestazione della richiesta IP reale",
"IPChangeNotification": "Notifica di modifica IP",
"FullIPNotification": "Mostra l'indirizzo IP completo nei messaggi di notifica",
"LoginFailed": "Accesso non riuscito",
"BruteForceAttackingToken": "Segnalino di attacco di forza bruta",
"BruteForceAttackingAgentSecret": "Segreti proxy dell'attacco di forza bruta",
"NewUser": "nuovo utente",
"Count": "contare",
"LastBlockReason": "Motivo dell'ultimo divieto",
"LastBlockTime": "L'ultima volta che è stato vietato"
}

View File

@@ -0,0 +1,152 @@
{
"nezha": "哪吒监控",
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟随系统"
},
"Username": "用户名",
"Password": "密码",
"Results": {
"UsernameMin": "用户名必须至少有 2 个字符。",
"PasswordRequired": "密码不能为空。",
"ErrorFetchingResource": "获取资源时出错:{{error}}",
"SelectAtLeastOneServer": "请至少选择一台服务器。",
"UnExpectedError": "意外错误,请查看控制台了解详细信息。",
"ForceUpdate": "强制升级:",
"NoRowsAreSelected": "未选择任何行",
"ThisOperationIsUnrecoverable": "这个操作将无法恢复!",
"TaskTriggeredSuccessfully": "任务触发成功",
"TheServerDoesNotOnline": "服务器不存在或者还未连接"
},
"Login": "登录",
"Server": "服务器",
"Service": "服务",
"Task": "任务",
"Notification": "通知",
"DDNS": "动态域名解析",
"NATT": "内网穿透",
"Group": "分组",
"Profile": "个人信息",
"Settings": "系统设置",
"Logout": "登出",
"NavigateTo": "导航至",
"SelectAPageToNavigateTo": "选择一个页面跳转",
"Close": "关闭",
"Error": "错误",
"Name": "名称",
"Version": "版本",
"Unknown": "未知",
"Enable": "启用",
"HideForGuest": "对游客隐藏",
"InstallCommands": "安装命令",
"Note": "备注",
"Success": "成功",
"Done": "完成",
"Offline": "离线",
"Failure": "失败",
"Loading": "加载中",
"NoResults": "没有内容",
"Actions": "操作",
"EditServer": "编辑服务器",
"Weight": "权重(数字越大,显示越靠前)",
"DDNSProfiles": "DDNS 配置文件 ID",
"SeparateWithComma": "(以英文逗号分隔)",
"Public": "公开",
"Private": "私有",
"Submit": "提交",
"Target": "目标",
"Coverage": "覆盖范围",
"CoverAll": "覆盖全部",
"IgnoreAll": "忽略全部",
"SpecificServers": "特定服务器",
"Type": "类型",
"Interval": "间隔",
"NotifierGroupID": "通知组ID",
"Trigger": "触发",
"TasksToTriggerOnAlert": "触发警报的任务",
"TasksToTriggerAfterRecovery": "恢复后要触发的任务",
"Confirm": "确认",
"ConfirmDeletion": "确认删除?",
"NewService": "新服务",
"Services": "服务",
"ShowInService": "服务中显示",
"Coverages": {
"Excludes": "排除特定服务器",
"Only": "仅特定服务器",
"Alarmed": "在触发报警的服务器上执行"
},
"EnableFailureNotification": "启用失败通知",
"MaximumLatency": "最大延迟时间(毫秒)",
"MinimumLatency": "最小延迟时间(毫秒)",
"EnableLatencyNotification": "启用延迟通知",
"EnableTriggerTask": "启用触发任务",
"CronExpression": "Cron表达式",
"Command": "命令",
"NotifierGroup": "通知组",
"SendSuccessNotification": "发送成功通知",
"LastExecution": "最后执行",
"Result": "结果",
"Scheduled": "计划任务",
"AlertRule": "警报规则",
"Notifier": "通知",
"VerifyTLS": "验证 TLS",
"TriggerMode": "触发模式",
"Rules": "规则",
"RequestMethod": "请求方式",
"RequestHeader": "请求头",
"DoNotSendTestMessage": "不发送测试消息",
"Always": "总是",
"Once": "仅一次",
"Provider": "提供商",
"Domains": "域名",
"MaximumRetryAttempts": "最大重试次数",
"Refresh": "刷新",
"CopyPath": "复制路径",
"Goto": "前往",
"UpdatePassword": "更改密码",
"OriginalPassword": "原始密码",
"NewPassword": "新密码",
"EditDDNS": "编辑DDNS",
"CreateDDNS": "创建DDNS",
"Credential": "凭据",
"RequestType": "请求类型",
"RequestBody": "请求主体",
"FileManager": "文件管理器",
"Downloading": "下载中",
"Uploading": "上传中",
"EditNAT": "编辑内网穿透",
"CreateNAT": "创建内网穿透",
"LocalService": "本地服务",
"BindHostname": "绑定域名",
"EditServerGroup": "编辑服务器分组",
"CreateServerGroup": "创建服务器分组",
"EditService": "编辑服务",
"CreateService": "创建服务",
"EditTask": "编辑任务",
"CreateTask": "创建任务",
"EditNotifier": "编辑通知",
"CreateNotifier": "创建通知",
"EditAlertRule": "编辑报警规则",
"CreateAlertRule": "创建报警规则",
"EditNotifierGroup": "编辑通知分组",
"CreateNotifierGroup": "创建通知分组",
"User": "用户",
"WAF": "Web应用防火墙",
"SiteName": "站点名称",
"Language": "语言",
"CustomCodes": "自定义代码(样式和脚本)",
"CustomCodesDashboard": "仪表板的自定义代码",
"DashboardOriginalHost": "仪表板服务器域名/IP无 CDN",
"CustomPublicDNSNameserversforDDNS": "DDNS 的自定义公共 DNS 名称服务器",
"RealIPHeader": "真实IP请求头",
"IPChangeNotification": "IP变更通知",
"FullIPNotification": "在通知消息中显示完整的 IP 地址",
"LoginFailed": "登录失败",
"BruteForceAttackingToken": "暴力攻击令牌",
"BruteForceAttackingAgentSecret": "暴力攻击代理秘密",
"NewUser": "新用户",
"Count": "计数",
"LastBlockReason": "最后封禁原因",
"LastBlockTime": "最后封禁时间"
}

View File

@@ -0,0 +1,152 @@
{
"nezha": "哪吒監控",
"theme": {
"light": "亮色",
"dark": "暗色",
"system": "跟隨系統"
},
"Username": "用戶名",
"Password": "密碼",
"Results": {
"UsernameMin": "使用者名稱必須至少有 2 個字元。",
"PasswordRequired": "密碼不能為空。",
"ErrorFetchingResource": "取得資源時發生錯誤:{{error}}",
"SelectAtLeastOneServer": "請至少選擇一台伺服器。",
"UnExpectedError": "意外錯誤,請查看控制台以了解詳細資訊。",
"ForceUpdate": "強制升級:",
"NoRowsAreSelected": "未選擇任何行",
"ThisOperationIsUnrecoverable": "這個操作將無法恢復!",
"TaskTriggeredSuccessfully": "任務觸發成功",
"TheServerDoesNotOnline": "伺服器不存在或尚未連接"
},
"Login": "登入",
"Server": "伺服器",
"Service": "服務",
"Task": "任務",
"Notification": "通知",
"DDNS": "動態網域解析",
"NATT": "內網穿透",
"Group": "分組",
"Profile": "個人資訊",
"Settings": "系統設定",
"Logout": "登出",
"NavigateTo": "導航至",
"SelectAPageToNavigateTo": "選擇一個頁面跳轉",
"Close": "關閉",
"Error": "錯誤",
"Name": "名稱",
"Version": "版本",
"Unknown": "未知",
"Enable": "啟用",
"HideForGuest": "對遊客隱藏",
"InstallCommands": "安裝命令",
"Note": "備註",
"Success": "成功",
"Done": "完成",
"Offline": "離線",
"Failure": "失敗",
"NoResults": "沒有內容",
"Loading": "載入中",
"Actions": "操作",
"EditServer": "編輯伺服器",
"Weight": "權重(數字越大,顯示越前)",
"DDNSProfiles": "DDNS 設定檔 ID",
"SeparateWithComma": "(以英文逗號分隔)",
"Public": "公開",
"Private": "私人",
"Submit": "提交",
"Target": "目標",
"Coverage": "覆蓋範圍",
"CoverAll": "覆蓋全部",
"IgnoreAll": "忽略全部",
"SpecificServers": "特定伺服器",
"Type": "類型",
"Interval": "間隔",
"NotifierGroupID": "通知群組ID",
"Trigger": "觸發",
"TasksToTriggerOnAlert": "觸發警報的任務",
"TasksToTriggerAfterRecovery": "恢復後要觸發的任務",
"Confirm": "確認",
"ConfirmDeletion": "確認刪除?",
"NewService": "新服務",
"Services": "服務",
"ShowInService": "服務中顯示",
"Coverages": {
"Only": "僅特定伺服器",
"Excludes": "排除特定伺服器",
"Alarmed": "在觸發警報的伺服器上執行"
},
"EnableFailureNotification": "啟用失敗通知",
"MaximumLatency": "最大延遲時間(毫秒)",
"MinimumLatency": "最小延遲時間(毫秒)",
"EnableLatencyNotification": "啟用延遲通知",
"EnableTriggerTask": "啟用觸發任務",
"CronExpression": "Cron表達式",
"Command": "命令",
"NotifierGroup": "通知群組",
"SendSuccessNotification": "發送成功通知",
"LastExecution": "最後執行",
"Result": "結果",
"Scheduled": "計劃任務",
"AlertRule": "警報規則",
"Notifier": "通知",
"VerifyTLS": "驗證 TLS",
"TriggerMode": "觸發模式",
"Rules": "規則",
"RequestMethod": "請求方式",
"RequestHeader": "請求頭",
"DoNotSendTestMessage": "不發送測試訊息",
"Always": "總是",
"Once": "僅一次",
"Provider": "提供者",
"Domains": "網域",
"MaximumRetryAttempts": "最大重試次數",
"Refresh": "刷新",
"CopyPath": "複製路徑",
"Goto": "前往",
"UpdatePassword": "更改密碼",
"OriginalPassword": "原始密碼",
"NewPassword": "新密碼",
"EditDDNS": "編輯DDNS",
"CreateDDNS": "建立DDNS",
"Credential": "憑證",
"RequestType": "請求類型",
"RequestBody": "請求主體",
"FileManager": "文件管理器",
"Downloading": "下載中",
"Uploading": "上傳中",
"EditNAT": "編輯內網穿透",
"CreateNAT": "創建內網穿透",
"LocalService": "本地服務",
"BindHostname": "綁定域名",
"EditServerGroup": "編輯伺服器分組",
"CreateServerGroup": "建立伺服器分組",
"EditService": "編輯服務",
"CreateService": "創建服務",
"EditTask": "編輯任務",
"CreateTask": "創建任務",
"CreateNotifier": "建立通知",
"EditNotifier": "編輯通知",
"EditAlertRule": "編輯警報規則",
"CreateAlertRule": "建立警報規則",
"EditNotifierGroup": "編輯通知分組",
"CreateNotifierGroup": "建立通知分組",
"User": "使用者",
"WAF": "Web應用防火牆",
"SiteName": "網站名稱",
"Language": "語言",
"CustomCodes": "自訂程式碼(樣式和腳本)",
"CustomCodesDashboard": "儀表板的自訂程式碼",
"DashboardOriginalHost": "儀表板伺服器網域/IP無 CDN",
"CustomPublicDNSNameserversforDDNS": "DDNS 的自訂公共 DNS 名稱伺服器",
"RealIPHeader": "真實IP請求頭",
"IPChangeNotification": "IP變更通知",
"FullIPNotification": "在通知訊息中顯示完整的 IP 位址",
"LoginFailed": "登入失敗",
"BruteForceAttackingToken": "暴力攻擊令牌",
"BruteForceAttackingAgentSecret": "暴力攻擊代理秘密",
"NewUser": "新用戶",
"Count": "計數",
"LastBlockReason": "最後封鎖原因",
"LastBlockTime": "最後封鎖時間"
}

View File

@@ -6,6 +6,7 @@ import {
} from "react-router-dom";
import './index.css'
import './lib/i18n';
import Root from "./routes/root";
import ErrorPage from "./error-page";

View File

@@ -19,7 +19,11 @@ import { deleteAlertRules } from "@/api/alert-rule";
import { NotificationTab } from "@/components/notification-tab";
import { AlertRuleCard } from "@/components/alert-rule";
import { useTranslation } from "react-i18next";
export default function AlertRulePage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>(
"/api/v1/alert-rule",
swrFetcher
@@ -27,9 +31,10 @@ export default function AlertRulePage() {
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelAlertRule>[] = [
@@ -61,7 +66,7 @@ export default function AlertRulePage() {
accessorFn: (row) => row.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
@@ -70,38 +75,38 @@ export default function AlertRulePage() {
},
},
{
header: "Notifier Group",
header: t("NotifierGroup"),
accessorKey: "ngroup",
accessorFn: (row) => row.notification_group_id,
},
{
header: "Trigger Mode",
header: t("TriggerMode"),
accessorKey: "trigger Mode",
accessorFn: (row) => triggerModes[row.trigger_mode] || "",
},
{
header: "Rules",
header: t("Rules"),
accessorKey: "rules",
accessorFn: (row) => JSON.stringify(row.rules),
},
{
header: "Tasks to trigger on alert",
header: t("TasksToTriggerOnAlert"),
accessorKey: "failTriggerTasks",
accessorFn: (row) => row.fail_trigger_tasks,
},
{
header: "Tasks to trigger after recovery",
header: t("TasksToTriggerAfterRecovery"),
accessorKey: "recoverTriggerTasks",
accessorFn: (row) => row.recover_trigger_tasks,
},
{
header: "Enable",
header: t("Enable"),
accessorKey: "enable",
accessorFn: (row) => row.enable,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -168,7 +173,7 @@ export default function AlertRulePage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -184,7 +189,7 @@ export default function AlertRulePage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -20,14 +20,18 @@ import { CronCard } from "@/components/cron";
import { cronTypes } from "@/types";
import { IconButton } from "@/components/xui/icon-button";
import { useTranslation } from "react-i18next";
export default function CronPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher);
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelCron>[] = [
@@ -59,7 +63,7 @@ export default function CronPage() {
accessorFn: (row) => row.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
cell: ({ row }) => {
const s = row.original;
@@ -67,17 +71,17 @@ export default function CronPage() {
},
},
{
header: "Task Type",
header: t("Type"),
accessorKey: "taskType",
accessorFn: (row) => cronTypes[row.task_type] || "",
},
{
header: "Cron Expression",
header: t("CronExpression"),
accessorKey: "scheduler",
accessorFn: (row) => row.scheduler,
},
{
header: "Command",
header: t("Command"),
accessorKey: "command",
cell: ({ row }) => {
const s = row.original;
@@ -85,17 +89,17 @@ export default function CronPage() {
},
},
{
header: "Notifier Group",
header: t("NotifierGroup"),
accessorKey: "ngroup",
accessorFn: (row) => row.notification_group_id,
},
{
header: "Send Success Notification",
header: t("SendSuccessNotification"),
accessorKey: "pushSuccessful",
accessorFn: (row) => row.push_successful ?? false,
},
{
header: "Coverage",
header: t("Coverage"),
accessorKey: "cover",
accessorFn: (row) => row.cover,
cell: ({ row }) => {
@@ -120,12 +124,12 @@ export default function CronPage() {
},
},
{
header: "Specific Servers",
header: t("SpecificServers"),
accessorKey: "servers",
accessorFn: (row) => row.servers,
},
{
header: "Last Execution",
header: t("LastExecution"),
accessorKey: "lastExecution",
accessorFn: (row) => row.last_executed_at,
cell: ({ row }) => {
@@ -134,13 +138,13 @@ export default function CronPage() {
},
},
{
header: "Last Result",
header: t("Result"),
accessorKey: "lastResult",
accessorFn: (row) => row.last_result ?? false,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -157,14 +161,14 @@ export default function CronPage() {
await runCron(s.id);
} catch (e) {
console.error(e);
toast("Error executing task", {
description: "Please see the console for details.",
toast(t("Error"), {
description: t("Results.UnExpectedError"),
});
await mutate();
return;
}
toast("Success", {
description: "The task triggered successfully.",
toast(t("Success"), {
description: t("Results.TaskTriggeredSuccessfully"),
});
await mutate();
}}
@@ -192,7 +196,7 @@ export default function CronPage() {
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">Task</h1>
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
@@ -225,7 +229,7 @@ export default function CronPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -241,7 +245,7 @@ export default function CronPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -18,7 +18,10 @@ import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns";
import { useTranslation } from "react-i18next";
export default function DDNSPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>("/api/v1/ddns", swrFetcher);
const [providers, setProviders] = useState<string[]>([]);
@@ -32,9 +35,10 @@ export default function DDNSPage() {
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelDDNSProfile>[] = [
@@ -66,7 +70,7 @@ export default function DDNSPage() {
accessorFn: (row) => row.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
@@ -75,22 +79,22 @@ export default function DDNSPage() {
},
},
{
header: "IPv4 Enabled",
header: "IPv4",
accessorKey: "enableIPv4",
accessorFn: (row) => row.enable_ipv4 ?? false,
},
{
header: "IPv6 Enabled",
header: "IPv6",
accessorKey: "enableIPv6",
accessorFn: (row) => row.enable_ipv6 ?? false,
},
{
header: "DDNS Provider",
header: t("Provider"),
accessorKey: "provider",
accessorFn: (row) => row.provider,
},
{
header: "Domains",
header: t('Domains'),
accessorKey: "domains",
accessorFn: (row) => row.domains,
cell: ({ row }) => {
@@ -99,13 +103,13 @@ export default function DDNSPage() {
},
},
{
header: "Maximum retry attempts",
header: t("MaximumRetryAttempts"),
accessorKey: "maxRetries",
accessorFn: (row) => row.max_retries,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -135,7 +139,7 @@ export default function DDNSPage() {
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">Dynamic DNS</h1>
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
@@ -168,7 +172,7 @@ export default function DDNSPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -184,7 +188,7 @@ export default function DDNSPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -15,16 +14,20 @@ import {
import { Input } from "@/components/ui/input"
import { useAuth } from "@/hooks/useAuth"
import { useTranslation } from "react-i18next"
import i18next from "i18next";
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
message: i18next.t("Results.UsernameMin", { number: 2 }),
}),
password: z.string().min(1, {
message: "Password cannot be empty.",
message: i18next.t("Results.PasswordRequired"),
})
})
export default () => {
function Login() {
const { login } = useAuth()
const form = useForm<z.infer<typeof formSchema>>({
@@ -39,6 +42,8 @@ export default () => {
login(values.username, values.password)
}
const { t } = useTranslation();
return (
<div className="my-8 max-w-xl m-auto">
<Form {...form}>
@@ -48,13 +53,10 @@ export default () => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormLabel>{t("Username")}</FormLabel>
<FormControl>
<Input placeholder="shadcn" autoComplete="username" {...field} />
<Input placeholder="admin" autoComplete="username" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -64,20 +66,19 @@ export default () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("Password")}</FormLabel>
<FormControl>
<Input type="password" placeholder="shadcn" autoComplete="current-password" {...field} />
<Input type="password" placeholder="admin" autoComplete="current-password" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Login</Button>
<Button type="submit">{t("Login")}</Button>
</form>
</Form>
</div>
)
}
export default Login;

View File

@@ -18,14 +18,18 @@ import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { deleteNAT } from "@/api/nat";
import { useTranslation } from "react-i18next";
export default function NATPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelNAT>[] = [
@@ -57,7 +61,7 @@ export default function NATPage() {
accessorFn: (row) => row.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
@@ -66,12 +70,12 @@ export default function NATPage() {
},
},
{
header: "Server ID",
header: t("Server")+" ID",
accessorKey: "serverID",
accessorFn: (row) => row.server_id,
},
{
header: "Local service",
header: t("LocalService"),
accessorKey: "host",
accessorFn: (row) => row.host,
cell: ({ row }) => {
@@ -80,7 +84,7 @@ export default function NATPage() {
},
},
{
header: "Bind hostname",
header: t("BindHostname"),
accessorKey: "domain",
accessorFn: (row) => row.domain,
cell: ({ row }) => {
@@ -90,7 +94,7 @@ export default function NATPage() {
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -120,7 +124,7 @@ export default function NATPage() {
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">NAT Traversal</h1>
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
@@ -153,7 +157,7 @@ export default function NATPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -169,7 +173,7 @@ export default function NATPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -19,7 +19,10 @@ import { deleteNotificationGroups } from "@/api/notification-group";
import { GroupTab } from "@/components/group-tab";
import { NotificationGroupCard } from "@/components/notification-group";
import { useTranslation } from "react-i18next";
export default function NotificationGroupPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
"/api/v1/notification-group",
swrFetcher
@@ -27,9 +30,10 @@ export default function NotificationGroupPage() {
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
@@ -61,7 +65,7 @@ export default function NotificationGroupPage() {
accessorFn: (row) => row.group.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.group.name,
cell: ({ row }) => {
@@ -70,13 +74,13 @@ export default function NotificationGroupPage() {
},
},
{
header: "Notifiers (ID)",
header: t("Notifier")+"(ID)",
accessorKey: "notifications",
accessorFn: (row) => row.notifications,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -143,7 +147,7 @@ export default function NotificationGroupPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -159,7 +163,7 @@ export default function NotificationGroupPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -20,7 +20,11 @@ import { NotificationTab } from "@/components/notification-tab";
import { NotifierCard } from "@/components/notifier";
import { useNotification } from "@/hooks/useNotfication";
import { useTranslation } from "react-i18next";
export default function NotificationPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
"/api/v1/notification",
swrFetcher
@@ -29,9 +33,10 @@ export default function NotificationPage() {
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelNotification>[] = [
@@ -63,7 +68,7 @@ export default function NotificationPage() {
accessorFn: (row) => row.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
@@ -72,7 +77,7 @@ export default function NotificationPage() {
},
},
{
header: "Groups",
header: t("Group"),
accessorKey: "groups",
accessorFn: (row) => {
return (
@@ -92,13 +97,13 @@ export default function NotificationPage() {
},
},
{
header: "Verify TLS",
header: t("VerifyTLS"),
accessorKey: "verify_tls",
accessorFn: (row) => row.verify_tls,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -165,7 +170,7 @@ export default function NotificationPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -181,7 +186,7 @@ export default function NotificationPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -5,7 +5,10 @@ import { ThemeProvider } from "@/components/theme-provider";
import Header from "@/components/header";
import { Toaster } from "@/components/ui/sonner";
import { useTranslation } from "react-i18next";
export default function Root() {
const { t } = useTranslation();
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<Card className="text-sm max-w-7xl mx-auto mt-5 min-h-[90%] flex flex-col justify-between">
@@ -14,7 +17,7 @@ export default function Root() {
<Outlet />
</div>
<footer className="mx-5 pb-5 text-foreground/60 font-thin text-center">
&copy; 2019-2024 Nezha Monitoring
&copy; 2019-2024 {t('nezha')}
</footer>
</Card>
<Toaster />

View File

@@ -19,7 +19,10 @@ import { deleteServerGroups } from "@/api/server-group";
import { GroupTab } from "@/components/group-tab";
import { ServerGroupCard } from "@/components/server-group";
import { useTranslation } from "react-i18next";
export default function ServerGroupPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>(
"/api/v1/server-group",
swrFetcher
@@ -27,9 +30,10 @@ export default function ServerGroupPage() {
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [
@@ -61,7 +65,7 @@ export default function ServerGroupPage() {
accessorFn: (row) => row.group.id,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.group.name,
cell: ({ row }) => {
@@ -70,13 +74,13 @@ export default function ServerGroupPage() {
},
},
{
header: "Servers (ID)",
header: t("Server")+"(ID)",
accessorKey: "servers",
accessorFn: (row) => row.servers,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -142,7 +146,7 @@ export default function ServerGroupPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -158,7 +162,7 @@ export default function ServerGroupPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -24,15 +24,19 @@ import { TerminalButton } from "@/components/terminal";
import { useServer } from "@/hooks/useServer";
import { joinIP } from "@/lib/utils";
import { useTranslation } from "react-i18next";
export default function ServerPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher);
const { serverGroups } = useServer();
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<Server>[] = [
@@ -64,7 +68,7 @@ export default function ServerPage() {
accessorFn: (row) => `${row.id}(${row.display_index})`,
},
{
header: "Name",
header: t("Name"),
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
@@ -73,7 +77,7 @@ export default function ServerPage() {
},
},
{
header: "Groups",
header: t("Group"),
accessorKey: "groups",
accessorFn: (row) => {
return (
@@ -90,28 +94,28 @@ export default function ServerPage() {
},
},
{
header: "Version",
header: t("Version"),
accessorKey: "host.version",
accessorFn: (row) => row.host.version || "Unknown",
accessorFn: (row) => row.host.version || t("Unknown"),
},
{
header: "Enable DDNS",
header: t("Enable") + t("DDNS"),
accessorKey: "enableDDNS",
accessorFn: (row) => row.enable_ddns ?? false,
},
{
header: "Hide from Guest",
header: t("HideForGuest"),
accessorKey: "hideForGuest",
accessorFn: (row) => row.hide_for_guest ?? false,
},
{
id: "installCommands",
header: "Install commands",
header: t("InstallCommands"),
cell: () => <InstallCommandsMenu />,
},
{
id: "note",
header: "Note",
header: t("Note"),
cell: ({ row }) => {
const s = row.original;
return <NoteMenu note={{ private: s.note, public: s.public_note }} />;
@@ -119,7 +123,7 @@ export default function ServerPage() {
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -152,7 +156,7 @@ export default function ServerPage() {
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">Server</h1>
<h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
@@ -166,8 +170,8 @@ export default function ServerPage() {
onClick={async () => {
const id = selectedRows.map((r) => r.original.id);
if (id.length < 1) {
toast("Error", {
description: "No rows are selected.",
toast(t("Error"), {
description: t("Results.SelectAtLeastOneServer"),
});
return;
}
@@ -177,23 +181,21 @@ export default function ServerPage() {
resp = await forceUpdateServer(id);
} catch (e) {
console.error(e);
toast("Error executing task", {
description: "Please see the console for details.",
toast(t("Error"), {
description: t("Results.UnExpectedError"),
});
return;
}
toast("Task executed successfully", {
description: `Result (Server ID):
${resp.success?.length ? `Success: ${resp.success.join(",")}, ` : ""}
${resp.failure?.length ? `Failure: ${resp.failure.join(",")}, ` : ""}
${resp.offline?.length ? `Offline: ${resp.offline.join(",")}` : ""}
`,
toast(t("Done"), {
description: t("Results.ForceUpdate")
+ (resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "")
+ (resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "")
+ (resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : "")
});
}}
/>
</HeaderButtonGroup>
</div>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -214,7 +216,7 @@ export default function ServerPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -230,7 +232,7 @@ export default function ServerPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -20,7 +20,10 @@ import { deleteService } from "@/api/service";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
export default function ServicePage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelServiceResponse>(
"/api/v1/service",
swrFetcher
@@ -28,9 +31,10 @@ export default function ServicePage() {
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<Service>[] = [
@@ -62,7 +66,7 @@ export default function ServicePage() {
accessorFn: (row) => row.service.id,
},
{
header: "Name",
header: t("Name"),
accessorFn: (row) => row.service.name,
accessorKey: "service.name",
cell: ({ row }) => {
@@ -71,7 +75,7 @@ export default function ServicePage() {
},
},
{
header: "Target",
header: t("Target"),
accessorFn: (row) => row.service.target,
accessorKey: "service.target",
cell: ({ row }) => {
@@ -80,7 +84,7 @@ export default function ServicePage() {
},
},
{
header: "Coverage",
header: t("Coverage"),
accessorKey: "service.cover",
accessorFn: (row) => row.service.cover,
cell: ({ row }) => {
@@ -90,10 +94,10 @@ export default function ServicePage() {
{(() => {
switch (s.cover) {
case 0: {
return <span>Cover All</span>;
return <span>{t("CoverAll")}</span>;
}
case 1: {
return <span>Ignore All</span>;
return <span>{t("IgnoreAll")}</span>;
}
}
})()}
@@ -102,44 +106,44 @@ export default function ServicePage() {
},
},
{
header: "Specific Servers",
header: t("SpecificServers"),
accessorKey: "service.skipServers",
accessorFn: (row) => Object.keys(row.service.skip_servers ?? {}),
},
{
header: "Type",
header: t("Type"),
accessorKey: "service.type",
accessorFn: (row) => row.service.type,
cell: ({ row }) => serviceTypes[row.original.service.type] || "",
},
{
header: "Interval",
header: t("Interval"),
accessorKey: "service.duration",
accessorFn: (row) => row.service.duration,
},
{
header: "Notifier Group ID",
header: t("NotifierGroupID"),
accessorKey: "service.ngroup",
accessorFn: (row) => row.service.notification_group_id,
},
{
header: "On Trigger",
header: t("Trigger"),
accessorKey: "service.triggerTask",
accessorFn: (row) => row.service.enable_trigger_task ?? false,
},
{
header: "Tasks to trigger on alert",
header: t("TasksToTriggerOnAlert"),
accessorKey: "service.failTriggerTasks",
accessorFn: (row) => row.service.fail_trigger_tasks,
},
{
header: "Tasks to trigger after recovery",
header: t("TasksToTriggerAfterRecovery"),
accessorKey: "service.recoverTriggerTasks",
accessorFn: (row) => row.service.recover_trigger_tasks,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -169,7 +173,7 @@ export default function ServicePage() {
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">Service</h1>
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Services")}</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
@@ -202,7 +206,7 @@ export default function ServicePage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -218,7 +222,7 @@ export default function ServicePage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -29,6 +29,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useTranslation } from "react-i18next";
const settingFormSchema = z.object({
custom_nameservers: asOptionalField(z.string()),
ignored_ip_notification: asOptionalField(z.string()),
@@ -46,14 +48,16 @@ const settingFormSchema = z.object({
});
export default function SettingsPage() {
const { t } = useTranslation();
const [config, setConfig] = useState<ModelConfig>();
const [error, setError] = useState<Error>();
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
useEffect(() => {
@@ -98,9 +102,7 @@ export default function SettingsPage() {
if (e instanceof Error) setError(e);
return;
} finally {
toast("Success", {
description: "Config updated successfully.",
});
toast(t("Success"));
}
};
@@ -115,7 +117,7 @@ export default function SettingsPage() {
name="site_name"
render={({ field }) => (
<FormItem>
<FormLabel>Site Name</FormLabel>
<FormLabel>{t("SiteName")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -128,7 +130,7 @@ export default function SettingsPage() {
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<FormLabel>{t("Language")}</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
@@ -154,7 +156,7 @@ export default function SettingsPage() {
name="custom_code"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Codes (Style and Script)</FormLabel>
<FormLabel>{t("CustomCodes")}</FormLabel>
<FormControl>
<Textarea className="resize-y min-h-48" {...field} />
</FormControl>
@@ -167,7 +169,7 @@ export default function SettingsPage() {
name="custom_code_dashboard"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Codes for Dashboard</FormLabel>
<FormLabel>{t("CustomCodesDashboard")}</FormLabel>
<FormControl>
<Textarea className="resize-y min-h-48" {...field} />
</FormControl>
@@ -180,7 +182,7 @@ export default function SettingsPage() {
name="install_host"
render={({ field }) => (
<FormItem>
<FormLabel>Dashboard Server Domain/IP without CDN</FormLabel>
<FormLabel>{t("DashboardOriginalHost")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@@ -194,7 +196,7 @@ export default function SettingsPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
Custom Public DNS Nameservers for DDNS (separate with comma)
{t("CustomPublicDNSNameserversforDDNS")+" " + t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input {...field} />
@@ -208,7 +210,7 @@ export default function SettingsPage() {
name="real_ip_header"
render={({ field }) => (
<FormItem>
<FormLabel>Real IP Header</FormLabel>
<FormLabel>{t("RealIPHeader")}</FormLabel>
<FormControl>
<Input placeholder="NZ::Use-Peer-IP" {...field} />
</FormControl>
@@ -217,7 +219,7 @@ export default function SettingsPage() {
)}
/>
<FormItem>
<FormLabel>IP Change Notification</FormLabel>
<FormLabel>{t("IPChangeNotification")}</FormLabel>
<Card className="w-full">
<CardContent>
<div className="flex flex-col space-y-4 mt-4">
@@ -226,7 +228,7 @@ export default function SettingsPage() {
name="cover"
render={({ field }) => (
<FormItem>
<FormLabel>Coverage</FormLabel>
<FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} value={`${field.value}`}>
<FormControl>
<SelectTrigger>
@@ -250,7 +252,7 @@ export default function SettingsPage() {
name="ignored_ip_notification"
render={({ field }) => (
<FormItem>
<FormLabel>Specific Servers (separate with comma)</FormLabel>
<FormLabel>{t("SpecificServers")+" " + t("SeparateWithComma")}</FormLabel>
<FormControl>
<Input placeholder="1,2,3" {...field} />
</FormControl>
@@ -266,7 +268,7 @@ export default function SettingsPage() {
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">Enable</Label>
<Label className="text-sm">{t("Enable")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -286,7 +288,7 @@ export default function SettingsPage() {
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">
Show Full IP Address in Notification Messages
{t("FullIPNotification")}
</Label>
</div>
</FormControl>
@@ -294,7 +296,7 @@ export default function SettingsPage() {
</FormItem>
)}
/>
<Button type="submit">Save</Button>
<Button type="submit">{t("Confirm")}</Button>
</form>
</Form>
</div>

View File

@@ -19,14 +19,18 @@ import { deleteUser } from "@/api/user";
import { SettingsTab } from "@/components/settings-tab";
import { UserCard } from "@/components/user";
import { useTranslation } from "react-i18next";
export default function UserPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher);
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t("Results.UnExpectedError", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelUser>[] = [
@@ -58,13 +62,13 @@ export default function UserPage() {
accessorFn: (row) => row.id,
},
{
header: "Username",
header: t("Username"),
accessorKey: "username",
accessorFn: (row) => row.username,
},
{
id: "actions",
header: "Actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
return (
@@ -131,7 +135,7 @@ export default function UserPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -147,7 +151,7 @@ export default function UserPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -19,14 +19,18 @@ import { deleteWAF } from "@/api/waf";
import { ip16Str } from "@/lib/utils";
import { SettingsTab } from "@/components/settings-tab";
import { useTranslation } from "react-i18next";
export default function WAFPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelWAF[]>("/api/v1/waf", swrFetcher);
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
toast(t("Error"), {
description: t(`Error fetching resource: ${error.message}.`),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
const columns: ColumnDef<ModelWAF>[] = [
@@ -58,18 +62,18 @@ export default function WAFPage() {
accessorFn: (row) => ip16Str(row.ip ?? []),
},
{
header: "Count",
header: t("Count"),
accessorKey: "count",
accessorFn: (row) => row.count,
},
{
header: "Last Block Reason",
header: t("LastBlockReason"),
accessorKey: "lastBlockReason",
accessorFn: (row) => row.last_block_reason,
cell: ({ row }) => <span>{wafBlockReasons[row.original.last_block_reason] || ""}</span>,
},
{
header: "Last Block Time",
header: t("LastBlockTime"),
accessorKey: "lastBlockTime",
accessorFn: (row) => row.last_block_timestamp,
cell: ({ row }) => {
@@ -146,7 +150,7 @@ export default function WAFPage() {
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading ...
{t("Loading")}...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
@@ -162,7 +166,7 @@ export default function WAFPage() {
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{t("NoResults")}
</TableCell>
</TableRow>
)}

View File

@@ -1,4 +1,6 @@
import i18next from "i18next";
export const triggerModes: Record<number, string> = {
0: "Always",
1: "Once",
0: i18next.t("Always"),
1: i18next.t("Once"),
}

View File

@@ -1,10 +1,12 @@
import i18next from "i18next";
export const cronTypes: Record<number, string> = {
0: "Scheduled",
1: "Trigger",
0: i18next.t("Scheduled"),
1: i18next.t("Trigger"),
}
export const cronCoverageTypes: Record<number, string> = {
0: "Only specific servers",
1: "All excludes specific servers",
2: "The alarmed servers"
0: i18next.t("Coverages.Only"),
1: i18next.t("Coverages.Excludes"),
2: i18next.t("Coverages.Alarmed"),
}

View File

@@ -1,3 +1,4 @@
import i18next from "i18next";
export const serviceTypes: Record<number, string> = {
1: "HTTP GET",
2: "ICMP Ping",
@@ -5,6 +6,6 @@ export const serviceTypes: Record<number, string> = {
}
export const serviceCoverageTypes: Record<number, string> = {
0: "All excludes specific servers",
1: "Only specific servers",
0: i18next.t("Coverages.Excludes"),
1: i18next.t("Coverages.Only"),
}