fix: translation

This commit is contained in:
wonfen 2024-11-09 08:59:30 +08:00
parent d3f6822080
commit e5740579f4
6 changed files with 446 additions and 269 deletions

View File

@ -32,7 +32,7 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
[狗狗加速 —— 技术流机场 Doggygo VPN](https://verge.dginv.click/#/register?code=oaxsAGo6)
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用https://verge.dginv.click/#/register?code=oaxsAGo6
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用:[点此注册](https://verge.dginv.click/#/register?code=oaxsAGo6)
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
@ -43,11 +43,13 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
## Features
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
- Improved UI and supports custom theme color.
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
- System proxy setting and guard.
- 基于性能强劲的 Rust 和 Tauri 2 框架
- 内置[Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo)内核,并支持切换 `Alpha` 版本内核。
- 简洁美观的用户界面,支持自定义主题颜色、代理组/托盘图标以及 `CSS Injection`
- 配置文件管理和增强Merge 和 Script配置文件语法提示。
- 系统代理和守卫、`TUN(虚拟网卡)` 模式。
- 可视化节点和规则编辑
- WebDav 配置备份和同步
### FAQ

View File

@ -4,11 +4,14 @@ import {
useState,
useRef,
SVGProps,
useCallback,
useMemo,
memo,
} from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { Typography } from "@mui/material";
import { useForm } from "react-hook-form";
import { useForm, UseFormRegister } from "react-hook-form";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { isValidUrl } from "@/utils/helper";
@ -53,6 +56,36 @@ type BackupFile = IWebDavFile & {
allow_apply: boolean;
};
interface BackupTableProps {
datasource: BackupFile[];
page: number;
rowsPerPage: number;
onPageChange: (event: any, newPage: number) => void;
onRowsPerPageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
totalCount: number;
}
interface WebDAVConfigFormProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
initialValues: Partial<IWebDavConfig>;
urlRef: React.RefObject<HTMLInputElement>;
usernameRef: React.RefObject<HTMLInputElement>;
passwordRef: React.RefObject<HTMLInputElement>;
showPassword: boolean;
onShowPasswordClick: () => void;
webdavChanged: boolean;
webdavUrl: string | undefined | null;
webdavUsername: string | undefined | null;
webdavPassword: string | undefined | null;
handleBackup: () => Promise<void>;
register: UseFormRegister<IWebDavConfig>;
}
// 将魔法数字和配置提取为常量
const DEFAULT_ROWS_PER_PAGE = 5;
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
@ -62,13 +95,16 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const [showPassword, setShowPassword] = useState(false);
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [backupState, setBackupState] = useState({
files: [] as BackupFile[],
page: 0,
rowsPerPage: DEFAULT_ROWS_PER_PAGE,
isLoading: false,
});
const OS = getSystem();
const urlRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: {
url: webdav_url,
@ -96,34 +132,36 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
}));
// Handle page change
const handleChangePage = (
_: React.MouseEvent<HTMLButtonElement> | null,
page: number
) => {
console.log(page);
setPage(page);
};
const handleChangePage = useCallback(
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
setBackupState((prev) => ({ ...prev, page }));
},
[]
);
// Handle rows per page change
const handleChangeRowsPerPage = (event: any) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0); // Reset to the first page
};
const handleChangeRowsPerPage = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setBackupState((prev) => ({
...prev,
rowsPerPage: parseInt(event.target.value, 10),
page: 0,
}));
},
[]
);
const fetchAndSetBackupFiles = () => {
setIsLoading(true); // Assuming setIsLoading is defined in your component or context to manage loading state
getAllBackupFiles()
.then((files: BackupFile[]) => {
console.log(files);
setBackupFiles(files); // Assuming setBackupFiles is a state setter function in your component or context
})
.catch((e) => {
console.error(e);
})
.finally(() => {
setIsLoading(false);
});
const fetchAndSetBackupFiles = async () => {
try {
setBackupState((prev) => ({ ...prev, isLoading: true }));
const files = await getAllBackupFiles();
setBackupState((prev) => ({ ...prev, files }));
} catch (error) {
console.error("Failed to fetch backup files:", error);
Notice.error(t("Failed to fetch backup files"));
} finally {
setBackupState((prev) => ({ ...prev, isLoading: false }));
}
};
const checkForm = () => {
@ -132,21 +170,21 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const url = urlRef.current?.value;
if (!url) {
Notice.error(t("Webdav url cannot be empty"));
Notice.error(t("WebDAV URL Required"));
urlRef.current?.focus();
return;
} else if (!isValidUrl(url)) {
Notice.error(t("Webdav address must be url"));
Notice.error(t("Invalid WebDAV URL"));
urlRef.current?.focus();
return;
}
if (!username) {
Notice.error(t("Username cannot be empty"));
Notice.error(t("Username Required"));
usernameRef.current?.focus();
return;
}
if (!password) {
Notice.error(t("Password cannot be empty"));
Notice.error(t("Password Required"));
passwordRef.current?.focus();
return;
}
@ -154,7 +192,7 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const submit = async (data: IWebDavConfig) => {
checkForm();
setIsLoading(true);
setBackupState((prev) => ({ ...prev, isLoading: true }));
await saveWebdavConfig(data.url, data.username, data.password)
.then(() => {
mutateVerge(
@ -165,36 +203,34 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
},
false
);
Notice.success(t("Webdav Config Saved Successfully"), 1500);
Notice.success(t("WebDAV Config Saved"));
})
.catch((e) => {
Notice.error(t("Webdav Config Save Failed", { error: e }), 3000);
Notice.error(t("WebDAV Config Save Failed", { error: e }), 3000);
})
.finally(() => {
setIsLoading(false);
setBackupState((prev) => ({ ...prev, isLoading: false }));
fetchAndSetBackupFiles();
});
};
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleClickShowPassword = useCallback(() => {
setShowPassword((prev) => !prev);
}, []);
const handleBackup = useLockFn(async () => {
try {
checkForm();
setIsLoading(true);
await createWebdavBackup()
.then(() => {
Notice.success(t("Backup Successfully"), 1500);
})
.finally(() => {
setIsLoading(false);
fetchAndSetBackupFiles();
})
.catch((e) => {
console.log(e, "backup failed");
Notice.error(t("Backup Failed", { error: e }), 3000);
});
setBackupState((prev) => ({ ...prev, isLoading: true }));
await createWebdavBackup();
Notice.success(t("Backup Created"));
await fetchAndSetBackupFiles();
} catch (error) {
console.error("Backup failed:", error);
Notice.error(t("Backup Failed", { error }));
} finally {
setBackupState((prev) => ({ ...prev, isLoading: false }));
}
});
const getAllBackupFiles = async () => {
@ -202,10 +238,8 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
return files
.map((file) => {
const platform = file.filename.split("-")[0];
const fileBackupTimeStr = file.filename.match(
/\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/
)!;
const backupTime = dayjs(fileBackupTimeStr[0], "YYYY-MM-DD_HH-mm-ss");
const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!;
const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
const allowApply = OS === platform;
return {
...file,
@ -217,10 +251,17 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
};
const datasource = backupFiles.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
const datasource = useMemo(() => {
return backupState.files.slice(
backupState.page * backupState.rowsPerPage,
backupState.page * backupState.rowsPerPage + backupState.rowsPerPage
);
}, [backupState.files, backupState.page, backupState.rowsPerPage]);
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleSubmit(submit)(e);
};
return (
<BaseDialog
@ -234,114 +275,60 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)}
>
<Box sx={{ maxWidth: 800 }}>
<BaseLoadingOverlay isLoading={isLoading} />
<BaseLoadingOverlay isLoading={backupState.isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}>
<form onSubmit={handleSubmit(submit)}>
<Grid container spacing={2}>
<Grid item xs={12} sm={9}>
<Grid container spacing={2}>
{/* WebDAV Server Address */}
<Grid item xs={12}>
<TextField
fullWidth
label="WebDAV Server URL"
variant="outlined"
size="small"
{...register("url")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={urlRef}
/>
</Grid>
{/* Username and Password */}
<Grid item xs={6}>
<TextField
label="Username"
variant="outlined"
size="small"
{...register("username")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={usernameRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Password"
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={passwordRef}
{...register("password")}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
),
<WebDAVConfigForm
onSubmit={onFormSubmit}
initialValues={{
url: webdav_url,
username: webdav_username,
password: webdav_password,
}}
urlRef={urlRef}
usernameRef={usernameRef}
passwordRef={passwordRef}
showPassword={showPassword}
onShowPasswordClick={handleClickShowPassword}
webdavChanged={webdavChanged}
webdavUrl={webdav_url}
webdavUsername={webdav_username}
webdavPassword={webdav_password}
handleBackup={handleBackup}
register={register}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={3}>
<Stack
direction="column"
justifyContent="center"
alignItems="stretch"
sx={{ height: "100%" }}
>
{webdavChanged ||
webdav_url === null ||
webdav_username == null ||
webdav_password == null ? (
<Button
variant="contained"
color="primary"
sx={{ height: "100%" }}
type="submit"
>
Save
</Button>
) : (
<Button
variant="contained"
color="success"
sx={{ height: "100%" }}
onClick={handleBackup}
type="button"
>
Backup
</Button>
)}
</Stack>
</Grid>
</Grid>
</form>
<Divider sx={{ marginY: 2 }} />
<BackupTable
datasource={datasource}
page={backupState.page}
rowsPerPage={backupState.rowsPerPage}
onPageChange={(_, page) => handleChangePage(null, page)}
onRowsPerPageChange={handleChangeRowsPerPage}
totalCount={backupState.files.length}
/>
</Paper>
</Box>
</BaseDialog>
);
});
const BackupTable = memo(
({
datasource,
page,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
totalCount,
}: BackupTableProps) => {
const { t } = useTranslation();
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
<TableCell>{t("Filename")}</TableCell>
<TableCell>{t("Time")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
@ -371,8 +358,9 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
>
<IconButton
color="secondary"
aria-label="delete"
aria-label={t("Delete")}
size="small"
title={t("Delete Backup")}
>
<DeleteIcon />
</IconButton>
@ -381,11 +369,11 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
flexItem
sx={{ mx: 1, height: 24 }}
/>
<IconButton
color="primary"
aria-label="restore"
aria-label={t("Restore")}
size="small"
title={t("Restore Backup")}
>
<RestoreIcon />
</IconButton>
@ -410,7 +398,7 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
color="textSecondary"
align="center"
>
{t("No Backups")}
</Typography>
</Box>
</TableCell>
@ -421,19 +409,126 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
<TablePagination
rowsPerPageOptions={[]}
component="div"
count={backupFiles.length}
count={totalCount}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage=""
onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange}
labelRowsPerPage={t("Rows per page")}
/>
</TableContainer>
</Paper>
</Box>
</BaseDialog>
);
});
}
);
const WebDAVConfigForm = memo(
({
onSubmit,
initialValues,
urlRef,
usernameRef,
passwordRef,
showPassword,
onShowPasswordClick,
webdavChanged,
webdavUrl,
webdavUsername,
webdavPassword,
handleBackup,
register,
}: WebDAVConfigFormProps) => {
const { t } = useTranslation();
return (
<form onSubmit={onSubmit}>
<Grid container spacing={2}>
<Grid item xs={12} sm={9}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label={t("WebDAV Server URL")}
variant="outlined"
size="small"
{...register("url")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={urlRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Username")}
variant="outlined"
size="small"
{...register("username")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={usernameRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Password")}
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={passwordRef}
{...register("password")}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowPasswordClick} edge="end">
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={3}>
<Stack
direction="column"
justifyContent="center"
alignItems="stretch"
sx={{ height: "100%" }}
>
{webdavChanged ||
webdavUrl === null ||
webdavUsername == null ||
webdavPassword == null ? (
<Button
variant="contained"
color="primary"
sx={{ height: "100%" }}
type="submit"
>
{t("Save")}
</Button>
) : (
<Button
variant="contained"
color="success"
sx={{ height: "100%" }}
onClick={handleBackup}
type="button"
>
{t("Backup")}
</Button>
)}
</Stack>
</Grid>
</Grid>
</form>
);
}
);
export function LinuxIcon(props: SVGProps<SVGSVGElement>) {
return (

View File

@ -371,5 +371,24 @@
"Switched to _clash Core": "Switched to {{core}} Core",
"GeoData Updated": "GeoData Updated",
"Currently on the Latest Version": "Currently on the Latest Version",
"Import Subscription Successful": "Import subscription successful"
"Import Subscription Successful": "Import subscription successful",
"WebDAV Server URL": "WebDAV Server URL",
"Username": "Username",
"Password": "Password",
"Backup": "Backup",
"Filename": "Filename",
"Actions": "Actions",
"Restore": "Restore",
"No Backups": "No backups available",
"WebDAV URL Required": "WebDAV URL cannot be empty",
"Invalid WebDAV URL": "Invalid WebDAV URL format",
"Username Required": "Username cannot be empty",
"Password Required": "Password cannot be empty",
"Failed to Fetch Backups": "Failed to fetch backup files",
"WebDAV Config Saved": "WebDAV configuration saved successfully",
"WebDAV Config Save Failed": "Failed to save WebDAV configuration",
"Backup Created": "Backup created successfully",
"Backup Failed": "Failed to create backup",
"Delete Backup": "Delete Backup",
"Restore Backup": "Restore Backup"
}

View File

@ -65,6 +65,8 @@
"Append Rule": "اضافه کردن قانون به انتها",
"Prepend Group": "اضافه کردن گروه به ابتدا",
"Append Group": "اضافه کردن گروه به انتها",
"Prepend Proxy": "پیش‌افزودن پراکسی",
"Append Proxy": "پس‌افزودن پراکسی",
"Rule Condition Required": "شرط قانون الزامی است",
"Invalid Rule": "قانون نامعتبر",
"Advanced": "پیشرفته",
@ -235,6 +237,9 @@
"Auto Launch": "راه‌اندازی خودکار",
"Silent Start": "شروع بی‌صدا",
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"TG Channel": "کانال تلگرام",
"Manual": "راهنما",
"Github Repo": "مخزن GitHub",
"Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN",
"Network Interface": "رابط شبکه",
@ -269,9 +274,6 @@
"Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
"Update GeoData": "به‌روزرسانی GeoData",
"TG Channel": "کانال تلگرام",
"Manual": "راهنما",
"Github Repo": "مخزن GitHub",
"Verge Setting": "تنظیمات Verge",
"Language": "زبان",
"Theme Mode": "حالت تم",
@ -369,5 +371,24 @@
"Switched to _clash Core": "تغییر به هسته {{core}}",
"GeoData Updated": "GeoData به‌روزرسانی شد",
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
"Import Subscription Successfully": "عضویت با موفقیت وارد شد"
"Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد",
"WebDAV Server URL": "آدرس سرور WebDAV",
"Username": "نام کاربری",
"Password": "رمز عبور",
"Backup": "پشتیبان‌گیری",
"Filename": "نام فایل",
"Actions": "عملیات",
"Restore": "بازیابی",
"No Backups": "هیچ پشتیبانی موجود نیست",
"WebDAV URL Required": "آدرس WebDAV نمی‌تواند خالی باشد",
"Invalid WebDAV URL": "فرمت آدرس WebDAV نامعتبر است",
"Username Required": "نام کاربری نمی‌تواند خالی باشد",
"Password Required": "رمز عبور نمی‌تواند خالی باشد",
"Failed to Fetch Backups": "دریافت فایل‌های پشتیبان ناموفق بود",
"WebDAV Config Saved": "پیکربندی WebDAV با موفقیت ذخیره شد",
"WebDAV Config Save Failed": "ذخیره پیکربندی WebDAV ناموفق بود",
"Backup Created": "پشتیبان‌گیری با موفقیت ایجاد شد",
"Backup Failed": "ایجاد پشتیبان ناموفق بود",
"Delete Backup": "حذف پشتیبان",
"Restore Backup": "بازیابی پشتیبان"
}

View File

@ -65,6 +65,8 @@
"Append Rule": "Добавить правило в конец",
"Prepend Group": "Добавить группу в начало",
"Append Group": "Добавить группу в конец",
"Prepend Proxy": "Добавить прокси в начало",
"Append Proxy": "Добавить прокси в конец",
"Rule Condition Required": "Требуется условие правила",
"Invalid Rule": "Недействительное правило",
"Advanced": "Дополнительно",
@ -369,5 +371,24 @@
"Switched to _clash Core": "Переключено на ядра {{core}}",
"GeoData Updated": "GeoData Обновлена",
"Currently on the Latest Version": "В настоящее время используется последняя версия",
"Import subscription successful": "Импорт подписки успешно"
"Import subscription successful": "Импорт подписки успешно",
"WebDAV Server URL": "URL-адрес сервера WebDAV",
"Username": "Имя пользователя",
"Password": "Пароль",
"Backup": "Резервное копирование",
"Filename": "Имя файла",
"Actions": "Действия",
"Restore": "Восстановить",
"No Backups": "Нет доступных резервных копий",
"WebDAV URL Required": "URL-адрес WebDAV не может быть пустым",
"Invalid WebDAV URL": "Неверный формат URL-адреса WebDAV",
"Username Required": "Имя пользователя не может быть пустым",
"Password Required": "Пароль не может быть пустым",
"Failed to Fetch Backups": "Не удалось получить файлы резервных копий",
"WebDAV Config Saved": "Конфигурация WebDAV успешно сохранена",
"WebDAV Config Save Failed": "Не удалось сохранить конфигурацию WebDAV",
"Backup Created": "Резервная копия успешно создана",
"Backup Failed": "Не удалось создать резервную копию",
"Delete Backup": "Удалить резервную копию",
"Restore Backup": "Восстановить резервную копию"
}

View File

@ -371,5 +371,24 @@
"Switched to _clash Core": "已切换至 {{core}} 内核",
"GeoData Updated": "已更新 GeoData",
"Currently on the Latest Version": "当前已是最新版本",
"Import Subscription Successful": "导入订阅成功"
"Import Subscription Successful": "导入订阅成功",
"WebDAV Server URL": "WebDAV 服务器地址",
"Username": "用户名",
"Password": "密码",
"Backup": "备份",
"Filename": "文件名称",
"Actions": "操作",
"Restore": "恢复",
"No Backups": "暂无备份",
"WebDAV URL Required": "WebDAV 地址不能为空",
"Invalid WebDAV URL": "无效的 WebDAV 地址格式",
"Username Required": "用户名不能为空",
"Password Required": "密码不能为空",
"Failed to Fetch Backups": "获取备份文件失败",
"WebDAV Config Saved": "WebDAV 配置保存成功",
"WebDAV Config Save Failed": "WebDAV 配置保存失败",
"Backup Created": "备份创建成功",
"Backup Failed": "备份创建失败",
"Delete Backup": "删除备份",
"Restore Backup": "恢复备份"
}