chore: i18n (#5276)

* chore: notice i18n

* feat: add script to clean up unused i18n keys

* chore: cleanup i18n keys

* refactor(i18n/proxies): migrate proxies UI to structured locale keys

* chore: i18n for rule module

* chore: i18n for profile module

* chore: i18n for connections module

* chore: i18n for settings module

* chore: i18n for verge settings

* chore: i18n for theme settings

* chore: i18n for theme

* chore(i18n): components.home.*

* chore(i18n): remove unused i18n keys

* chore(i18n): components.profile.*

* chore(i18n): components.connection

* chore(i18n): pages.logs.*

* chore(i18n): pages.*.provider

* chore(i18n): components.settings.externalCors.*

* chore(i18n): components.settings.clash.*

* chore(i18n): components.settings.liteMode.*

* chore(i18n): components.settings.backup.*

* chore(i18n): components.settings.clash.port.*

* chore(i18n): components.settings.misc.*

* chore(i18n): components.settings.update.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): components.settings.sysproxy.*

* chore(i18n): pages.profiles.notices/components.providers.notices

* refactor(notice): unify showNotice usage

* refactor(notice): add typed showNotice shortcuts, centralize defaults, and simplify subscriptions

* refactor: unify showNotice usage

* refactor(notice): unify showNotice API

* refactor(notice): unify showNotice usage

* chore(i18n): components.test.*

* chore(i18n): components.settings.dns.*

* chore(i18n): components.home.clashInfo.*

* chore(i18n): components.home.systemInfo.*

* chore(i18n): components.home.ipInfo/traffic.*

* chore(i18n): navigation.*

* refactor(i18n): remove pages.* namespace and migrate route texts under module-level page keys

* chore(i18n): common.*

* chore(i18n): common.*

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

* fix: improve error handling in patch_profiles_config to prevent requests during profile switching

* fix: change error handling in patch_profiles_config to return false when a switch is in progress

fix: ensure CURRENT_SWITCHING_PROFILE is reset after config updates in perform_config_update and patch_profiles_config

* chore(i18n): restructure root-level locale keys into namespaces

* chore(i18n): add missing i18n keys

* docs: i18n guide

* chore: adjust i18n

* refactor(i18n): align UI actions and status labels with common keys

* refactor(i18n): unify two-name locale namespaces

* refactor(i18n/components): unify locale keys and update component references

* chore(i18n): add shared and entities namespaces to all locale files

* refactor(i18n): consolidate shared and entity namespaces across features

* chore(deps): update npm dependencies to ^7.3.5 (#5310)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* refactor(i18n): migrate shared editor modes and consolidate entities namespaces

* tmp

* refactor(i18n): flatten locales and move theme/validation strings

* docs: CONTRIBUTING_i18n.md

* refactor(i18n): restructure feedback and profile namespaces for better organization

* refactor(i18n): unify settings locale structure and update references

* refactor(i18n): reorganize locale keys for home, proxies, rules, connections, logs, unlock, and tests

* refactor(i18n/feedback/layout): unify shared toasts & normalize layout namespace

* refactor(i18n): centralize common UI strings in shared

* refactor(i18n): flatten headers and unify locale schema

* refactor(i18n): consolidate duplicate per-feature translations into shared namespace

* refactor(i18n): split locales into per-namespace files

* style: lint

* refactor(i18n): unify unlock UI translations under tests namespace

* feat(i18n): add type-checked translation keys

* style: eslint import order

* feat(i18n): replace ad-hoc loader with rust-i18n backend bundles

* chore(prebuild): remove locale-copy step

* fix(i18n, notice): propagate runtime params and update cleanup script path

* fix(i18n,notice): make locale formatting idempotent and guard early notice translations

* fix(i18n): resolve locale aliases and match OS codes correctly

* fix(unlock): use i18next-compatible double-brace interpolation in failure notice

* fix(i18n): route unlock error notices through translation keys

* fix(i18n): i18n types

* feat(i18n): localize upgrade notice for Clash core viewer

* fix(notice): ensure runtime overrides apply to prefix translations

* chore(i18n): replace literal notices with translation keys

* chore(i18n): types

* chore(i18n): regen typings before formatting to keep keys in sync

* chore(i18n): simply labels

* chore(i18n): adjust translation

* chore: remove eslint-plugin-i18next

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend (#5341)

* chore(i18n): translate settings.json (missed in previous pass) (#5343)

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* chore(i18n): add/refine Korean translations across frontend scopes and Rust backend

* fix(i18n-tauri): quote placeholder-leading value in ko.yml to prevent rust_i18n parse panic

* chore(i18n): translate settings.json (forgot to include previously)

---------

Co-authored-by: rozan <34974262+thelojan@users.noreply.github.com>
This commit is contained in:
Sline
2025-11-08 19:40:38 +08:00
committed by GitHub
co-authored by renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> rozan
parent c1a2e99f63
commit c8aa72186e
286 changed files with 24243 additions and 8867 deletions
+58 -1
View File
@@ -1,14 +1,17 @@
import { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import {
subscribeNotices,
hideNotice,
getSnapshotNotices,
} from "@/services/noticeService";
import type { TranslationKey } from "@/types/generated/i18n-keys";
export const NoticeManager: React.FC = () => {
const { t } = useTranslation();
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
@@ -60,7 +63,61 @@ export const NoticeManager: React.FC = () => {
</IconButton>
}
>
{notice.message}
{notice.i18n
? (() => {
const params = (notice.i18n.params ?? {}) as Record<
string,
unknown
>;
const {
prefixKey,
prefixParams,
prefix,
message,
...restParams
} = params;
const prefixKeyParams =
prefixParams &&
typeof prefixParams === "object" &&
prefixParams !== null
? (prefixParams as Record<string, unknown>)
: undefined;
const resolvedPrefix =
typeof prefixKey === "string"
? t(prefixKey as TranslationKey, {
defaultValue: prefixKey,
...(prefixKeyParams ?? {}),
...restParams,
})
: typeof prefix === "string"
? prefix
: undefined;
const finalParams: Record<string, unknown> = {
...restParams,
};
if (resolvedPrefix !== undefined) {
finalParams.prefix = resolvedPrefix;
}
if (typeof message === "string") {
finalParams.message = message;
}
const defaultValue =
resolvedPrefix && typeof message === "string"
? `${resolvedPrefix} ${message}`
: typeof message === "string"
? message
: undefined;
return t(notice.i18n.key as TranslationKey, {
defaultValue,
...finalParams,
});
})()
: notice.message}
</Alert>
</Snackbar>
))}
+14 -5
View File
@@ -1,16 +1,25 @@
import { InboxRounded } from "@mui/icons-material";
import { alpha, Box, Typography } from "@mui/material";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import type { TranslationKey } from "@/types/generated/i18n-keys";
interface Props {
text?: React.ReactNode;
extra?: React.ReactNode;
text?: ReactNode;
textKey?: TranslationKey;
extra?: ReactNode;
}
export const BaseEmpty = (props: Props) => {
const { text = "Empty", extra } = props;
export const BaseEmpty = ({
text,
textKey = "shared.statuses.empty",
extra,
}: Props) => {
const { t } = useTranslation();
const resolvedText: ReactNode = text !== undefined ? text : t(textKey);
return (
<Box
sx={({ palette }) => ({
@@ -24,7 +33,7 @@ export const BaseEmpty = (props: Props) => {
})}
>
<InboxRounded sx={{ fontSize: "4em" }} />
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
<Typography sx={{ fontSize: "1.25em" }}>{resolvedText}</Typography>
{extra}
</Box>
);
+6 -6
View File
@@ -135,7 +135,7 @@ export const BaseSearchBox = ({
if (useRegularExpression && value) {
const isValid = validateRegex(value);
if (!isValid) {
setErrorMessage(t("Invalid regular expression"));
setErrorMessage(t("shared.validation.invalidRegex"));
}
}
@@ -156,7 +156,7 @@ export const BaseSearchBox = ({
} else {
const value = inputRef.current?.value ?? "";
if (value && !validateRegex(value)) {
setErrorMessage(t("Invalid regular expression"));
setErrorMessage(t("shared.validation.invalidRegex"));
}
}
return next;
@@ -173,7 +173,7 @@ export const BaseSearchBox = ({
size="small"
variant="outlined"
spellCheck="false"
placeholder={placeholder ?? t("Filter conditions")}
placeholder={placeholder ?? t("shared.placeholders.filter")}
sx={{ input: { py: 0.65, px: 1.25 } }}
onChange={onChange}
error={!!errorMessage}
@@ -182,7 +182,7 @@ export const BaseSearchBox = ({
sx: { pr: 1 },
endAdornment: (
<Box display="flex">
<Tooltip title={t("Match Case")}>
<Tooltip title={t("shared.placeholders.matchCase")}>
<div>
<SvgIcon
component={matchCaseIcon}
@@ -192,7 +192,7 @@ export const BaseSearchBox = ({
/>
</div>
</Tooltip>
<Tooltip title={t("Match Whole Word")}>
<Tooltip title={t("shared.placeholders.matchWholeWord")}>
<div>
<SvgIcon
component={matchWholeWordIcon}
@@ -202,7 +202,7 @@ export const BaseSearchBox = ({
/>
</div>
</Tooltip>
<Tooltip title={t("Use Regular Expression")}>
<Tooltip title={t("shared.placeholders.useRegex")}>
<div>
<SvgIcon
component={useRegularExpressionIcon}
@@ -12,7 +12,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
size="small"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
placeholder={t("shared.placeholders.filter")}
sx={{ input: { py: 0.65, px: 1.25 } }}
{...props}
/>
@@ -177,7 +177,7 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: "error.main" }} />
<Typography variant="h6" gutterBottom>
{t("Traffic Statistics Error")}
{t("shared.feedback.errors.trafficStats")}
</Typography>
<Typography
@@ -186,18 +186,17 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
textAlign="center"
sx={{ mb: 2 }}
>
{t(
"The traffic statistics component encountered an error and has been disabled to prevent crashes.",
)}
{t("shared.feedback.errors.trafficStatsDescription")}
</Typography>
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
<Typography variant="body2">
<strong>Error:</strong> {error?.message || "Unknown error"}
<strong>Error:</strong>{" "}
{error instanceof Error ? error.message : "Unknown error"}
</Typography>
{retryCount > 0 && (
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
{t("Retry attempts")}: {retryCount}/{maxRetries}
{t("shared.labels.retryAttempts")}: {retryCount}/{maxRetries}
</Typography>
)}
</Alert>
@@ -211,12 +210,12 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
onClick={onRetry}
size="small"
>
{t("Retry")}
{t("shared.actions.retry")}
</Button>
)}
<Button variant="outlined" onClick={onRefresh} size="small">
{t("Refresh Page")}
{t("shared.actions.refreshPage")}
</Button>
<Button
@@ -225,7 +224,9 @@ const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
onClick={onToggleDetails}
size="small"
>
{showDetails ? t("Hide Details") : t("Show Details")}
{showDetails
? t("shared.actions.hideDetails")
: t("shared.actions.showDetails")}
</Button>
</Box>
+35 -16
View File
@@ -1,8 +1,8 @@
import { Box, Button, Snackbar, useTheme } from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { t } from "i18next";
import { useImperativeHandle, useState, type Ref } from "react";
import { useTranslation } from "react-i18next";
import { closeConnections } from "tauri-plugin-mihomo-api";
import parseTraffic from "@/utils/parse-traffic";
@@ -55,6 +55,7 @@ interface InnerProps {
}
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
const { t } = useTranslation();
const { metadata, rulePayload } = data;
const theme = useTheme();
const chains = [...data.chains].reverse().join(" / ");
@@ -67,34 +68,52 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
: metadata.remoteDestination;
const information = [
{ label: t("Host"), value: host },
{ label: t("Downloaded"), value: parseTraffic(data.download).join(" ") },
{ label: t("Uploaded"), value: parseTraffic(data.upload).join(" ") },
{ label: t("connections.components.fields.host"), value: host },
{
label: t("DL Speed"),
label: t("shared.labels.downloaded"),
value: parseTraffic(data.download).join(" "),
},
{
label: t("shared.labels.uploaded"),
value: parseTraffic(data.upload).join(" "),
},
{
label: t("connections.components.fields.dlSpeed"),
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
},
{
label: t("UL Speed"),
label: t("connections.components.fields.ulSpeed"),
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
},
{
label: t("Chains"),
label: t("connections.components.fields.chains"),
value: chains,
},
{ label: t("Rule"), value: rule },
{ label: t("connections.components.fields.rule"), value: rule },
{
label: t("Process"),
label: t("connections.components.fields.process"),
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ""}`,
},
{ label: t("Time"), value: dayjs(data.start).fromNow() },
{
label: t("Source"),
label: t("connections.components.fields.time"),
value: dayjs(data.start).fromNow(),
},
{
label: t("connections.components.fields.source"),
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
},
{ label: t("Destination"), value: Destination },
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
{
label: t("connections.components.fields.destination"),
value: Destination,
},
{
label: t("connections.components.fields.destinationPort"),
value: `${metadata.destinationPort}`,
},
{
label: t("connections.components.fields.type"),
value: `${metadata.type}(${metadata.network})`,
},
];
const onDelete = useLockFn(async () => closeConnections(data.id));
@@ -118,13 +137,13 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
<Box sx={{ textAlign: "right" }}>
<Button
variant="contained"
title={t("Close Connection")}
title={t("connections.components.actions.closeConnection")}
onClick={() => {
onDelete();
onClose?.();
}}
>
{t("Close Connection")}
{t("connections.components.actions.closeConnection")}
</Button>
</Box>
</Box>
@@ -9,6 +9,7 @@ import {
} from "@mui/material";
import { useLockFn } from "ahooks";
import dayjs from "dayjs";
import { useTranslation } from "react-i18next";
import { closeConnections } from "tauri-plugin-mihomo-api";
import parseTraffic from "@/utils/parse-traffic";
@@ -33,6 +34,7 @@ export const ConnectionItem = (props: Props) => {
const { value, onShowDetail } = props;
const { id, metadata, chains, start, curUpload, curDownload } = value;
const { t } = useTranslation();
const onDelete = useLockFn(async () => closeConnections(id));
const showTraffic = curUpload! >= 100 || curDownload! >= 100;
@@ -42,7 +44,13 @@ export const ConnectionItem = (props: Props) => {
dense
sx={{ borderBottom: "1px solid var(--divider-color)" }}
secondaryAction={
<IconButton edge="end" color="inherit" onClick={onDelete}>
<IconButton
edge="end"
color="inherit"
onClick={onDelete}
title={t("connections.components.actions.closeConnection")}
aria-label={t("connections.components.actions.closeConnection")}
>
<CloseRounded />
</IconButton>
}
+12 -13
View File
@@ -162,13 +162,13 @@ export const ConnectionTable = (props: Props) => {
return [
{
field: "host",
headerName: t("Host"),
headerName: t("connections.components.fields.host"),
width: columnWidths["host"] || 220,
minWidth: 180,
},
{
field: "download",
headerName: t("Downloaded"),
headerName: t("shared.labels.downloaded"),
width: columnWidths["download"] || 88,
align: "right",
headerAlign: "right",
@@ -176,7 +176,7 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "upload",
headerName: t("Uploaded"),
headerName: t("shared.labels.uploaded"),
width: columnWidths["upload"] || 88,
align: "right",
headerAlign: "right",
@@ -184,7 +184,7 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "dlSpeed",
headerName: t("DL Speed"),
headerName: t("connections.components.fields.dlSpeed"),
width: columnWidths["dlSpeed"] || 88,
align: "right",
headerAlign: "right",
@@ -192,7 +192,7 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "ulSpeed",
headerName: t("UL Speed"),
headerName: t("connections.components.fields.ulSpeed"),
width: columnWidths["ulSpeed"] || 88,
align: "right",
headerAlign: "right",
@@ -200,25 +200,25 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "chains",
headerName: t("Chains"),
headerName: t("connections.components.fields.chains"),
width: columnWidths["chains"] || 340,
minWidth: 180,
},
{
field: "rule",
headerName: t("Rule"),
headerName: t("connections.components.fields.rule"),
width: columnWidths["rule"] || 280,
minWidth: 180,
},
{
field: "process",
headerName: t("Process"),
headerName: t("connections.components.fields.process"),
width: columnWidths["process"] || 220,
minWidth: 180,
},
{
field: "time",
headerName: t("Time"),
headerName: t("connections.components.fields.time"),
width: columnWidths["time"] || 120,
minWidth: 100,
align: "right",
@@ -229,19 +229,19 @@ export const ConnectionTable = (props: Props) => {
},
{
field: "source",
headerName: t("Source"),
headerName: t("connections.components.fields.source"),
width: columnWidths["source"] || 200,
minWidth: 130,
},
{
field: "remoteDestination",
headerName: t("Destination"),
headerName: t("connections.components.fields.destination"),
width: columnWidths["remoteDestination"] || 200,
minWidth: 130,
},
{
field: "type",
headerName: t("Type"),
headerName: t("connections.components.fields.type"),
width: columnWidths["type"] || 160,
minWidth: 100,
},
@@ -250,7 +250,6 @@ export const ConnectionTable = (props: Props) => {
const handleColumnResize = (params: GridColumnResizeParams) => {
const { colDef, width } = params;
console.log("Column resize:", colDef.field, width);
setColumnWidths((prev) => ({
...prev,
[colDef.field]: width,
+6 -6
View File
@@ -32,7 +32,7 @@ export const ClashInfoCard = () => {
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Core Version")}
{t("home.components.clashInfo.fields.coreVersion")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashVersion || "-"}
@@ -41,7 +41,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("System Proxy Address")}
{t("home.components.clashInfo.fields.systemProxyAddress")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{systemProxyAddress}
@@ -50,7 +50,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Mixed Port")}
{t("home.components.clashInfo.fields.mixedPort")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{clashConfig.mixedPort || "-"}
@@ -59,7 +59,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Uptime")}
{t("home.components.clashInfo.fields.uptime")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{formattedUptime}
@@ -68,7 +68,7 @@ export const ClashInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Rules Count")}
{t("home.components.clashInfo.fields.rulesCount")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{rules.length}
@@ -87,7 +87,7 @@ export const ClashInfoCard = () => {
return (
<EnhancedCard
title={t("Clash Info")}
title={t("home.components.clashInfo.title")}
icon={<DeveloperBoardOutlined />}
iconColor="warning"
action={null}
+43 -16
View File
@@ -12,6 +12,31 @@ import { closeAllConnections } from "tauri-plugin-mihomo-api";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-context";
import { patchClashMode } from "@/services/cmds";
import type { TranslationKey } from "@/types/generated/i18n-keys";
const CLASH_MODES = ["rule", "global", "direct"] as const;
type ClashMode = (typeof CLASH_MODES)[number];
const isClashMode = (mode: string): mode is ClashMode =>
(CLASH_MODES as readonly string[]).includes(mode);
const MODE_META: Record<
ClashMode,
{ label: TranslationKey; description: TranslationKey }
> = {
rule: {
label: "home.components.clashMode.labels.rule",
description: "home.components.clashMode.descriptions.rule",
},
global: {
label: "home.components.clashMode.labels.global",
description: "home.components.clashMode.descriptions.global",
},
direct: {
label: "home.components.clashMode.labels.direct",
description: "home.components.clashMode.descriptions.direct",
},
};
export const ClashModeCard = () => {
const { t } = useTranslation();
@@ -19,19 +44,21 @@ export const ClashModeCard = () => {
const { clashConfig, refreshClashConfig } = useAppData();
// 支持的模式列表
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
const modeList = CLASH_MODES;
// 直接使用API返回的模式,不维护本地状态
const currentMode = clashConfig?.mode?.toLowerCase();
const currentModeKey =
typeof currentMode === "string" && isClashMode(currentMode)
? currentMode
: undefined;
const modeDescription = useMemo(() => {
if (typeof currentMode === "string" && currentMode.length > 0) {
return t(
`${currentMode[0].toLocaleUpperCase()}${currentMode.slice(1)} Mode Description`,
);
if (currentModeKey) {
return t(MODE_META[currentModeKey].description);
}
return t("Core communication error");
}, [currentMode, t]);
return t("home.components.clashMode.errors.communication");
}, [currentModeKey, t]);
// 模式图标映射
const modeIcons = useMemo(
@@ -44,8 +71,8 @@ export const ClashModeCard = () => {
);
// 切换模式的处理函数
const onChangeMode = useLockFn(async (mode: string) => {
if (mode === currentMode) return;
const onChangeMode = useLockFn(async (mode: ClashMode) => {
if (mode === currentModeKey) return;
if (verge?.auto_close_connection) {
closeAllConnections();
}
@@ -60,7 +87,7 @@ export const ClashModeCard = () => {
});
// 按钮样式
const buttonStyles = (mode: string) => ({
const buttonStyles = (mode: ClashMode) => ({
cursor: "pointer",
px: 2,
py: 1.2,
@@ -68,8 +95,8 @@ export const ClashModeCard = () => {
alignItems: "center",
justifyContent: "center",
gap: 1,
bgcolor: mode === currentMode ? "primary.main" : "background.paper",
color: mode === currentMode ? "primary.contrastText" : "text.primary",
bgcolor: mode === currentModeKey ? "primary.main" : "background.paper",
color: mode === currentModeKey ? "primary.contrastText" : "text.primary",
borderRadius: 1.5,
transition: "all 0.2s ease-in-out",
position: "relative",
@@ -82,7 +109,7 @@ export const ClashModeCard = () => {
transform: "translateY(1px)",
},
"&::after":
mode === currentMode
mode === currentModeKey
? {
content: '""',
position: "absolute",
@@ -128,7 +155,7 @@ export const ClashModeCard = () => {
{modeList.map((mode) => (
<Paper
key={mode}
elevation={mode === currentMode ? 2 : 0}
elevation={mode === currentModeKey ? 2 : 0}
onClick={() => onChangeMode(mode)}
sx={buttonStyles(mode)}
>
@@ -137,10 +164,10 @@ export const ClashModeCard = () => {
variant="body2"
sx={{
textTransform: "capitalize",
fontWeight: mode === currentMode ? 600 : 400,
fontWeight: mode === currentModeKey ? 600 : 400,
}}
>
{t(mode)}
{t(MODE_META[mode].label)}
</Typography>
</Paper>
))}
+19 -13
View File
@@ -811,11 +811,11 @@ export const CurrentProxyCard = () => {
const getSortTooltip = (): string => {
switch (sortType) {
case 0:
return t("Sort by default");
return t("proxies.page.tooltips.sortDefault");
case 1:
return t("Sort by delay");
return t("proxies.page.tooltips.sortDelay");
case 2:
return t("Sort by name");
return t("proxies.page.tooltips.sortName");
default:
return "";
}
@@ -823,7 +823,7 @@ export const CurrentProxyCard = () => {
return (
<EnhancedCard
title={t("Current Node")}
title={t("home.components.currentProxy.title")}
icon={
<Tooltip
title={
@@ -840,7 +840,9 @@ export const CurrentProxyCard = () => {
iconColor={currentProxy ? "primary" : undefined}
action={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip title={t("Delay check")}>
<Tooltip
title={t("home.components.currentProxy.actions.refreshDelay")}
>
<span>
<IconButton
size="small"
@@ -868,7 +870,7 @@ export const CurrentProxyCard = () => {
sx={{ borderRadius: 1.5 }}
endIcon={<ChevronRight fontSize="small" />}
>
{t("Label-Proxies")}
{t("layout.components.navigation.tabs.proxies")}
</Button>
</Box>
}
@@ -906,7 +908,7 @@ export const CurrentProxyCard = () => {
{isGlobalMode && (
<Chip
size="small"
label={t("Global Mode")}
label={t("home.components.currentProxy.labels.globalMode")}
color="primary"
sx={{ mr: 0.5 }}
/>
@@ -914,7 +916,7 @@ export const CurrentProxyCard = () => {
{isDirectMode && (
<Chip
size="small"
label={t("Direct Mode")}
label={t("home.components.currentProxy.labels.directMode")}
color="success"
sx={{ mr: 0.5 }}
/>
@@ -954,12 +956,14 @@ export const CurrentProxyCard = () => {
size="small"
sx={{ mb: 1.5 }}
>
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
<InputLabel id="proxy-group-select-label">
{t("home.components.currentProxy.labels.group")}
</InputLabel>
<Select
labelId="proxy-group-select-label"
value={state.selection.group}
onChange={handleGroupChange}
label={t("Group")}
label={t("home.components.currentProxy.labels.group")}
disabled={isGlobalMode || isDirectMode}
>
{state.proxyData.groups.map((group) => (
@@ -972,12 +976,14 @@ export const CurrentProxyCard = () => {
{/* 代理节点选择器 */}
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 0 }}>
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
<InputLabel id="proxy-select-label">
{t("home.components.currentProxy.labels.proxy")}
</InputLabel>
<Select
labelId="proxy-select-label"
value={state.selection.proxy}
onChange={handleProxyChange}
label={t("Proxy")}
label={t("home.components.currentProxy.labels.proxy")}
disabled={isDirectMode}
renderValue={renderProxyValue}
MenuProps={{
@@ -1033,7 +1039,7 @@ export const CurrentProxyCard = () => {
) : (
<Box sx={{ textAlign: "center", py: 4 }}>
<Typography variant="body1" color="text.secondary">
{t("No active proxy node")}
{t("home.components.currentProxy.labels.noActiveNode")}
</Typography>
</Box>
)}
@@ -851,7 +851,9 @@ export const EnhancedCanvasTrafficGraph = memo(
// 获取时间范围文本
const getTimeRangeText = useCallback(() => {
return t("{{time}} Minutes", { time: timeRange });
return t("home.components.traffic.patterns.minutes", {
time: timeRange,
});
}, [timeRange, t]);
return (
@@ -934,7 +936,7 @@ export const EnhancedCanvasTrafficGraph = memo(
textAlign: "right",
}}
>
{t("Upload")}
{t("home.components.traffic.legends.upload")}
</Box>
<Box
sx={{
@@ -944,7 +946,7 @@ export const EnhancedCanvasTrafficGraph = memo(
textAlign: "right",
}}
>
{t("Download")}
{t("home.components.traffic.legends.download")}
</Box>
</Box>
@@ -219,42 +219,42 @@ export const EnhancedTrafficStats = () => {
() => [
{
icon: <ArrowUpwardRounded fontSize="small" />,
title: t("Upload Speed"),
title: t("home.components.traffic.metrics.uploadSpeed"),
value: parsedData.up,
unit: `${parsedData.upUnit}/s`,
color: "secondary" as const,
},
{
icon: <ArrowDownwardRounded fontSize="small" />,
title: t("Download Speed"),
title: t("home.components.traffic.metrics.downloadSpeed"),
value: parsedData.down,
unit: `${parsedData.downUnit}/s`,
color: "primary" as const,
},
{
icon: <LinkRounded fontSize="small" />,
title: t("Active Connections"),
title: t("home.components.traffic.metrics.activeConnections"),
value: parsedData.connectionsCount,
unit: "",
color: "success" as const,
},
{
icon: <CloudUploadRounded fontSize="small" />,
title: t("Uploaded"),
title: t("shared.labels.uploaded"),
value: parsedData.uploadTotal,
unit: parsedData.uploadTotalUnit,
color: "secondary" as const,
},
{
icon: <CloudDownloadRounded fontSize="small" />,
title: t("Downloaded"),
title: t("shared.labels.downloaded"),
value: parsedData.downloadTotal,
unit: parsedData.downloadTotalUnit,
color: "primary" as const,
},
{
icon: <MemoryRounded fontSize="small" />,
title: t("Memory Usage"),
title: t("home.components.traffic.metrics.memoryUsage"),
value: parsedData.inuse,
unit: parsedData.inuseUnit,
color: "error" as const,
+10 -10
View File
@@ -111,7 +111,7 @@ const ProfileDetails = ({
noWrap
sx={{ display: "flex", alignItems: "center" }}
>
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
<span style={{ flexShrink: 0 }}>{t("shared.labels.from")}: </span>
{current.home ? (
<Link
component="button"
@@ -186,7 +186,7 @@ const ProfileDetails = ({
sx={{ cursor: "pointer" }}
onClick={onUpdateProfile}
>
{t("Update Time")}:{" "}
{t("shared.labels.updateTime")}:{" "}
<Box component="span" fontWeight="medium">
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
</Box>
@@ -199,7 +199,7 @@ const ProfileDetails = ({
<Stack direction="row" alignItems="center" spacing={1}>
<SpeedOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary">
{t("Used / Total")}:{" "}
{t("shared.labels.usedTotal")}:{" "}
<Box component="span" fontWeight="medium">
{parseTraffic(usedTraffic)} /{" "}
{parseTraffic(current.extra.total)}
@@ -211,7 +211,7 @@ const ProfileDetails = ({
<Stack direction="row" alignItems="center" spacing={1}>
<EventOutlined fontSize="small" color="action" />
<Typography variant="body2" color="text.secondary">
{t("Expire Time")}:{" "}
{t("shared.labels.expireTime")}:{" "}
<Box component="span" fontWeight="medium">
{parseExpire(current.extra.expire)}
</Box>
@@ -266,10 +266,10 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
/>
<Typography variant="h6" gutterBottom>
{t("Import")} {t("Profiles")}
{t("profiles.page.actions.import")} {t("profiles.page.title")}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Click to import subscription")}
{t("profiles.components.card.labels.clickToImport")}
</Typography>
</Box>
);
@@ -296,8 +296,8 @@ export const HomeProfileCard = ({
// 刷新首页数据
refreshAll();
} catch (err: any) {
showNotice("error", err.message || err.toString(), 3000);
} catch (err) {
showNotice.error(err, 3000);
} finally {
setUpdating(false);
}
@@ -310,7 +310,7 @@ export const HomeProfileCard = ({
// 卡片标题
const cardTitle = useMemo(() => {
if (!current) return t("Profiles");
if (!current) return t("profiles.page.title");
if (!current.home) return current.name;
@@ -363,7 +363,7 @@ export const HomeProfileCard = ({
endIcon={<StorageOutlined fontSize="small" />}
sx={{ borderRadius: 1.5 }}
>
{t("Label-Profiles")}
{t("layout.components.navigation.tabs.proxies")}
</Button>
);
}, [current, goToProfiles, t]);
+27 -14
View File
@@ -68,8 +68,12 @@ export const IpInfoCard = () => {
const data = await getIpInfo();
setIpInfo(data);
setCountdown(IP_REFRESH_SECONDS);
} catch (err: any) {
setError(err.message || t("Failed to get IP info"));
} catch (err) {
setError(
err instanceof Error
? err.message
: t("home.components.ipInfo.errors.load"),
);
} finally {
setLoading(false);
}
@@ -114,7 +118,7 @@ export const IpInfoCard = () => {
if (loading) {
return (
<EnhancedCard
title={t("IP Information")}
title={t("home.components.ipInfo.title")}
icon={<LocationOnOutlined />}
iconColor="info"
action={
@@ -137,7 +141,7 @@ export const IpInfoCard = () => {
if (error) {
return (
<EnhancedCard
title={t("IP Information")}
title={t("home.components.ipInfo.title")}
icon={<LocationOnOutlined />}
iconColor="info"
action={
@@ -160,7 +164,7 @@ export const IpInfoCard = () => {
{error}
</Typography>
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
{t("Retry")}
{t("shared.actions.retry")}
</Button>
</Box>
</EnhancedCard>
@@ -170,7 +174,7 @@ export const IpInfoCard = () => {
// 渲染正常数据
return (
<EnhancedCard
title={t("IP Information")}
title={t("home.components.ipInfo.title")}
icon={<LocationOnOutlined />}
iconColor="info"
action={
@@ -222,7 +226,7 @@ export const IpInfoCard = () => {
maxWidth: "100%",
}}
>
{ipInfo?.country || t("Unknown")}
{ipInfo?.country || t("home.components.ipInfo.labels.unknown")}
</Typography>
</Box>
@@ -232,7 +236,7 @@ export const IpInfoCard = () => {
color="text.secondary"
sx={{ flexShrink: 0 }}
>
{t("IP")}:
{t("home.components.ipInfo.labels.ip")}:
</Typography>
<Box
sx={{
@@ -266,20 +270,29 @@ export const IpInfoCard = () => {
</Box>
<InfoItem
label={t("ASN")}
label={t("home.components.ipInfo.labels.asn")}
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
/>
</Box>
{/* 右侧:组织、ISP和位置信息 */}
<Box sx={{ width: "60%", overflow: "auto" }}>
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
<InfoItem
label={t("Location")}
label={t("home.components.ipInfo.labels.isp")}
value={ipInfo?.isp}
/>
<InfoItem
label={t("home.components.ipInfo.labels.org")}
value={ipInfo?.asn_organization}
/>
<InfoItem
label={t("home.components.ipInfo.labels.location")}
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
/>
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
<InfoItem
label={t("home.components.ipInfo.labels.timezone")}
value={ipInfo?.timezone}
/>
</Box>
</Box>
@@ -297,7 +310,7 @@ export const IpInfoCard = () => {
}}
>
<Typography variant="caption">
{t("Auto refresh")}: {countdown}s
{t("home.components.ipInfo.labels.autoRefresh")}: {countdown}s
</Typography>
<Typography
variant="caption"
+16 -12
View File
@@ -147,8 +147,8 @@ export const ProxyTunCard: FC = () => {
const { enable_tun_mode } = verge ?? {};
const handleError = (err: Error) => {
showNotice("error", err.message || err.toString());
const handleError = (err: unknown) => {
showNotice.error(err);
};
const handleTabChange = (tab: string) => {
@@ -160,18 +160,18 @@ export const ProxyTunCard: FC = () => {
if (activeTab === "system") {
return {
text: systemProxyActualState
? t("System Proxy Enabled")
: t("System Proxy Disabled"),
tooltip: t("System Proxy Info"),
? t("home.components.proxyTun.status.systemProxyEnabled")
: t("home.components.proxyTun.status.systemProxyDisabled"),
tooltip: t("home.components.proxyTun.tooltips.systemProxy"),
};
} else {
return {
text: !isTunModeAvailable
? t("TUN Mode Service Required")
? t("home.components.proxyTun.status.tunModeServiceRequired")
: enable_tun_mode
? t("TUN Mode Enabled")
: t("TUN Mode Disabled"),
tooltip: t("TUN Mode Intercept Info"),
? t("home.components.proxyTun.status.tunModeEnabled")
: t("home.components.proxyTun.status.tunModeDisabled"),
tooltip: t("home.components.proxyTun.tooltips.tunMode"),
};
}
}, [
@@ -198,14 +198,14 @@ export const ProxyTunCard: FC = () => {
isActive={activeTab === "system"}
onClick={() => handleTabChange("system")}
icon={ComputerRounded}
label={t("System Proxy")}
label={t("settings.sections.system.toggles.systemProxy")}
hasIndicator={systemProxyActualState}
/>
<TabButton
isActive={activeTab === "tun"}
onClick={() => handleTabChange("tun")}
icon={TroubleshootRounded}
label={t("Tun Mode")}
label={t("settings.sections.system.toggles.tunMode")}
hasIndicator={enable_tun_mode && isTunModeAvailable}
/>
</Stack>
@@ -236,7 +236,11 @@ export const ProxyTunCard: FC = () => {
>
<ProxyControlSwitches
onError={handleError}
label={activeTab === "system" ? t("System Proxy") : t("Tun Mode")}
label={
activeTab === "system"
? t("settings.sections.system.toggles.systemProxy")
: t("settings.sections.system.toggles.tunMode")
}
noRightPadding={true}
/>
</Box>
+32 -22
View File
@@ -174,13 +174,15 @@ export const SystemInfoCard = () => {
try {
const info = await checkUpdate();
if (!info?.available) {
showNotice("success", t("Currently on the Latest Version"));
showNotice.success(
"settings.components.verge.advanced.notifications.latestVersion",
);
} else {
showNotice("info", t("Update Available"), 2000);
showNotice.info("shared.feedback.notifications.updateAvailable", 2000);
goToSettings();
}
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -217,11 +219,11 @@ export const SystemInfoCard = () => {
<>
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
titleAccess={t("home.components.systemInfo.badges.adminMode")}
/>
<DnsOutlined
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
titleAccess={t("Service Mode")}
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
/>
</>
);
@@ -229,21 +231,21 @@ export const SystemInfoCard = () => {
return (
<AdminPanelSettingsOutlined
sx={{ color: "primary.main", fontSize: 16 }}
titleAccess={t("Administrator Mode")}
titleAccess={t("home.components.systemInfo.badges.adminMode")}
/>
);
} else if (isSidecarMode) {
return (
<ExtensionOutlined
sx={{ color: "info.main", fontSize: 16 }}
titleAccess={t("Sidecar Mode")}
titleAccess={t("home.components.systemInfo.badges.sidecarMode")}
/>
);
} else {
return (
<DnsOutlined
sx={{ color: "success.main", fontSize: 16 }}
titleAccess={t("Service Mode")}
titleAccess={t("home.components.systemInfo.badges.serviceMode")}
/>
);
}
@@ -254,13 +256,13 @@ export const SystemInfoCard = () => {
if (isAdminMode) {
// 判断是否同时处于服务模式
if (!isSidecarMode) {
return t("Administrator + Service Mode");
return t("home.components.systemInfo.badges.adminServiceMode");
}
return t("Administrator Mode");
return t("home.components.systemInfo.badges.adminMode");
} else if (isSidecarMode) {
return t("Sidecar Mode");
return t("home.components.systemInfo.badges.sidecarMode");
} else {
return t("Service Mode");
return t("home.components.systemInfo.badges.serviceMode");
}
};
@@ -269,11 +271,15 @@ export const SystemInfoCard = () => {
return (
<EnhancedCard
title={t("System Info")}
title={t("home.components.systemInfo.title")}
icon={<InfoOutlined />}
iconColor="error"
action={
<IconButton size="small" onClick={goToSettings} title={t("Settings")}>
<IconButton
size="small"
onClick={goToSettings}
title={t("home.components.systemInfo.actions.settings")}
>
<SettingsOutlined fontSize="small" />
</IconButton>
}
@@ -281,7 +287,7 @@ export const SystemInfoCard = () => {
<Stack spacing={1.5}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("OS Info")}
{t("home.components.systemInfo.fields.osInfo")}
</Typography>
<Typography variant="body2" fontWeight="medium">
{systemState.osInfo}
@@ -294,19 +300,23 @@ export const SystemInfoCard = () => {
alignItems="center"
>
<Typography variant="body2" color="text.secondary">
{t("Auto Launch")}
{t("home.components.systemInfo.fields.autoLaunch")}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
{isAdminMode && (
<Tooltip
title={t("Administrator mode may not support auto launch")}
title={t("home.components.systemInfo.tooltips.autoLaunchAdmin")}
>
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
</Tooltip>
)}
<Chip
size="small"
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
label={
autoLaunchEnabled
? t("shared.statuses.enabled")
: t("shared.statuses.disabled")
}
color={autoLaunchEnabled ? "success" : "default"}
variant={autoLaunchEnabled ? "filled" : "outlined"}
onClick={toggleAutoLaunch}
@@ -321,7 +331,7 @@ export const SystemInfoCard = () => {
alignItems="center"
>
<Typography variant="body2" color="text.secondary">
{t("Running Mode")}
{t("home.components.systemInfo.fields.runningMode")}
</Typography>
<Typography
variant="body2"
@@ -336,7 +346,7 @@ export const SystemInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Last Check Update")}
{t("home.components.systemInfo.fields.lastCheckUpdate")}
</Typography>
<Typography
variant="body2"
@@ -354,7 +364,7 @@ export const SystemInfoCard = () => {
<Divider />
<Stack direction="row" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">
{t("Verge Version")}
{t("home.components.systemInfo.fields.vergeVersion")}
</Typography>
<Typography variant="body2" fontWeight="medium">
v{appVersion}
+3 -3
View File
@@ -173,16 +173,16 @@ export const TestCard = () => {
return (
<EnhancedCard
title={t("Website Tests")}
title={t("home.components.tests.title")}
icon={<NetworkCheck />}
action={
<Box sx={{ display: "flex", gap: 1 }}>
<Tooltip title={t("Test All")} arrow>
<Tooltip title={t("tests.page.actions.testAll")} arrow>
<IconButton size="small" onClick={handleTestAll}>
<NetworkCheck fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title={t("Create Test")} arrow>
<Tooltip title={t("tests.modals.test.title.create")} arrow>
<IconButton size="small" onClick={handleCreateTest}>
<Add fontSize="small" />
</IconButton>
+3 -3
View File
@@ -87,7 +87,7 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}>
<Box
title={`${t("Upload Speed")}`}
title={`${t("home.components.traffic.metrics.uploadSpeed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
@@ -105,7 +105,7 @@ export const LayoutTraffic = () => {
</Box>
<Box
title={`${t("Download Speed")}`}
title={`${t("home.components.traffic.metrics.downloadSpeed")}`}
{...boxStyle}
sx={{
...boxStyle.sx,
@@ -124,7 +124,7 @@ export const LayoutTraffic = () => {
{displayMemory && (
<Box
title={`${t("Memory Usage")} `}
title={`${t("home.components.traffic.metrics.memoryUsage")} `}
{...boxStyle}
sx={{
cursor: "auto",
+2 -2
View File
@@ -35,10 +35,10 @@ export const ConfirmViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={onConfirm} variant="contained">
{t("Confirm")}
{t("shared.actions.confirm")}
</Button>
</DialogActions>
</Dialog>
+16 -12
View File
@@ -97,7 +97,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
onClose,
} = props;
const resolvedTitle = title ?? t("Edit File");
const resolvedTitle = title ?? t("profiles.components.menu.editFile");
const resolvedInitialData = useMemo(
() => initialData ?? Promise.resolve(""),
[initialData],
@@ -130,8 +130,8 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
try {
currData.current = value;
onChange?.(prevData.current, currData.current);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -141,16 +141,16 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
onSave?.(prevData.current, currData.current);
}
onClose();
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
const handleClose = useLockFn(async () => {
try {
onClose();
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -200,7 +200,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
readOnly: readOnly, // 只读模式
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
readOnlyMessage: {
value: t("profiles.modals.editor.messages.readOnly"),
}, // 只读模式尝试编辑时的提示信息
renderValidationDecorations: "on", // 只读模式下显示校验信息
quickSuggestions: {
strings: true, // 字符串类型的建议
@@ -229,7 +231,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
size="medium"
color="inherit"
sx={{ display: readOnly ? "none" : "" }}
title={t("Format document")}
title={t("profiles.modals.editor.actions.format")}
onClick={() =>
editorRef.current
?.getAction("editor.action.formatDocument")
@@ -241,7 +243,9 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
<IconButton
size="medium"
color="inherit"
title={t(isMaximized ? "Minimize" : "Maximize")}
title={t(
isMaximized ? "shared.window.minimize" : "shared.window.maximize",
)}
onClick={() => appWindow.toggleMaximize().then(editorResize)}
>
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
@@ -251,11 +255,11 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t(readOnly ? "Close" : "Cancel")}
{t(readOnly ? "shared.actions.close" : "shared.actions.cancel")}
</Button>
{!readOnly && (
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
)}
</DialogActions>
+1 -1
View File
@@ -42,7 +42,7 @@ export const FileInput = (props: Props) => {
sx={{ flex: "none" }}
onClick={() => inputRef.current?.click()}
>
{t("Choose File")}
{t("profiles.components.fileInput.chooseFile")}
</Button>
<input
+156 -43
View File
@@ -57,6 +57,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useThemeMode } from "@/services/states";
import type { TranslationKey } from "@/types/generated/i18n-keys";
import getSystem from "@/utils/get-system";
import { BaseSearchBox } from "../base/base-search-box";
@@ -73,6 +74,24 @@ interface Props {
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
const PROXY_STRATEGY_LABEL_KEYS: Record<string, TranslationKey> = {
select: "proxies.components.enums.strategies.select",
"url-test": "proxies.components.enums.strategies.url-test",
fallback: "proxies.components.enums.strategies.fallback",
"load-balance": "proxies.components.enums.strategies.load-balance",
relay: "proxies.components.enums.strategies.relay",
};
const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
builtinProxyPolicies.reduce(
(acc, policy) => {
acc[policy] =
`proxies.components.enums.policies.${policy}` as TranslationKey;
return acc;
},
{} as Record<string, TranslationKey>,
);
const normalizeDeleteSeq = (input?: unknown): string[] => {
if (!Array.isArray(input)) {
return [];
@@ -121,6 +140,20 @@ export const GroupsEditorViewer = (props: Props) => {
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
props;
const { t } = useTranslation();
const translateStrategy = useCallback(
(value: string) =>
PROXY_STRATEGY_LABEL_KEYS[value]
? t(PROXY_STRATEGY_LABEL_KEYS[value])
: value,
[t],
);
const translatePolicy = useCallback(
(value: string) =>
PROXY_POLICY_LABEL_KEYS[value]
? t(PROXY_POLICY_LABEL_KEYS[value])
: value,
[t],
);
const themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
@@ -369,7 +402,7 @@ export const GroupsEditorViewer = (props: Props) => {
const validateGroup = () => {
const group = formIns.getValues();
if (group.name === "") {
throw new Error(t("Group Name Required"));
throw new Error(t("profiles.modals.groupsEditor.errors.nameRequired"));
}
};
@@ -384,12 +417,12 @@ export const GroupsEditorViewer = (props: Props) => {
}
await saveProfileFile(property, nextData);
showNotice("success", t("Saved Successfully"));
showNotice.success("shared.feedback.notifications.saved");
setPrevData(nextData);
onSave?.(prevData, nextData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -398,7 +431,7 @@ export const GroupsEditorViewer = (props: Props) => {
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Groups")}
{t("profiles.modals.groupsEditor.title")}
<Box>
<Button
variant="contained"
@@ -407,7 +440,9 @@ export const GroupsEditorViewer = (props: Props) => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -436,7 +471,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Group Type")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.type")}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
@@ -448,9 +485,10 @@ export const GroupsEditorViewer = (props: Props) => {
"relay",
]}
value={field.value}
getOptionLabel={translateStrategy}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
<li {...props} title={translateStrategy(option)}>
{translateStrategy(option)}
</li>
)}
onChange={(_, value) => value && field.onChange(value)}
@@ -464,7 +502,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Group Name")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.name")}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -481,7 +521,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Proxy Group Icon")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.fields.icon")}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -496,7 +538,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Use Proxies")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.proxies",
)}
/>
<Autocomplete
size="small"
sx={{
@@ -508,10 +554,11 @@ export const GroupsEditorViewer = (props: Props) => {
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
<li {...props} title={translatePolicy(option)}>
{translatePolicy(option)}
</li>
)}
getOptionLabel={translatePolicy}
/>
</Item>
)}
@@ -521,7 +568,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Use Provider")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.provider",
)}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
@@ -539,7 +590,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Health Check Url")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.healthCheckUrl",
)}
/>
<TextField
autoComplete="new-password"
placeholder="https://cp.cloudflare.com/generate_204"
@@ -555,7 +610,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Expected Status")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.expectedStatus",
)}
/>
<TextField
autoComplete="new-password"
placeholder="*"
@@ -573,7 +632,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Interval")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.interval",
)}
/>
<TextField
autoComplete="new-password"
placeholder="300"
@@ -587,7 +650,7 @@ export const GroupsEditorViewer = (props: Props) => {
input: {
endAdornment: (
<InputAdornment position="end">
{t("seconds")}
{t("shared.units.seconds")}
</InputAdornment>
),
},
@@ -601,7 +664,7 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Timeout")} />
<ListItemText primary={t("shared.labels.timeout")} />
<TextField
autoComplete="new-password"
placeholder="5000"
@@ -615,7 +678,7 @@ export const GroupsEditorViewer = (props: Props) => {
input: {
endAdornment: (
<InputAdornment position="end">
{t("millis")}
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
@@ -629,7 +692,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Max Failed Times")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.maxFailedTimes",
)}
/>
<TextField
autoComplete="new-password"
placeholder="5"
@@ -648,7 +715,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Interface Name")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.interfaceName",
)}
/>
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
@@ -665,7 +736,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Routing Mark")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.routingMark",
)}
/>
<TextField
autoComplete="new-password"
type="number"
@@ -683,7 +758,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Filter")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.filter",
)}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -698,7 +777,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Exclude Filter")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.excludeFilter",
)}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -713,7 +796,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Exclude Type")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.excludeType",
)}
/>
<Autocomplete
multiple
options={[
@@ -759,7 +846,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAll",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -769,7 +860,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All Proxies")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAllProxies",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -779,7 +874,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All Providers")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.fields.includeAllProviders",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -789,7 +888,9 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Lazy")} />
<ListItemText
primary={t("profiles.modals.groupsEditor.toggles.lazy")}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -799,7 +900,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Disable UDP")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.toggles.disableUdp",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -809,7 +914,11 @@ export const GroupsEditorViewer = (props: Props) => {
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Hidden")} />
<ListItemText
primary={t(
"profiles.modals.groupsEditor.toggles.hidden",
)}
/>
<Switch checked={field.value} {...field} />
</Item>
)}
@@ -825,16 +934,18 @@ export const GroupsEditorViewer = (props: Props) => {
validateGroup();
for (const item of [...prependSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists"));
throw new Error(
t("profiles.modals.groupsEditor.errors.nameExists"),
);
}
}
setPrependSeq([formIns.getValues(), ...prependSeq]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
}}
>
{t("Prepend Group")}
{t("profiles.modals.groupsEditor.actions.prepend")}
</Button>
</Item>
<Item>
@@ -847,16 +958,18 @@ export const GroupsEditorViewer = (props: Props) => {
validateGroup();
for (const item of [...appendSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists"));
throw new Error(
t("profiles.modals.groupsEditor.errors.nameExists"),
);
}
}
setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
}}
>
{t("Append Group")}
{t("profiles.modals.groupsEditor.actions.append")}
</Button>
</Item>
</List>
@@ -1007,11 +1120,11 @@ export const GroupsEditorViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>
+2 -2
View File
@@ -26,7 +26,7 @@ export const LogViewer = (props: Props) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("Script Console")}</DialogTitle>
<DialogTitle>{t("profiles.modals.logViewer.title")}</DialogTitle>
<DialogContent
sx={{
@@ -62,7 +62,7 @@ export const LogViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Close")}
{t("shared.actions.close")}
</Button>
</DialogActions>
</Dialog>
+133 -46
View File
@@ -36,6 +36,7 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import type { TranslationKey } from "@/types/generated/i18n-keys";
import parseTraffic from "@/utils/parse-traffic";
import { ProfileBox } from "./profile-box";
@@ -120,34 +121,46 @@ export const ProfileItem = (props: Props) => {
// 如果已经过期,显示"更新失败"
if (nextUpdateDate.isBefore(now)) {
setNextUpdateTime(t("Last Update failed"));
setNextUpdateTime(
t("profiles.components.profileItem.status.lastUpdateFailed"),
);
} else {
// 否则显示剩余时间
const diffMinutes = nextUpdateDate.diff(now, "minute");
if (diffMinutes < 60) {
if (diffMinutes <= 0) {
setNextUpdateTime(`${t("Next Up")} <1m`);
setNextUpdateTime(
`${t("profiles.components.profileItem.status.nextUp")} <1m`,
);
} else {
setNextUpdateTime(`${t("Next Up")} ${diffMinutes}m`);
setNextUpdateTime(
`${t("profiles.components.profileItem.status.nextUp")} ${diffMinutes}m`,
);
}
} else {
const hours = Math.floor(diffMinutes / 60);
const mins = diffMinutes % 60;
setNextUpdateTime(`${t("Next Up")} ${hours}h ${mins}m`);
setNextUpdateTime(
`${t("profiles.components.profileItem.status.nextUp")} ${hours}h ${mins}m`,
);
}
}
} else {
console.log(`返回的下次更新时间为空`);
setNextUpdateTime(t("No schedule"));
setNextUpdateTime(
t("profiles.components.profileItem.status.noSchedule"),
);
}
} catch (err) {
console.error(`获取下次更新时间出错:`, err);
setNextUpdateTime(t("Unknown"));
setNextUpdateTime(t("profiles.components.profileItem.status.unknown"));
}
} else {
console.log(`该配置未设置更新间隔或间隔为0`);
setNextUpdateTime(t("Auto update disabled"));
setNextUpdateTime(
t("profiles.components.profileItem.status.autoUpdateDisabled"),
);
}
});
@@ -312,8 +325,8 @@ export const ProfileItem = (props: Props) => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
showNotice("error", err?.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -354,42 +367,95 @@ export const ProfileItem = (props: Props) => {
}
});
const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
).concat([
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
type ContextMenuItem = {
label: string;
handler: () => void;
disabled: boolean;
};
const menuLabels: Record<string, TranslationKey> = {
home: "profiles.components.menu.home",
select: "profiles.components.menu.select",
editInfo: "profiles.components.menu.editInfo",
editFile: "profiles.components.menu.editFile",
editRules: "profiles.components.menu.editRules",
editProxies: "profiles.components.menu.editProxies",
editGroups: "profiles.components.menu.editGroups",
extendConfig: "profiles.components.menu.extendConfig",
extendScript: "profiles.components.menu.extendScript",
openFile: "profiles.components.menu.openFile",
update: "profiles.components.menu.update",
updateViaProxy: "profiles.components.menu.updateViaProxy",
delete: "shared.actions.delete",
} as const;
const urlModeMenu: ContextMenuItem[] = [
...(hasHome
? [
{
label: menuLabels.home,
handler: onOpenHome,
disabled: false,
} satisfies ContextMenuItem,
]
: []),
{
label: "Edit Rules",
label: menuLabels.select,
handler: onForceSelect,
disabled: false,
},
{
label: menuLabels.editInfo,
handler: onEditInfo,
disabled: false,
},
{
label: menuLabels.editFile,
handler: onEditFile,
disabled: false,
},
{
label: menuLabels.editRules,
handler: onEditRules,
disabled: !option?.rules,
},
{
label: "Edit Proxies",
label: menuLabels.editProxies,
handler: onEditProxies,
disabled: !option?.proxies,
},
{
label: "Edit Groups",
label: menuLabels.editGroups,
handler: onEditGroups,
disabled: !option?.groups,
},
{
label: "Extend Config",
label: menuLabels.extendConfig,
handler: onEditMerge,
disabled: !option?.merge,
},
{
label: "Extend Script",
label: menuLabels.extendScript,
handler: onEditScript,
disabled: !option?.script,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{ label: "Update", handler: () => onUpdate(0), disabled: false },
{ label: "Update via proxy", handler: () => onUpdate(2), disabled: false },
{
label: "Delete",
label: menuLabels.openFile,
handler: onOpenFile,
disabled: false,
},
{
label: menuLabels.update,
handler: () => onUpdate(0),
disabled: false,
},
{
label: menuLabels.updateViaProxy,
handler: () => onUpdate(2),
disabled: false,
},
{
label: menuLabels.delete,
handler: () => {
setAnchorEl(null);
if (batchMode) {
@@ -403,39 +469,55 @@ export const ProfileItem = (props: Props) => {
},
disabled: false,
},
]);
const fileModeMenu = [
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
];
const fileModeMenu: ContextMenuItem[] = [
{
label: "Edit Rules",
label: menuLabels.select,
handler: onForceSelect,
disabled: false,
},
{
label: menuLabels.editInfo,
handler: onEditInfo,
disabled: false,
},
{
label: menuLabels.editFile,
handler: onEditFile,
disabled: false,
},
{
label: menuLabels.editRules,
handler: onEditRules,
disabled: !option?.rules,
},
{
label: "Edit Proxies",
label: menuLabels.editProxies,
handler: onEditProxies,
disabled: !option?.proxies,
},
{
label: "Edit Groups",
label: menuLabels.editGroups,
handler: onEditGroups,
disabled: !option?.groups,
},
{
label: "Extend Config",
label: menuLabels.extendConfig,
handler: onEditMerge,
disabled: !option?.merge,
},
{
label: "Extend Script",
label: menuLabels.extendScript,
handler: onEditScript,
disabled: !option?.script,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{
label: "Delete",
label: menuLabels.openFile,
handler: onOpenFile,
disabled: false,
},
{
label: menuLabels.delete,
handler: () => {
setAnchorEl(null);
if (batchMode) {
@@ -599,7 +681,7 @@ export const ProfileItem = (props: Props) => {
{/* only if has url can it be updated */}
{hasUrl && (
<IconButton
title={t("Refresh")}
title={t("shared.actions.refresh")}
sx={{
position: "absolute",
p: "3px",
@@ -637,7 +719,10 @@ export const ProfileItem = (props: Props) => {
</Typography>
) : (
hasUrl && (
<Typography noWrap title={`${t("From")} ${from}`}>
<Typography
noWrap
title={`${t("shared.labels.from")} ${from}`}
>
{from}
</Typography>
)
@@ -657,8 +742,8 @@ export const ProfileItem = (props: Props) => {
textAlign="right"
title={
showNextUpdate
? t("Click to show last update time")
: `${t("Update Time")}: ${parseExpire(updated)}\n${t("Click to show next update")}`
? t("profiles.components.profileItem.tooltips.showLast")
: `${t("shared.labels.updateTime")}: ${parseExpire(updated)}\n${t("profiles.components.profileItem.tooltips.showNext")}`
}
sx={{
cursor: "pointer",
@@ -686,14 +771,16 @@ export const ProfileItem = (props: Props) => {
{/* the third line show extra info or last updated time */}
{hasExtra ? (
<Box sx={{ ...boxStyle, fontSize: 14 }}>
<span title={t("Used / Total")}>
<span title={t("shared.labels.usedTotal")}>
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
<span title={t("Expire Time")}>{expire}</span>
<span title={t("shared.labels.expireTime")}>{expire}</span>
</Box>
) : (
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
<span title={t("Update Time")}>{parseExpire(updated)}</span>
<span title={t("shared.labels.updateTime")}>
{parseExpire(updated)}
</span>
</Box>
)}
<LinearProgress
@@ -728,7 +815,7 @@ export const ProfileItem = (props: Props) => {
(theme) => {
return {
color:
item.label === "Delete"
item.label === menuLabels.delete
? theme.palette.error.main
: undefined,
};
@@ -813,8 +900,8 @@ export const ProfileItem = (props: Props) => {
)}
<ConfirmViewer
title={t("Confirm deletion")}
message={t("This operation is not reversible")}
title={t("profiles.modals.confirmDelete.title")}
message={t("profiles.modals.confirmDelete.message")}
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
+20 -10
View File
@@ -47,16 +47,26 @@ export const ProfileMore = (props: Props) => {
setAnchorEl(null);
try {
await viewProfile(id);
} catch (err: any) {
showNotice("error", err?.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
const hasError = entries.some(([level]) => level === "exception");
const globalTitles: Record<Props["id"], string> = {
Merge: "profiles.components.more.global.merge",
Script: "profiles.components.more.global.script",
};
const chipLabels: Record<Props["id"], string> = {
Merge: "profiles.components.more.chips.merge",
Script: "profiles.components.more.chips.script",
};
const itemMenu = [
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "profiles.components.menu.editFile", handler: onEditFile },
{ label: "profiles.components.menu.openFile", handler: onOpenFile },
];
const boxStyle = {
@@ -89,13 +99,13 @@ export const ProfileMore = (props: Props) => {
variant="h6"
component="h2"
noWrap
title={t(`Global ${id}`)}
title={t(globalTitles[id])}
>
{t(`Global ${id}`)}
{t(globalTitles[id])}
</Typography>
<Chip
label={id}
label={t(chipLabels[id])}
color="primary"
size="small"
variant="outlined"
@@ -111,7 +121,7 @@ export const ProfileMore = (props: Props) => {
size="small"
edge="start"
color="error"
title={t("Script Console")}
title={t("profiles.modals.logViewer.title")}
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
@@ -122,7 +132,7 @@ export const ProfileMore = (props: Props) => {
size="small"
edge="start"
color="inherit"
title={t("Script Console")}
title={t("profiles.modals.logViewer.title")}
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
@@ -170,7 +180,7 @@ export const ProfileMore = (props: Props) => {
{fileOpen && (
<EditorViewer
open={true}
title={`${t("Global " + id)}`}
title={t(globalTitles[id])}
initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined}
+46 -24
View File
@@ -144,9 +144,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
}
} catch {
// 首次创建/更新失败,尝试使用自身代理
showNotice(
"info",
t("Profile creation failed, retrying with Clash proxy..."),
showNotice.info(
"profiles.modals.profileForm.feedback.notifications.creationRetry",
);
// 使用自身代理的配置
@@ -170,9 +169,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
await patchProfile(form.uid, { option: originalOptions });
}
showNotice(
"success",
t("Profile creation succeeded with Clash proxy"),
showNotice.success(
"profiles.modals.profileForm.feedback.notifications.creationSuccess",
);
}
}
@@ -186,8 +184,8 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
setTimeout(() => {
onChange(isActivating);
}, 0);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
} finally {
setLoading(false);
}
@@ -220,10 +218,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
return (
<BaseDialog
open={open}
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
title={
openType === "new"
? t("profiles.modals.profileForm.title.create")
: t("profiles.modals.profileForm.title.edit")
}
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={handleClose}
onCancel={handleClose}
onOk={handleOk}
@@ -234,8 +236,14 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
<InputLabel>{t("Type")}</InputLabel>
<Select {...field} autoFocus label={t("Type")}>
<InputLabel>
{t("profiles.modals.profileForm.fields.type")}
</InputLabel>
<Select
{...field}
autoFocus
label={t("profiles.modals.profileForm.fields.type")}
>
<MenuItem value="remote">Remote</MenuItem>
<MenuItem value="local">Local</MenuItem>
</Select>
@@ -247,7 +255,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
name="name"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("Name")} />
<TextField {...text} {...field} label={t("shared.labels.name")} />
)}
/>
@@ -255,7 +263,11 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
name="desc"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("Descriptions")} />
<TextField
{...text}
{...field}
label={t("profiles.modals.profileForm.fields.description")}
/>
)}
/>
@@ -269,7 +281,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{...text}
{...field}
multiline
label={t("Subscription URL")}
label={t("profiles.modals.profileForm.fields.subscriptionUrl")}
/>
)}
/>
@@ -296,12 +308,12 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{...field}
type="number"
placeholder="60"
label={t("HTTP Request Timeout")}
label={t("profiles.modals.profileForm.fields.httpTimeout")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
{t("seconds")}
{t("shared.units.seconds")}
</InputAdornment>
),
},
@@ -321,11 +333,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
{...text}
{...field}
type="number"
label={t("Update Interval")}
label={t("profiles.modals.profileForm.fields.updateInterval")}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("mins")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.minutes")}
</InputAdornment>
),
},
}}
@@ -350,7 +364,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Use System Proxy")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.useSystemProxy")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -361,7 +377,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.useClashProxy")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -372,7 +390,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.acceptInvalidCerts")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -383,7 +403,9 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
control={control}
render={({ field }) => (
<StyledBox>
<InputLabel>{t("Allow Auto Update")}</InputLabel>
<InputLabel>
{t("profiles.modals.profileForm.fields.allowAutoUpdate")}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
</StyledBox>
)}
@@ -163,11 +163,11 @@ export const ProxiesEditorViewer = (props: Props) => {
proxies.push(proxy);
names.push(proxy.name);
}
} catch (err: any) {
} catch (err) {
console.warn(
"[ProxiesEditorViewer] parseUri failed for line:",
uri,
err?.message || err,
err,
);
// 不阻塞主流程
}
@@ -263,11 +263,11 @@ export const ProxiesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice.success("shared.feedback.notifications.saved");
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -276,7 +276,7 @@ export const ProxiesEditorViewer = (props: Props) => {
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Proxies")}
{t("profiles.modals.proxiesEditor.title")}
<Box>
<Button
variant="contained"
@@ -285,7 +285,9 @@ export const ProxiesEditorViewer = (props: Props) => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -312,7 +314,9 @@ export const ProxiesEditorViewer = (props: Props) => {
<Item>
<TextField
autoComplete="new-password"
placeholder={t("Use newlines for multiple uri")}
placeholder={t(
"profiles.modals.proxiesEditor.placeholders.multiUri",
)}
fullWidth
rows={9}
multiline
@@ -332,7 +336,7 @@ export const ProxiesEditorViewer = (props: Props) => {
});
}}
>
{t("Prepend Proxy")}
{t("profiles.modals.proxiesEditor.actions.prepend")}
</Button>
</Item>
<Item>
@@ -346,7 +350,7 @@ export const ProxiesEditorViewer = (props: Props) => {
});
}}
>
{t("Append Proxy")}
{t("profiles.modals.proxiesEditor.actions.append")}
</Button>
</Item>
</List>
@@ -497,11 +501,11 @@ export const ProxiesEditorViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>
+70 -29
View File
@@ -240,8 +240,24 @@ const rules: {
},
];
const RULE_TYPE_LABEL_KEYS: Record<string, string> = Object.fromEntries(
rules.map((rule) => [
rule.name,
`rules.modals.editor.ruleTypes.${rule.name}`,
]),
);
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
const PROXY_POLICY_LABEL_KEYS: Record<string, string> =
builtinProxyPolicies.reduce(
(acc, policy) => {
acc[policy] = `proxy.policies.${policy}`;
return acc;
},
{} as Record<string, string>,
);
export const RulesEditorViewer = (props: Props) => {
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
props;
@@ -350,8 +366,8 @@ export const RulesEditorViewer = (props: Props) => {
{ forceQuotes: true },
),
);
} catch (e: any) {
showNotice("error", e?.message || e?.toString() || "YAML dump error");
} catch (error) {
showNotice.error(error ?? "YAML dump error");
}
};
let idleId: number | undefined;
@@ -462,10 +478,12 @@ export const RulesEditorViewer = (props: Props) => {
const validateRule = () => {
if ((ruleType.required ?? true) && !ruleContent) {
throw new Error(t("Rule Condition Required"));
throw new Error(
t("rules.modals.editor.form.validation.conditionRequired"),
);
}
if (ruleType.validator && !ruleType.validator(ruleContent)) {
throw new Error(t("Invalid Rule"));
throw new Error(t("rules.modals.editor.form.validation.invalidRule"));
}
const condition = (ruleType.required ?? true) ? ruleContent : "";
@@ -477,11 +495,11 @@ export const RulesEditorViewer = (props: Props) => {
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
showNotice("success", t("Saved Successfully"));
showNotice.success("shared.feedback.notifications.saved");
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
showNotice("error", err.toString());
showNotice.error(err);
}
});
@@ -490,7 +508,7 @@ export const RulesEditorViewer = (props: Props) => {
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Rules")}
{t("rules.modals.editor.title")}
<Box>
<Button
variant="contained"
@@ -499,7 +517,9 @@ export const RulesEditorViewer = (props: Props) => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -518,26 +538,37 @@ export const RulesEditorViewer = (props: Props) => {
}}
>
<Item>
<ListItemText primary={t("Rule Type")} />
<ListItemText
primary={t("rules.modals.editor.form.labels.type")}
/>
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={rules}
value={ruleType}
getOptionLabel={(option) => option.name}
renderOption={(props, option) => (
<li {...props} title={t(option.name)}>
{option.name}
</li>
)}
getOptionLabel={(option) =>
t(RULE_TYPE_LABEL_KEYS[option.name] ?? option.name)
}
renderOption={(props, option) => {
const label = t(
RULE_TYPE_LABEL_KEYS[option.name] ?? option.name,
);
return (
<li {...props} title={label}>
{label}
</li>
);
}}
onChange={(_, value) => value && setRuleType(value)}
/>
</Item>
<Item
sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
>
<ListItemText primary={t("Rule Content")} />
<ListItemText
primary={t("rules.modals.editor.form.labels.content")}
/>
{ruleType.name === "RULE-SET" && (
<Autocomplete
@@ -574,24 +605,34 @@ export const RulesEditorViewer = (props: Props) => {
)}
</Item>
<Item>
<ListItemText primary={t("Proxy Policy")} />
<ListItemText
primary={t("rules.modals.editor.form.labels.proxyPolicy")}
/>
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={proxyPolicyList}
value={proxyPolicy}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
getOptionLabel={(option) =>
t(PROXY_POLICY_LABEL_KEYS[option] ?? option)
}
renderOption={(props, option) => {
const label = t(PROXY_POLICY_LABEL_KEYS[option] ?? option);
return (
<li {...props} title={label}>
{label}
</li>
);
}}
onChange={(_, value) => value && setProxyPolicy(value)}
/>
</Item>
{ruleType.noResolve && (
<Item>
<ListItemText primary={t("No Resolve")} />
<ListItemText
primary={t("rules.modals.editor.form.toggles.noResolve")}
/>
<Switch
checked={noResolve}
onChange={() => setNoResolve(!noResolve)}
@@ -609,11 +650,11 @@ export const RulesEditorViewer = (props: Props) => {
if (prependSeq.includes(raw)) return;
setPrependSeq([raw, ...prependSeq]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
}}
>
{t("Prepend Rule")}
{t("rules.modals.editor.form.actions.prependRule")}
</Button>
</Item>
<Item>
@@ -627,11 +668,11 @@ export const RulesEditorViewer = (props: Props) => {
if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
}}
>
{t("Append Rule")}
{t("rules.modals.editor.form.actions.appendRule")}
</Button>
</Item>
</List>
@@ -776,11 +817,11 @@ export const RulesEditorViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
{t("shared.actions.cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>
+34 -17
View File
@@ -66,12 +66,17 @@ export const ProviderButton = () => {
await refreshProxy();
await refreshProxyProviders();
showNotice("success", `${name} 更新成功`);
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
showNotice.success(
"proxies.feedback.notifications.provider.updateSuccess",
{
name,
},
);
} catch (err) {
showNotice.error("proxies.feedback.notifications.provider.updateFailed", {
name,
message: String(err),
});
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
@@ -84,7 +89,7 @@ export const ProviderButton = () => {
// 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的代理提供者");
showNotice.info("proxies.feedback.notifications.provider.none");
return;
}
@@ -114,9 +119,11 @@ export const ProviderButton = () => {
await refreshProxy();
await refreshProxyProviders();
showNotice("success", "全部代理提供者更新成功");
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
showNotice.success("proxies.feedback.notifications.provider.allUpdated");
} catch (err) {
showNotice.error("proxies.feedback.notifications.provider.genericError", {
message: String(err),
});
} finally {
// 清除所有更新状态
setUpdating({});
@@ -138,7 +145,7 @@ export const ProviderButton = () => {
onClick={() => setOpen(true)}
sx={{ mr: 1 }}
>
{t("Proxy Provider")}
{t("proxies.page.provider.title")}
</Button>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
@@ -148,14 +155,17 @@ export const ProviderButton = () => {
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Typography variant="h6">
{t("proxies.page.provider.title")}
</Typography>
<Box>
<Button
variant="contained"
size="small"
onClick={updateAllProviders}
aria-label={t("proxies.page.provider.actions.updateAll")}
>
{t("Update All")}
{t("proxies.page.provider.actions.updateAll")}
</Button>
</Box>
</Box>
@@ -246,7 +256,7 @@ export const ProviderButton = () => {
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
<small>{t("shared.labels.updateAt")}: </small>
{time.fromNow()}
</Typography>
</Box>
@@ -264,11 +274,17 @@ export const ProviderButton = () => {
justifyContent: "space-between",
}}
>
<span title={t("Used / Total") as string}>
<span
title={t("shared.labels.usedTotal") as string}
>
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title={t("Expire Time") as string}>
<span
title={
t("shared.labels.expireTime") as string
}
>
{parseExpire(expire)}
</span>
</Box>
@@ -313,7 +329,8 @@ export const ProviderButton = () => {
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
title={t("proxies.page.provider.actions.update")}
aria-label={t("proxies.page.provider.actions.update")}
>
<RefreshRounded />
</IconButton>
@@ -326,7 +343,7 @@ export const ProviderButton = () => {
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}
{t("shared.actions.close")}
</Button>
</DialogActions>
</Dialog>
+20 -17
View File
@@ -156,7 +156,11 @@ const SortableItem = ({ proxy, index, onRemove }: SortableItemProps) => {
{proxy.delay !== undefined && (
<Chip
label={proxy.delay > 0 ? `${proxy.delay}ms` : t("timeout") || "超时"}
label={
proxy.delay > 0
? `${proxy.delay}ms`
: t("shared.labels.timeout") || "超时"
}
size="small"
color={
proxy.delay > 0 && proxy.delay < 200
@@ -299,7 +303,7 @@ export const ProxyChain = ({
// onUpdateChain([]);
} catch (error) {
console.error("Failed to disconnect from proxy chain:", error);
alert(t("Failed to disconnect from proxy chain") || "断开链式代理失败");
alert(t("proxies.page.chain.disconnectFailed") || "断开链式代理失败");
} finally {
setIsConnecting(false);
}
@@ -307,9 +311,7 @@ export const ProxyChain = ({
}
if (proxyChain.length < 2) {
alert(
t("Chain proxy requires at least 2 nodes") || "链式代理至少需要2个节点",
);
alert(t("proxies.page.chain.minimumNodes") || "链式代理至少需要2个节点");
return;
}
@@ -341,7 +343,7 @@ export const ProxyChain = ({
console.log("Successfully connected to proxy chain");
} catch (error) {
console.error("Failed to connect to proxy chain:", error);
alert(t("Failed to connect to proxy chain") || "连接链式代理失败");
alert(t("proxies.page.chain.connectFailed") || "连接链式代理失败");
} finally {
setIsConnecting(false);
}
@@ -471,7 +473,7 @@ export const ProxyChain = ({
mb: 2,
}}
>
<Typography variant="h6">{t("Chain Proxy Config")}</Typography>
<Typography variant="h6">{t("proxies.page.chain.header")}</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{proxyChain.length > 0 && (
<IconButton
@@ -486,7 +488,9 @@ export const ProxyChain = ({
backgroundColor: theme.palette.error.light + "20",
},
}}
title={t("Delete Chain Config") || "删除链式配置"}
title={
t("proxies.page.actions.clearChainConfig") || "删除链式配置"
}
>
<DeleteIcon fontSize="small" />
</IconButton>
@@ -507,16 +511,16 @@ export const ProxyChain = ({
}}
title={
proxyChain.length < 2
? t("Chain proxy requires at least 2 nodes") ||
? t("proxies.page.chain.minimumNodes") ||
"链式代理至少需要2个节点"
: undefined
}
>
{isConnecting
? t("Connecting...") || "连接中..."
? t("proxies.page.actions.connecting") || "连接中..."
: isConnected
? t("Disconnect") || "断开"
: t("Connect") || "连接"}
? t("proxies.page.actions.disconnect") || "断开"
: t("proxies.page.actions.connect") || "连接"}
</Button>
</Box>
</Box>
@@ -526,10 +530,9 @@ export const ProxyChain = ({
sx={{ mb: 2 }}
>
{proxyChain.length === 1
? t(
"Chain proxy requires at least 2 nodes. Please add one more node.",
) || "链式代理至少需要2个节点,请再添加一个节点。"
: t("Click nodes in order to add to proxy chain") ||
? t("proxies.page.chain.minimumNodesHint") ||
"链式代理至少需要2个节点,请再添加一个节点。"
: t("proxies.page.chain.instruction") ||
"按顺序点击节点添加到代理链中"}
</Alert>
@@ -544,7 +547,7 @@ export const ProxyChain = ({
color: theme.palette.text.secondary,
}}
>
<Typography>{t("No proxy chain configured")}</Typography>
<Typography>{t("proxies.page.chain.empty")}</Typography>
</Box>
) : (
<DndContext
+4 -4
View File
@@ -239,7 +239,7 @@ export const ProxyGroups = (props: Props) => {
setProxyChain((prev) => {
// 检查是否已经存在相同名称的代理,防止重复添加
if (prev.some((item) => item.name === proxy.name)) {
const warningMessage = t("Proxy node already exists in chain");
const warningMessage = t("proxies.page.chain.duplicateNode");
setDuplicateWarning({
open: true,
message: warningMessage,
@@ -372,7 +372,7 @@ export const ProxyGroups = (props: Props) => {
}, [renderList]);
if (mode === "direct") {
return <BaseEmpty text={t("clash_mode_direct")} />;
return <BaseEmpty textKey="proxies.page.messages.directMode" />;
}
if (isChainMode) {
@@ -403,7 +403,7 @@ export const ProxyGroups = (props: Props) => {
variant="h6"
sx={{ fontWeight: 600, fontSize: "16px" }}
>
{t("Proxy Rules")}
{t("proxies.page.rules.title")}
</Typography>
{currentGroup && (
<Box
@@ -442,7 +442,7 @@ export const ProxyGroups = (props: Props) => {
variant="body2"
sx={{ mr: 0.5, fontSize: "12px" }}
>
{t("Select Rules")}
{t("proxies.page.rules.select")}
</Typography>
<ExpandMoreRounded fontSize="small" />
</IconButton>
+16 -10
View File
@@ -67,7 +67,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("locate")}
title={t("proxies.page.tooltips.locate")}
onClick={onLocation}
>
<MyLocationRounded />
@@ -76,7 +76,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("Delay check")}
title={t("proxies.page.tooltips.delayCheck")}
onClick={() => {
console.log(`[ProxyHead] 点击延迟测试按钮,组: ${groupName}`);
// Remind the user that it is custom test url
@@ -94,9 +94,11 @@ export const ProxyHead = ({
size="small"
color="inherit"
title={
[t("Sort by default"), t("Sort by delay"), t("Sort by name")][
sortType
]
[
t("proxies.page.tooltips.sortDefault"),
t("proxies.page.tooltips.sortDelay"),
t("proxies.page.tooltips.sortName"),
][sortType]
}
onClick={() =>
onHeadState({ sortType: ((sortType + 1) % 3) as ProxySortType })
@@ -110,7 +112,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("Delay check URL")}
title={t("proxies.page.tooltips.delayCheckUrl")}
onClick={() =>
onHeadState({ textState: textState === "url" ? null : "url" })
}
@@ -125,7 +127,11 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={showType ? t("Proxy basic") : t("Proxy detail")}
title={
showType
? t("proxies.page.tooltips.showBasic")
: t("proxies.page.tooltips.showDetail")
}
onClick={() => onHeadState({ showType: !showType })}
>
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
@@ -134,7 +140,7 @@ export const ProxyHead = ({
<IconButton
size="small"
color="inherit"
title={t("Filter")}
title={t("proxies.page.tooltips.filter")}
onClick={() =>
onHeadState({ textState: textState === "filter" ? null : "filter" })
}
@@ -154,7 +160,7 @@ export const ProxyHead = ({
value={filterText}
size="small"
variant="outlined"
placeholder={t("Filter conditions")}
placeholder={t("shared.placeholders.filter")}
onChange={(e) => onHeadState({ filterText: e.target.value })}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>
@@ -169,7 +175,7 @@ export const ProxyHead = ({
value={testUrl}
size="small"
variant="outlined"
placeholder={t("Delay check URL")}
placeholder={t("proxies.page.placeholders.delayCheckUrl")}
onChange={(e) => onHeadState({ testUrl: e.target.value })}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>
+3 -1
View File
@@ -271,7 +271,9 @@ export const ProxyItemMini = (props: Props) => {
<span
className={proxy.name === group.now ? "the-pin" : "the-unpin"}
title={
group.type === "URLTest" ? t("Delay check to cancel fixed") : ""
group.type === "URLTest"
? t("proxies.page.labels.delayCheckReset")
: ""
}
>
📌
+1 -1
View File
@@ -160,7 +160,7 @@ export const ProxyRender = (props: RenderProps) => {
}}
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("Proxy Count")} arrow>
<Tooltip title={t("proxies.page.labels.proxyCount")} arrow>
<Chip
size="small"
label={`${group.all.length}`}
+25 -15
View File
@@ -58,12 +58,17 @@ export const ProviderButton = () => {
await refreshRules();
await refreshRuleProviders();
showNotice("success", `${name} 更新成功`);
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
showNotice.success(
"rules.feedback.notifications.provider.updateSuccess",
{
name,
},
);
} catch (err) {
showNotice.error("rules.feedback.notifications.provider.updateFailed", {
name,
message: String(err),
});
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
@@ -76,7 +81,7 @@ export const ProviderButton = () => {
// 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的规则提供者");
showNotice.info("rules.feedback.notifications.provider.none");
return;
}
@@ -106,9 +111,11 @@ export const ProviderButton = () => {
await refreshRules();
await refreshRuleProviders();
showNotice("success", "全部规则提供者更新成功");
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
showNotice.success("rules.feedback.notifications.provider.allUpdated");
} catch (err) {
showNotice.error("rules.feedback.notifications.provider.genericError", {
message: String(err),
});
} finally {
// 清除所有更新状态
setUpdating({});
@@ -129,7 +136,7 @@ export const ProviderButton = () => {
startIcon={<StorageOutlined />}
onClick={() => setOpen(true)}
>
{t("Rule Provider")}
{t("rules.page.provider.trigger")}
</Button>
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
@@ -139,13 +146,15 @@ export const ProviderButton = () => {
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">{t("Rule Providers")}</Typography>
<Typography variant="h6">
{t("rules.page.provider.dialogTitle")}
</Typography>
<Button
variant="contained"
size="small"
onClick={updateAllProviders}
>
{t("Update All")}
{t("rules.page.provider.actions.updateAll")}
</Button>
</Box>
</DialogTitle>
@@ -216,7 +225,7 @@ export const ProviderButton = () => {
color="text.secondary"
noWrap
>
<small>{t("Update At")}: </small>
<small>{t("shared.labels.updateAt")}: </small>
{time.fromNow()}
</Typography>
</Box>
@@ -246,6 +255,7 @@ export const ProviderButton = () => {
color="primary"
onClick={() => updateProvider(key)}
disabled={isUpdating}
aria-label={t("rules.page.provider.actions.update")}
sx={{
animation: isUpdating
? "spin 1s linear infinite"
@@ -255,7 +265,7 @@ export const ProviderButton = () => {
"100%": { transform: "rotate(360deg)" },
},
}}
title={t("Update Provider") as string}
title={t("rules.page.provider.actions.update")}
>
<RefreshRounded />
</IconButton>
@@ -268,7 +278,7 @@ export const ProviderButton = () => {
<DialogActions>
<Button onClick={handleClose} variant="outlined">
{t("Close")}
{t("shared.actions.close")}
</Button>
</DialogActions>
</Dialog>
@@ -84,22 +84,22 @@ export const BackupConfigViewer = memo(
if (!url) {
urlRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("WebDAV URL Required"));
showNotice.error("settings.modals.backup.messages.webdavUrlRequired");
throw new Error(t("settings.modals.backup.messages.webdavUrlRequired"));
} else if (!isValidUrl(url)) {
urlRef.current?.focus();
showNotice("error", t("Invalid WebDAV URL"));
throw new Error(t("Invalid WebDAV URL"));
showNotice.error("settings.modals.backup.messages.invalidWebdavUrl");
throw new Error(t("settings.modals.backup.messages.invalidWebdavUrl"));
}
if (!username) {
usernameRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Username Required"));
showNotice.error("settings.modals.backup.messages.usernameRequired");
throw new Error(t("settings.modals.backup.messages.usernameRequired"));
}
if (!password) {
passwordRef.current?.focus();
showNotice("error", t("WebDAV URL Required"));
throw new Error(t("Password Required"));
showNotice.error("settings.modals.backup.messages.passwordRequired");
throw new Error(t("settings.modals.backup.messages.passwordRequired"));
}
};
@@ -112,11 +112,17 @@ export const BackupConfigViewer = memo(
data.username.trim(),
data.password,
).then(() => {
showNotice("success", t("WebDAV Config Saved"));
showNotice.success(
"settings.modals.backup.messages.webdavConfigSaved",
);
onSaveSuccess();
});
} catch (error) {
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
showNotice.error(
"settings.modals.backup.messages.webdavConfigSaveFailed",
{ error },
3000,
);
} finally {
setLoading(false);
}
@@ -127,11 +133,13 @@ export const BackupConfigViewer = memo(
try {
setLoading(true);
await createWebdavBackup().then(async () => {
showNotice("success", t("Backup Created"));
showNotice.success("settings.modals.backup.messages.backupCreated");
await onBackupSuccess();
});
} catch (error) {
showNotice("error", t("Backup Failed", { error }));
showNotice.error("settings.modals.backup.messages.backupFailed", {
error,
});
} finally {
setLoading(false);
}
@@ -145,7 +153,7 @@ export const BackupConfigViewer = memo(
<Grid size={{ xs: 12 }}>
<TextField
fullWidth
label={t("WebDAV Server URL")}
label={t("settings.modals.backup.fields.webdavUrl")}
variant="outlined"
size="small"
{...register("url")}
@@ -157,7 +165,7 @@ export const BackupConfigViewer = memo(
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
label={t("Username")}
label={t("settings.modals.backup.fields.username")}
variant="outlined"
size="small"
{...register("username")}
@@ -169,7 +177,7 @@ export const BackupConfigViewer = memo(
</Grid>
<Grid size={{ xs: 6 }}>
<TextField
label={t("Password")}
label={t("shared.labels.password")}
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
@@ -214,7 +222,7 @@ export const BackupConfigViewer = memo(
type="button"
onClick={handleSubmit(save)}
>
{t("Save")}
{t("shared.actions.save")}
</Button>
) : (
<>
@@ -225,7 +233,7 @@ export const BackupConfigViewer = memo(
type="button"
size="large"
>
{t("Backup")}
{t("settings.modals.backup.actions.backup")}
</Button>
<Button
variant="outlined"
@@ -233,7 +241,7 @@ export const BackupConfigViewer = memo(
type="button"
size="large"
>
{t("Refresh")}
{t("shared.actions.refresh")}
</Button>
</>
)}
@@ -75,7 +75,7 @@ export const BackupTableViewer = memo(
const handleRestore = useLockFn(async (filename: string) => {
await onRestore(filename).then(() => {
showNotice("success", t("Restore Success, App will restart in 1s"));
showNotice.success("settings.modals.backup.messages.restoreSuccess");
});
await restartApp();
});
@@ -92,10 +92,14 @@ export const BackupTableViewer = memo(
return;
}
await onExport(filename, savePath);
showNotice("success", t("Local Backup Exported"));
showNotice.success(
"settings.modals.backup.messages.localBackupExported",
);
} catch (error) {
console.error(error);
showNotice("error", t("Local Backup Export Failed"));
showNotice.error(
"settings.modals.backup.messages.localBackupExportFailed",
);
}
});
@@ -104,9 +108,15 @@ export const BackupTableViewer = memo(
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Filename")}</TableCell>
<TableCell>{t("Backup Time")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
<TableCell>
{t("settings.modals.backup.table.filename")}
</TableCell>
<TableCell>
{t("settings.modals.backup.table.backupTime")}
</TableCell>
<TableCell align="right">
{t("settings.modals.backup.table.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -140,9 +150,13 @@ export const BackupTableViewer = memo(
<>
<IconButton
color="primary"
aria-label={t("Export")}
aria-label={t(
"settings.modals.backup.actions.export",
)}
size="small"
title={t("Export Backup")}
title={t(
"settings.modals.backup.actions.exportBackup",
)}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
await handleExport(file.filename);
@@ -159,13 +173,17 @@ export const BackupTableViewer = memo(
)}
<IconButton
color="secondary"
aria-label={t("Delete")}
aria-label={t("shared.actions.delete")}
size="small"
title={t("Delete Backup")}
title={t(
"settings.modals.backup.actions.deleteBackup",
)}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await confirmAsync(
t("Confirm to delete this backup file?"),
t(
"settings.modals.backup.messages.confirmDelete",
),
);
if (confirmed) {
await handleDelete(file.filename);
@@ -181,14 +199,20 @@ export const BackupTableViewer = memo(
/>
<IconButton
color="primary"
aria-label={t("Restore")}
aria-label={t(
"settings.modals.backup.actions.restore",
)}
size="small"
title={t("Restore Backup")}
title={t(
"settings.modals.backup.actions.restoreBackup",
)}
disabled={!file.allow_apply}
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await confirmAsync(
t("Confirm to restore this backup file?"),
t(
"settings.modals.backup.messages.confirmRestore",
),
);
if (confirmed) {
await handleRestore(file.filename);
@@ -219,7 +243,7 @@ export const BackupTableViewer = memo(
color="textSecondary"
align="center"
>
{t("No Backups")}
{t("settings.modals.backup.table.noBackups")}
</Typography>
</Box>
</TableCell>
@@ -234,7 +258,7 @@ export const BackupTableViewer = memo(
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
page={page}
onPageChange={onPageChange}
labelRowsPerPage={t("Rows per page")}
labelRowsPerPage={t("settings.modals.backup.table.rowsPerPage")}
/>
</TableContainer>
);
@@ -127,7 +127,6 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
setBackupFiles([]);
setTotal(0);
console.error(error);
// Notice.error(t("Failed to fetch backup files"));
} finally {
setIsLoading(false);
}
@@ -247,7 +246,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
return (
<BaseDialog
open={open}
title={t("Backup Setting")}
title={t("settings.modals.backup.title")}
contentSx={{
minWidth: { xs: 320, sm: 620 },
maxWidth: "unset",
@@ -278,11 +277,14 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Tabs
value={source}
onChange={handleChangeSource}
aria-label={t("Select Backup Target")}
aria-label={t("settings.modals.backup.actions.selectTarget")}
sx={{ mb: 2 }}
>
<Tab value="local" label={t("Local Backup")} />
<Tab value="webdav" label={t("WebDAV Backup")} />
<Tab value="local" label={t("settings.modals.backup.tabs.local")} />
<Tab
value="webdav"
label={t("settings.modals.backup.tabs.webdav")}
/>
</Tabs>
{source === "local" ? (
<LocalBackupActions
@@ -342,7 +344,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
backgroundColor: (theme) => theme.palette.background.paper,
}}
>
{t("Close")}
{t("shared.actions.close")}
</Button>
</Box>,
dialogPaper,
@@ -24,8 +24,16 @@ import { changeClashCore, restartCore } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
const VALID_CORE = [
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
{ name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" },
{
name: "Mihomo",
core: "verge-mihomo",
chipKey: "settings.modals.clashCore.variants.release",
},
{
name: "Mihomo Alpha",
core: "verge-mihomo-alpha",
chipKey: "settings.modals.clashCore.variants.alpha",
},
];
export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
@@ -54,7 +62,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
const errorMsg = await changeClashCore(core);
if (errorMsg) {
showNotice("error", errorMsg);
showNotice.error(errorMsg);
setChangingCore(null);
return;
}
@@ -65,9 +73,9 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
mutate("getVersion");
setChangingCore(null);
}, 500);
} catch (err: any) {
} catch (err) {
setChangingCore(null);
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -75,11 +83,13 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
try {
setRestarting(true);
await restartCore();
showNotice("success", t(`Clash Core Restarted`));
showNotice.success(
t("settings.feedback.notifications.clash.restartSuccess"),
);
setRestarting(false);
} catch (err: any) {
} catch (err) {
setRestarting(false);
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -88,14 +98,16 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
setUpgrading(true);
await upgradeCore();
setUpgrading(false);
showNotice("success", t(`Core Version Updated`));
showNotice.success(
t("settings.feedback.notifications.clash.versionUpdated"),
);
} catch (err: any) {
setUpgrading(false);
const errMsg = err.response?.data?.message || err.toString();
const errMsg = err?.response?.data?.message ?? String(err);
const showMsg = errMsg.includes("already using latest version")
? "Already Using Latest Core Version"
? t("settings.feedback.notifications.clash.alreadyLatestVersion")
: errMsg;
showNotice("error", t(showMsg));
showNotice.info(showMsg);
}
});
@@ -104,7 +116,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Clash Core")}
{t("settings.sections.clash.form.fields.clashCore")}
<Box>
<LoadingButton
variant="contained"
@@ -116,7 +128,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
sx={{ marginRight: "8px" }}
onClick={onUpgrade}
>
{t("Upgrade")}
{t("shared.actions.upgrade")}
</LoadingButton>
<LoadingButton
variant="contained"
@@ -127,7 +139,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
disabled={upgrading}
onClick={onRestart}
>
{t("Restart")}
{t("shared.actions.restart")}
</LoadingButton>
</Box>
</Box>
@@ -141,7 +153,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
marginTop: "-8px",
}}
disableOk
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -157,7 +169,7 @@ export function ClashCoreViewer({ ref }: { ref?: Ref<DialogRef> }) {
{changingCore === each.core ? (
<CircularProgress size={20} sx={{ mr: 1 }} />
) : (
<Chip label={t(`${each.chip}`)} size="small" />
<Chip label={t(each.chipKey)} size="small" />
)}
</ListItemButton>
))}
@@ -69,11 +69,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Port settings saved"));
showNotice.success("settings.modals.clashPort.messages.saved");
},
onError: (e) => {
showNotice("error", e.message || t("Failed to save port settings"));
// showNotice("error", t("Failed to save port settings"));
onError: (error) => {
showNotice.error(
"settings.modals.clashPort.messages.saveFailed",
error,
);
},
},
);
@@ -150,7 +152,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
return (
<BaseDialog
open={open}
title={t("Port Config")}
title={t("settings.modals.clashPort.title")}
contentSx={{
width: 400,
}}
@@ -158,13 +160,13 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
loading ? (
<Stack direction="row" alignItems="center" spacing={1}>
<CircularProgress size={20} />
{t("Saving...")}
{t("shared.statuses.saving")}
</Stack>
) : (
t("Save")
t("shared.actions.save")
)
}
cancelBtn={t("Cancel")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -172,7 +174,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<List sx={{ width: "100%" }}>
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Mixed Port")}
primary={t("settings.modals.clashPort.fields.mixed")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -188,7 +190,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setMixedPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
sx={{ mr: 0.5 }}
>
<Shuffle fontSize="small" />
@@ -204,7 +206,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Socks Port")}
primary={t("settings.modals.clashPort.fields.socks")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -221,7 +223,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setSocksPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!socksEnabled}
sx={{ mr: 0.5 }}
>
@@ -238,7 +240,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Http Port")}
primary={t("settings.modals.clashPort.fields.http")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -255,7 +257,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setHttpPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!httpEnabled}
sx={{ mr: 0.5 }}
>
@@ -273,7 +275,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
{OS !== "windows" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Redir Port")}
primary={t("settings.modals.clashPort.fields.redir")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -290,7 +292,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setRedirPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!redirEnabled}
sx={{ mr: 0.5 }}
>
@@ -309,7 +311,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
{OS === "linux" && (
<ListItem sx={{ padding: "4px 0", minHeight: 36 }}>
<ListItemText
primary={t("Tproxy Port")}
primary={t("settings.modals.clashPort.fields.tproxy")}
slotProps={{ primary: { sx: { fontSize: 12 } } }}
/>
<div style={{ display: "flex", alignItems: "center" }}>
@@ -326,7 +328,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((_, ref) => {
<IconButton
size="small"
onClick={() => setTproxyPort(generateRandomPort())}
title={t("Random Port")}
title={t("settings.modals.clashPort.actions.random")}
disabled={!tproxyEnabled}
sx={{ mr: 0.5 }}
>
@@ -27,8 +27,8 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
open={true}
title={
<Box display="flex" alignItems="center" gap={2}>
{t("Runtime Config")}
<Chip label={t("ReadOnly")} size="small" />
{t("settings.components.verge.advanced.fields.runtimeConfig")}
<Chip label={t("shared.labels.readOnly")} size="small" />
</Box>
}
initialData={Promise.resolve(runtimeConfig)}
@@ -56,12 +56,16 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
// 如果启用了外部控制器,则保存控制器地址和密钥
if (enableController) {
if (!controller.trim()) {
showNotice("error", t("Controller address cannot be empty"));
showNotice.error(
"settings.sections.externalController.messages.addressRequired",
);
return;
}
if (!secret.trim()) {
showNotice("error", t("Secret cannot be empty"));
showNotice.error(
"settings.sections.externalController.messages.secretRequired",
);
return;
}
@@ -71,12 +75,12 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
await patchInfo({ "external-controller": "" });
}
showNotice("success", t("Configuration saved successfully"));
showNotice.success("shared.feedback.notifications.common.saveSuccess");
setOpen(false);
} catch (err: any) {
showNotice(
"error",
err.message || t("Failed to save configuration"),
} catch (err) {
showNotice.error(
"shared.feedback.notifications.common.saveFailed",
err,
4000,
);
} finally {
@@ -93,7 +97,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
setTimeout(() => setCopySuccess(null));
} catch (err) {
console.warn("[ControllerViewer] copy to clipboard failed:", err);
showNotice("error", t("Failed to copy"));
showNotice.error(
"settings.sections.externalController.messages.copyFailed",
);
}
},
);
@@ -101,19 +107,19 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
return (
<BaseDialog
open={open}
title={t("External Controller")}
title={t("settings.sections.externalController.title")}
contentSx={{ width: 400 }}
okBtn={
isSaving ? (
<Box display="flex" alignItems="center" gap={1}>
<CircularProgress size={16} color="inherit" />
{t("Saving...")}
{t("shared.statuses.saving")}
</Box>
) : (
t("Save")
t("shared.actions.save")
)
}
cancelBtn={t("Cancel")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -126,7 +132,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Enable External Controller")} />
<ListItemText
primary={t("settings.sections.externalController.fields.enable")}
/>
<Switch
edge="end"
checked={enableController}
@@ -142,7 +150,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
justifyContent: "space-between",
}}
>
<ListItemText primary={t("External Controller")} />
<ListItemText
primary={t("settings.sections.externalController.fields.address")}
/>
<Box display="flex" alignItems="center" gap={1}>
<TextField
size="small"
@@ -152,11 +162,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
pointerEvents: enableController ? "auto" : "none",
}}
value={controller}
placeholder="Required"
placeholder={t(
"settings.sections.externalController.placeholders.address",
)}
onChange={(e) => setController(e.target.value)}
disabled={isSaving || !enableController}
/>
<Tooltip title={t("Copy to clipboard")}>
<Tooltip
title={t("settings.sections.externalController.tooltips.copy")}
>
<IconButton
size="small"
onClick={() => handleCopyToClipboard(controller, "controller")}
@@ -176,7 +190,9 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
justifyContent: "space-between",
}}
>
<ListItemText primary={t("Core Secret")} />
<ListItemText
primary={t("settings.sections.externalController.fields.secret")}
/>
<Box display="flex" alignItems="center" gap={1}>
<TextField
size="small"
@@ -186,11 +202,15 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
pointerEvents: enableController ? "auto" : "none",
}}
value={secret}
placeholder={t("Recommended")}
placeholder={t(
"settings.sections.externalController.placeholders.secret",
)}
onChange={(e) => setSecret(e.target.value)}
disabled={isSaving || !enableController}
/>
<Tooltip title={t("Copy to clipboard")}>
<Tooltip
title={t("settings.sections.externalController.tooltips.copy")}
>
<IconButton
size="small"
onClick={() => handleCopyToClipboard(secret, "secret")}
@@ -211,8 +231,10 @@ export function ControllerViewer({ ref }: { ref?: Ref<DialogRef> }) {
>
<Alert severity="success">
{copySuccess === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard")}
? t(
"settings.sections.externalController.messages.controllerCopied",
)
: t("settings.sections.externalController.messages.secretCopied")}
</Alert>
</Snackbar>
</BaseDialog>
+86 -58
View File
@@ -424,9 +424,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
skipYamlSyncRef.current = true;
updateValuesFromConfig(parsedYaml);
} catch {
showNotice("error", t("Invalid YAML format"));
showNotice.error("settings.modals.dns.errors.invalidYaml");
}
}, [yamlContent, t, updateValuesFromConfig]);
}, [yamlContent, updateValuesFromConfig]);
useEffect(() => {
if (skipYamlSyncRef.current) {
@@ -509,7 +509,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
// 使用YAML编辑器的值
const parsedConfig = yaml.load(yamlContent);
if (typeof parsedConfig !== "object" || parsedConfig === null) {
throw new Error(t("Invalid configuration"));
throw new Error(t("settings.modals.dns.errors.invalid"));
}
config = parsedConfig as Record<string, any>;
}
@@ -547,9 +547,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
}
showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
showNotice.error(
"settings.modals.dns.messages.configError",
cleanErrorMsg,
);
return;
}
@@ -561,9 +561,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
setOpen(false);
showNotice("success", t("DNS settings saved"));
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.success("settings.modals.dns.messages.saved");
} catch (err) {
showNotice.error(err);
}
});
@@ -613,7 +613,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between" alignItems="center">
{t("DNS Overwrite")}
{t("settings.modals.dns.dialog.title")}
<Box display="flex" alignItems="center" gap={1}>
<Button
variant="outlined"
@@ -622,7 +622,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
startIcon={<RestartAltRounded />}
onClick={resetToDefaults}
>
{t("Reset to Default")}
{t("shared.actions.resetToDefault")}
</Button>
<Button
variant="contained"
@@ -631,7 +631,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
{visualization
? t("shared.editorModes.advanced")
: t("shared.editorModes.visualization")}
</Button>
</Box>
</Box>
@@ -643,8 +645,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? {}
: { padding: "0 24px", display: "flex", flexDirection: "column" }),
}}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -655,7 +657,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
color="warning.main"
sx={{ mb: 2, mt: 0, fontStyle: "italic" }}
>
{t("DNS Settings Warning")}
{t("settings.modals.dns.dialog.warning")}
</Typography>
{visualization ? (
@@ -664,11 +666,11 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="subtitle1"
sx={{ mt: 1, mb: 1, fontWeight: "bold" }}
>
{t("DNS Settings")}
{t("settings.modals.dns.sections.general")}
</Typography>
<Item>
<ListItemText primary={t("Enable DNS")} />
<ListItemText primary={t("settings.modals.dns.fields.enable")} />
<Switch
edge="end"
checked={values.enable}
@@ -677,7 +679,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("DNS Listen")} />
<ListItemText primary={t("settings.modals.dns.fields.listen")} />
<TextField
size="small"
autoComplete="off"
@@ -689,7 +691,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("Enhanced Mode")} />
<ListItemText
primary={t("settings.modals.dns.fields.enhancedMode")}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.enhancedMode}
@@ -702,7 +706,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("Fake IP Range")} />
<ListItemText
primary={t("settings.modals.dns.fields.fakeIpRange")}
/>
<TextField
size="small"
autoComplete="off"
@@ -714,7 +720,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("Fake IP Filter Mode")} />
<ListItemText
primary={t("settings.modals.dns.fields.fakeIpFilterMode")}
/>
<FormControl size="small" sx={{ width: 150 }}>
<Select
value={values.fakeIpFilterMode}
@@ -728,8 +736,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("IPv6")}
secondary={t("Enable IPv6 DNS resolution")}
primary={t("settings.modals.dns.fields.ipv6.label")}
secondary={t("settings.modals.dns.fields.ipv6.description")}
/>
<Switch
edge="end"
@@ -740,8 +748,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Prefer H3")}
secondary={t("DNS DOH使用HTTP/3")}
primary={t("settings.modals.dns.fields.preferH3.label")}
secondary={t("settings.modals.dns.fields.preferH3.description")}
/>
<Switch
edge="end"
@@ -752,8 +760,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Respect Rules")}
secondary={t("DNS connections follow routing rules")}
primary={t("settings.modals.dns.fields.respectRules.label")}
secondary={t(
"settings.modals.dns.fields.respectRules.description",
)}
/>
<Switch
edge="end"
@@ -764,8 +774,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Use Hosts")}
secondary={t("Enable to resolve hosts through hosts file")}
primary={t("settings.modals.dns.fields.useHosts.label")}
secondary={t("settings.modals.dns.fields.useHosts.description")}
/>
<Switch
edge="end"
@@ -776,8 +786,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Use System Hosts")}
secondary={t("Enable to resolve hosts through system hosts file")}
primary={t("settings.modals.dns.fields.useSystemHosts.label")}
secondary={t(
"settings.modals.dns.fields.useSystemHosts.description",
)}
/>
<Switch
edge="end"
@@ -788,8 +800,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item>
<ListItemText
primary={t("Direct Nameserver Follow Policy")}
secondary={t("Whether to follow nameserver policy")}
primary={t("settings.modals.dns.fields.directPolicy.label")}
secondary={t(
"settings.modals.dns.fields.directPolicy.description",
)}
/>
<Switch
edge="end"
@@ -800,8 +814,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Default Nameserver")}
secondary={t("Default DNS servers used to resolve DNS servers")}
primary={t("settings.modals.dns.fields.defaultNameserver.label")}
secondary={t(
"settings.modals.dns.fields.defaultNameserver.description",
)}
/>
<TextField
fullWidth
@@ -817,8 +833,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Nameserver")}
secondary={t("List of DNS servers")}
primary={t("settings.modals.dns.fields.nameserver.label")}
secondary={t("settings.modals.dns.fields.nameserver.description")}
/>
<TextField
fullWidth
@@ -834,8 +850,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback")}
secondary={t("List of fallback DNS servers")}
primary={t("settings.modals.dns.fields.fallback.label")}
secondary={t("settings.modals.dns.fields.fallback.description")}
/>
<TextField
fullWidth
@@ -851,8 +867,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Proxy Server Nameserver")}
secondary={t("Proxy Node Nameserver")}
primary={t("settings.modals.dns.fields.proxy.label")}
secondary={t("settings.modals.dns.fields.proxy.description")}
/>
<TextField
fullWidth
@@ -868,8 +884,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Direct Nameserver")}
secondary={t("Direct outbound Nameserver")}
primary={t("settings.modals.dns.fields.directNameserver.label")}
secondary={t(
"settings.modals.dns.fields.directNameserver.description",
)}
/>
<TextField
fullWidth
@@ -885,8 +903,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fake IP Filter")}
secondary={t("Domains that skip fake IP resolution")}
primary={t("settings.modals.dns.fields.fakeIpFilter.label")}
secondary={t(
"settings.modals.dns.fields.fakeIpFilter.description",
)}
/>
<TextField
fullWidth
@@ -902,8 +922,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Nameserver Policy")}
secondary={t("Domain-specific DNS server")}
primary={t("settings.modals.dns.fields.nameserverPolicy.label")}
secondary={t(
"settings.modals.dns.fields.nameserverPolicy.description",
)}
/>
<TextField
fullWidth
@@ -921,13 +943,15 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="subtitle2"
sx={{ mt: 2, mb: 1, fontWeight: "bold" }}
>
{t("Fallback Filter Settings")}
{t("settings.modals.dns.sections.fallbackFilter")}
</Typography>
<Item>
<ListItemText
primary={t("GeoIP Filtering")}
secondary={t("Enable GeoIP filtering for fallback")}
primary={t("settings.modals.dns.fields.geoipFiltering.label")}
secondary={t(
"settings.modals.dns.fields.geoipFiltering.description",
)}
/>
<Switch
edge="end"
@@ -937,7 +961,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
</Item>
<Item>
<ListItemText primary={t("GeoIP Code")} />
<ListItemText primary={t("settings.modals.dns.fields.geoipCode")} />
<TextField
size="small"
autoComplete="off"
@@ -950,8 +974,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback IP CIDR")}
secondary={t("IP CIDRs not using fallback servers")}
primary={t("settings.modals.dns.fields.fallbackIpCidr.label")}
secondary={t(
"settings.modals.dns.fields.fallbackIpCidr.description",
)}
/>
<TextField
fullWidth
@@ -967,8 +993,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Fallback Domain")}
secondary={t("Domains using fallback servers")}
primary={t("settings.modals.dns.fields.fallbackDomain.label")}
secondary={t(
"settings.modals.dns.fields.fallbackDomain.description",
)}
/>
<TextField
fullWidth
@@ -987,13 +1015,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="subtitle1"
sx={{ mt: 3, mb: 0, fontWeight: "bold" }}
>
{t("Hosts Settings")}
{t("settings.modals.dns.sections.hosts")}
</Typography>
<Item sx={{ flexDirection: "column", alignItems: "flex-start" }}>
<ListItemText
primary={t("Hosts")}
secondary={t("Custom domain to IP or domain mapping")}
primary={t("settings.modals.dns.fields.hosts.label")}
secondary={t("settings.modals.dns.fields.hosts.description")}
/>
<TextField
fullWidth
@@ -140,10 +140,12 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
manual: true,
onSuccess: () => {
setOpen(false);
showNotice("success", t("Configuration saved successfully"));
showNotice.success(
"shared.feedback.notifications.common.saveSuccess",
);
},
onError: () => {
showNotice("error", t("Failed to save configuration"));
showNotice.error("shared.feedback.notifications.common.saveFailed");
},
},
);
@@ -181,10 +183,10 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
return (
<BaseDialog
open={open}
title={t("External Cors Configuration")}
title={t("settings.sections.externalCors.title")}
contentSx={{ width: 500 }}
okBtn={loading ? t("Saving...") : t("Save")}
cancelBtn={t("Cancel")}
okBtn={loading ? t("shared.statuses.saving") : t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={handleSave}
@@ -198,7 +200,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
width="100%"
>
<span style={{ fontWeight: "normal" }}>
{t("Allow private network access")}
{t("settings.sections.externalCors.fields.allowPrivateNetwork")}
</span>
<Switch
edge="end"
@@ -218,7 +220,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
<ListItem sx={{ padding: "8px 0" }}>
<div style={{ width: "100%" }}>
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
{t("Allowed Origins")}
{t("settings.sections.externalCors.fields.allowedOrigins")}
</div>
{originEntries.map(({ origin, index, key }) => (
<div
@@ -235,7 +237,9 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
sx={{ fontSize: 14, marginRight: 2 }}
value={origin}
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
placeholder={t("Please enter a valid url")}
placeholder={t(
"settings.sections.externalCors.placeholders.origin",
)}
inputProps={{ style: { fontSize: 14 } }}
/>
<Button
@@ -256,7 +260,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
onClick={handleAddOrigin}
sx={addButtonStyle}
>
{t("Add")}
{t("settings.sections.externalCors.actions.add")}
</Button>
<div
@@ -270,7 +274,7 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
<div
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
>
{t("Always included origins: {{urls}}", {
{t("settings.sections.externalCors.messages.alwaysIncluded", {
urls: DEV_URLS.join(", "),
})}
</div>
+1 -1
View File
@@ -102,7 +102,7 @@ export const HotkeyInput = (props: Props) => {
<IconButton
size="small"
title={t("Delete")}
title={t("shared.actions.delete")}
color="inherit"
onClick={() => {
onChange([]);
+22 -8
View File
@@ -24,7 +24,19 @@ const HOTKEY_FUNC = [
"toggle_system_proxy",
"toggle_tun_mode",
"entry_lightweight_mode",
];
] as const;
const HOTKEY_FUNC_LABELS: Record<(typeof HOTKEY_FUNC)[number], string> = {
open_or_close_dashboard:
"settings.modals.hotkey.functions.openOrCloseDashboard",
clash_mode_rule: "settings.modals.hotkey.functions.rule",
clash_mode_global: "settings.modals.hotkey.functions.global",
clash_mode_direct: "settings.modals.hotkey.functions.direct",
toggle_system_proxy: "settings.modals.hotkey.functions.toggleSystemProxy",
toggle_tun_mode: "settings.modals.hotkey.functions.toggleTunMode",
entry_lightweight_mode:
"settings.modals.hotkey.functions.entryLightweightMode",
};
export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -81,24 +93,26 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
enable_global_hotkey: enableGlobalHotkey,
});
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
return (
<BaseDialog
open={open}
title={t("Hotkey Setting")}
title={t("settings.modals.hotkey.title")}
contentSx={{ width: 450, maxHeight: 380 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<ItemWrapper style={{ marginBottom: 16 }}>
<Typography>{t("Enable Global Hotkey")}</Typography>
<Typography>
{t("settings.modals.hotkey.toggles.enableGlobal")}
</Typography>
<Switch
edge="end"
checked={enableGlobalHotkey}
@@ -108,7 +122,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
{HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func}>
<Typography>{t(func)}</Typography>
<Typography>{t(HOTKEY_FUNC_LABELS[func])}</Typography>
<HotkeyInput
value={hotkeyMap[func] ?? []}
onChange={(v) => setHotkeyMap((m) => ({ ...m, [func]: v }))}
+86 -28
View File
@@ -104,7 +104,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
const onSwitchFormat = (_e: any, value: boolean) => value;
const onError = (err: any) => {
showNotice("error", err.message || err.toString());
showNotice.error(err);
};
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
@@ -113,16 +113,20 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
return (
<BaseDialog
open={open}
title={t("Layout Setting")}
title={t("settings.components.verge.layout.title")}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List>
<Item>
<ListItemText primary={t("Prefer System Titlebar")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.preferSystemTitlebar",
)}
/>
<GuardState
value={decorated}
valueProps="checked"
@@ -137,7 +141,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Traffic Graph")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.trafficGraph")}
/>
<GuardState
value={verge?.traffic_graph ?? true}
valueProps="checked"
@@ -151,7 +157,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Memory Usage")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.memoryUsage")}
/>
<GuardState
value={verge?.enable_memory_usage ?? true}
valueProps="checked"
@@ -165,7 +173,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Proxy Group Icon")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.proxyGroupIcon",
)}
/>
<GuardState
value={verge?.enable_group_icon ?? true}
valueProps="checked"
@@ -182,9 +194,13 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
<ListItemText
primary={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<span>{t("Hover Jump Navigator")}</span>
<span>
{t("settings.components.verge.layout.fields.hoverNavigator")}
</span>
<TooltipIcon
title={t("Hover Jump Navigator Info")}
title={t(
"settings.components.verge.layout.tooltips.hoverNavigator",
)}
sx={{ opacity: "0.7" }}
/>
</Box>
@@ -206,9 +222,15 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
<ListItemText
primary={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
<span>{t("Hover Jump Navigator Delay")}</span>
<span>
{t(
"settings.components.verge.layout.fields.hoverNavigatorDelay",
)}
</span>
<TooltipIcon
title={t("Hover Jump Navigator Delay Info")}
title={t(
"settings.components.verge.layout.tooltips.hoverNavigatorDelay",
)}
sx={{ opacity: "0.7" }}
/>
</Box>
@@ -241,7 +263,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
input: {
endAdornment: (
<InputAdornment position="end">
{t("millis")}
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
@@ -256,7 +278,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Nav Icon")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.navIcon")}
/>
<GuardState
value={verge?.menu_icon ?? "monochrome"}
onCatch={onError}
@@ -265,16 +289,24 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
onGuard={(value) => patchVerge({ menu_icon: value })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
<MenuItem value="disable">{t("Disable")}</MenuItem>
<MenuItem value="monochrome">
{t("settings.components.verge.layout.options.icon.monochrome")}
</MenuItem>
<MenuItem value="colorful">
{t("settings.components.verge.layout.options.icon.colorful")}
</MenuItem>
<MenuItem value="disable">
{t("settings.components.verge.layout.options.icon.disable")}
</MenuItem>
</Select>
</GuardState>
</Item>
{OS === "macos" && (
<Item>
<ListItemText primary={t("Tray Icon")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.trayIcon")}
/>
<GuardState
value={verge?.tray_icon ?? "monochrome"}
onCatch={onError}
@@ -286,15 +318,21 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
size="small"
sx={{ width: 140, "> div": { py: "7.5px" } }}
>
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
<MenuItem value="colorful">{t("Colorful")}</MenuItem>
<MenuItem value="monochrome">
{t(
"settings.components.verge.layout.options.icon.monochrome",
)}
</MenuItem>
<MenuItem value="colorful">
{t("settings.components.verge.layout.options.icon.colorful")}
</MenuItem>
</Select>
</GuardState>
</Item>
)}
{/* {OS === "macos" && (
<Item>
<ListItemText primary={t("Enable Tray Speed")} />
<ListItemText primary={t("settings.components.verge.layout.fields.enableTraySpeed")} />
<GuardState
value={verge?.enable_tray_speed ?? false}
valueProps="checked"
@@ -309,7 +347,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
)} */}
{/* {OS === "macos" && (
<Item>
<ListItemText primary={t("Enable Tray Icon")} />
<ListItemText primary={t("settings.components.verge.layout.fields.enableTrayIcon")} />
<GuardState
value={
verge?.enable_tray_icon === false &&
@@ -328,7 +366,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
)} */}
<Item>
<ListItemText primary={t("Show Proxy Groups Inline")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.showProxyGroupsInline",
)}
/>
<GuardState
value={verge?.tray_inline_proxy_groups ?? false}
valueProps="checked"
@@ -342,7 +384,11 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
</Item>
<Item>
<ListItemText primary={t("Common Tray Icon")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.commonTrayIcon",
)}
/>
<GuardState
value={verge?.common_tray_icon}
onCatch={onError}
@@ -384,13 +430,19 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}}
>
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
{verge?.common_tray_icon
? t("shared.actions.clear")
: t("settings.components.verge.basic.actions.browse")}
</Button>
</GuardState>
</Item>
<Item>
<ListItemText primary={t("System Proxy Tray Icon")} />
<ListItemText
primary={t(
"settings.components.verge.layout.fields.systemProxyTrayIcon",
)}
/>
<GuardState
value={verge?.sysproxy_tray_icon}
onCatch={onError}
@@ -430,13 +482,17 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}}
>
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
{verge?.sysproxy_tray_icon
? t("shared.actions.clear")
: t("settings.components.verge.basic.actions.browse")}
</Button>
</GuardState>
</Item>
<Item>
<ListItemText primary={t("Tun Tray Icon")} />
<ListItemText
primary={t("settings.components.verge.layout.fields.tunTrayIcon")}
/>
<GuardState
value={verge?.tun_tray_icon}
onCatch={onError}
@@ -474,7 +530,9 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
}
}}
>
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
{verge?.tun_tray_icon
? t("shared.actions.clear")
: t("settings.components.verge.basic.actions.browse")}
</Button>
</GuardState>
</Item>
@@ -45,25 +45,27 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
auto_light_weight_minutes: values.autoEnterLiteModeDelay,
});
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} catch (err) {
showNotice.error(err);
}
});
return (
<BaseDialog
open={open}
title={t("LightWeight Mode Settings")}
title={t("settings.modals.liteMode.title")}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Enter LightWeight Mode Now")} />
<ListItemText
primary={t("settings.modals.liteMode.actions.enterNow")}
/>
<Typography
variant="button"
sx={{
@@ -73,17 +75,17 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
}}
onClick={async () => await entry_lightweight_mode()}
>
{t("Enable")}
{t("shared.actions.enable")}
</Typography>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Auto Enter LightWeight Mode")}
primary={t("settings.modals.liteMode.toggles.autoEnter")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Auto Enter LightWeight Mode Info")}
title={t("settings.modals.liteMode.tooltips.autoEnter")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -99,7 +101,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
{values.autoEnterLiteMode && (
<>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Enter LightWeight Mode Delay")} />
<ListItemText
primary={t("settings.modals.liteMode.fields.delay")}
/>
<TextField
autoComplete="off"
size="small"
@@ -119,7 +123,7 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
input: {
endAdornment: (
<InputAdornment position="end">
{t("mins")}
{t("shared.units.minutes")}
</InputAdornment>
),
},
@@ -133,10 +137,9 @@ export function LiteModeViewer({ ref }: { ref?: Ref<DialogRef> }) {
color="text.secondary"
sx={{ fontStyle: "italic" }}
>
{t(
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
{ n: values.autoEnterLiteModeDelay },
)}
{t("settings.modals.liteMode.messages.autoEnterHint", {
n: values.autoEnterLiteModeDelay,
})}
</Typography>
</ListItem>
</>
@@ -20,11 +20,13 @@ export const LocalBackupActions = memo(
try {
setLoading(true);
await createLocalBackup();
showNotice("success", t("Local Backup Created"));
showNotice.success(
"settings.modals.backup.messages.localBackupCreated",
);
await onBackupSuccess();
} catch (error) {
console.error(error);
showNotice("error", t("Local Backup Failed"));
showNotice.error("settings.modals.backup.messages.localBackupFailed");
} finally {
setLoading(false);
}
@@ -43,7 +45,7 @@ export const LocalBackupActions = memo(
<Grid container spacing={2}>
<Grid size={{ xs: 12, sm: 9 }}>
<Typography variant="body2" color="text.secondary">
{t("Local Backup Info")}
{t("settings.modals.backup.fields.info")}
</Typography>
</Grid>
<Grid size={{ xs: 12, sm: 3 }}>
@@ -60,7 +62,7 @@ export const LocalBackupActions = memo(
type="button"
size="large"
>
{t("Backup")}
{t("settings.modals.backup.actions.backup")}
</Button>
<Button
variant="outlined"
@@ -68,7 +70,7 @@ export const LocalBackupActions = memo(
type="button"
size="large"
>
{t("Refresh")}
{t("shared.actions.refresh")}
</Button>
</Stack>
</Grid>
+68 -29
View File
@@ -69,25 +69,27 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
auto_log_clean: values.autoLogClean as any,
});
setOpen(false);
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
return (
<BaseDialog
open={open}
title={t("Miscellaneous")}
title={t("settings.modals.misc.title")}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("App Log Level")} />
<ListItemText
primary={t("settings.modals.misc.fields.appLogLevel")}
/>
<Select
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
@@ -109,7 +111,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("App Log Max Size")}
primary={t("settings.modals.misc.fields.appLogMaxSize")}
sx={{ maxWidth: "fit-content" }}
/>
<TextField
@@ -130,7 +132,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("KB")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.kilobytes")}
</InputAdornment>
),
},
}}
@@ -139,7 +143,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("App Log Max Count")}
primary={t("settings.modals.misc.fields.appLogMaxCount")}
sx={{ maxWidth: "fit-content" }}
/>
<TextField
@@ -160,7 +164,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("Files")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.files")}
</InputAdornment>
),
},
}}
@@ -169,11 +175,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Auto Close Connections")}
primary={t("settings.modals.misc.fields.autoCloseConnections")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Auto Close Connections Info")}
title={t("settings.modals.misc.tooltips.autoCloseConnections")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -187,7 +193,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Check Update")} />
<ListItemText
primary={t("settings.modals.misc.fields.autoCheckUpdate")}
/>
<Switch
edge="end"
checked={values.autoCheckUpdate}
@@ -199,11 +207,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Enable Builtin Enhanced")}
primary={t("settings.modals.misc.fields.enableBuiltinEnhanced")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Enable Builtin Enhanced Info")}
title={t("settings.modals.misc.tooltips.enableBuiltinEnhanced")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -217,7 +225,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Layout Columns")} />
<ListItemText
primary={t("settings.modals.misc.fields.proxyLayoutColumns")}
/>
<Select
size="small"
sx={{ width: 160, "> div": { py: "7.5px" } }}
@@ -230,7 +240,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
}
>
<MenuItem value={6} key={6}>
{t("Auto Columns")}
{t("settings.modals.misc.options.proxyLayoutColumns.auto")}
</MenuItem>
{[1, 2, 3, 4, 5].map((i) => (
<MenuItem value={i} key={i}>
@@ -241,7 +251,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Log Clean")} />
<ListItemText
primary={t("settings.modals.misc.fields.autoLogClean")}
/>
<Select
size="small"
sx={{ width: 160, "> div": { py: "7.5px" } }}
@@ -255,11 +267,34 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
>
{/* 1: 1天, 2: 7天, 3: 30天, 4: 90天*/}
{[
{ key: t("Never Clean"), value: 0 },
{ key: t("Retain _n Days", { n: 1 }), value: 1 },
{ key: t("Retain _n Days", { n: 7 }), value: 2 },
{ key: t("Retain _n Days", { n: 30 }), value: 3 },
{ key: t("Retain _n Days", { n: 90 }), value: 4 },
{
key: t("settings.modals.misc.options.autoLogClean.never"),
value: 0,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 1,
}),
value: 1,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 7,
}),
value: 2,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 30,
}),
value: 3,
},
{
key: t("settings.modals.misc.options.autoLogClean.retainDays", {
n: 90,
}),
value: 4,
},
].map((i) => (
<MenuItem key={i.value} value={i.value}>
{i.key}
@@ -270,11 +305,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Auto Delay Detection")}
primary={t("settings.modals.misc.fields.autoDelayDetection")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Auto Delay Detection Info")}
title={t("settings.modals.misc.tooltips.autoDelayDetection")}
sx={{ opacity: "0.7" }}
/>
<Switch
@@ -289,11 +324,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Default Latency Test")}
primary={t("settings.modals.misc.fields.defaultLatencyTest")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon
title={t("Default Latency Test Info")}
title={t("settings.modals.misc.tooltips.defaultLatencyTest")}
sx={{ opacity: "0.7" }}
/>
<TextField
@@ -312,7 +347,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} />
<ListItemText
primary={t("settings.modals.misc.fields.defaultLatencyTimeout")}
/>
<TextField
autoComplete="new-password"
size="small"
@@ -332,7 +369,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">{t("millis")}</InputAdornment>
<InputAdornment position="end">
{t("shared.units.milliseconds")}
</InputAdornment>
),
},
}}
@@ -35,7 +35,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Network Interface")}
{t("settings.modals.networkInterface.title")}
<Box>
<Button
variant="contained"
@@ -51,7 +51,7 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -66,13 +66,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
address.V4 && (
<AddressDisplay
key={address.V4.ip}
label={t("Ip Address")}
label={t(
"settings.modals.networkInterface.fields.ipAddress",
)}
content={address.V4.ip}
/>
),
)}
<AddressDisplay
label={t("Mac Address")}
label={t(
"settings.modals.networkInterface.fields.macAddress",
)}
content={item.mac_addr ?? ""}
/>
</>
@@ -84,13 +88,17 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
address.V6 && (
<AddressDisplay
key={address.V6.ip}
label={t("Ip Address")}
label={t(
"settings.modals.networkInterface.fields.ipAddress",
)}
content={address.V6.ip}
/>
),
)}
<AddressDisplay
label={t("Mac Address")}
label={t(
"settings.modals.networkInterface.fields.macAddress",
)}
content={item.mac_addr ?? ""}
/>
</>
@@ -109,8 +117,6 @@ const AddressDisplay = ({
label: string;
content: string;
}) => {
const { t } = useTranslation();
return (
<Box
sx={{
@@ -135,7 +141,9 @@ const AddressDisplay = ({
size="small"
onClick={async () => {
await writeText(content);
showNotice("success", t("Copy Success"));
showNotice.success(
"shared.feedback.notifications.common.copySuccess",
);
}}
>
<ContentCopyRounded sx={{ fontSize: "18px" }} />
@@ -21,13 +21,15 @@ export const PasswordInput = (props: Props) => {
return (
<Dialog open={true} maxWidth="xs" fullWidth>
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
<DialogTitle>
{t("settings.modals.password.prompts.enterRoot")}
</DialogTitle>
<DialogContent>
<TextField
sx={{ mt: 1 }}
autoFocus
label={t("Password")}
label={t("shared.labels.password")}
fullWidth
size="small"
type="password"
@@ -42,7 +44,7 @@ export const PasswordInput = (props: Props) => {
onClick={async () => await onConfirm(passwd)}
variant="contained"
>
{t("Confirm")}
{t("shared.actions.confirm")}
</Button>
</DialogActions>
</Dialog>
+55 -32
View File
@@ -160,8 +160,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
mutate("getAutotemProxy"),
]);
}
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
};
@@ -277,14 +277,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const onSave = useLockFn(async () => {
if (value.duration < 1) {
showNotice(
"error",
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
);
showNotice.error("settings.modals.sysproxy.messages.durationTooShort");
return;
}
if (value.bypass && !validReg.test(value.bypass)) {
showNotice("error", t("Invalid Bypass Format"));
showNotice.error("settings.modals.sysproxy.messages.invalidBypass");
return;
}
@@ -301,7 +298,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
!ipv6Regex.test(value.proxy_host) &&
!hostnameRegex.test(value.proxy_host)
) {
showNotice("error", t("Invalid Proxy Host Format"));
showNotice.error("settings.modals.sysproxy.messages.invalidProxyHost");
return;
}
@@ -401,10 +398,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
console.warn("代理状态更新失败:", err);
}
}, 50);
} catch (err: any) {
} catch (err) {
console.error("配置保存失败:", err);
mutateVerge();
showNotice("error", err.toString());
showNotice.error(err);
// setOpen(true);
}
});
@@ -413,10 +410,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
return (
<BaseDialog
open={open}
title={t("System Proxy Setting")}
title={t("settings.modals.sysproxy.title")}
contentSx={{ width: 450, maxHeight: 565 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -424,28 +421,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
disableOk={saving}
>
<List>
<BaseFieldset label={t("Current System Proxy")} padding="15px 10px">
<BaseFieldset
label={t("settings.modals.sysproxy.fieldsets.currentStatus")}
padding="15px 10px"
>
<FlexBox>
<Typography className="label">{t("Enable status")}</Typography>
<Typography className="label">
{t("settings.modals.sysproxy.fields.enableStatus")}
</Typography>
<Typography className="value">
{value.pac
? autoproxy?.enable
? t("Enabled")
: t("Disabled")
? t("shared.statuses.enabled")
: t("shared.statuses.disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
? t("shared.statuses.enabled")
: t("shared.statuses.disabled")}
</Typography>
</FlexBox>
{!value.pac && (
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="label">
{t("settings.modals.sysproxy.fields.serverAddr")}
</Typography>
<Typography className="value">{getSystemProxyAddress}</Typography>
</FlexBox>
)}
{value.pac && (
<FlexBox>
<Typography className="label">{t("PAC URL")}</Typography>
<Typography className="label">
{t("settings.modals.sysproxy.fields.pacUrl")}
</Typography>
<Typography className="value">
{getCurrentPacUrl || "-"}
</Typography>
@@ -453,7 +459,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
)}
</BaseFieldset>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Host")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.proxyHost")}
/>
<Autocomplete
size="small"
sx={{ width: 150 }}
@@ -478,7 +486,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Use PAC Mode")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.usePacMode")}
/>
<Switch
edge="end"
disabled={!enabled}
@@ -489,10 +499,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Proxy Guard")}
primary={t("settings.modals.sysproxy.fields.proxyGuard")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Proxy Guard Info")} sx={{ opacity: "0.7" }} />
<TooltipIcon
title={t("settings.modals.sysproxy.tooltips.proxyGuard")}
sx={{ opacity: "0.7" }}
/>
<Switch
edge="end"
disabled={!enabled}
@@ -503,7 +516,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.guardDuration")}
/>
<TextField
disabled={!enabled}
size="small"
@@ -524,7 +539,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
{!value.pac && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Always use Default Bypass")} />
<ListItemText
primary={t(
"settings.modals.sysproxy.fields.alwaysUseDefaultBypass",
)}
/>
<Switch
edge="end"
disabled={!enabled}
@@ -543,7 +562,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{!value.pac && !value.use_default && (
<>
<ListItemText primary={t("Proxy Bypass")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.proxyBypass")}
/>
<TextField
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
@@ -561,7 +582,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{!value.pac && value.use_default && (
<>
<ListItemText primary={t("Bypass")} />
<ListItemText
primary={t("settings.modals.sysproxy.fields.bypass")}
/>
<FlexBox>
<TextField
disabled={true}
@@ -578,7 +601,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{value.pac && (
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
<ListItemText
primary={t("PAC Script Content")}
primary={t("settings.modals.sysproxy.fields.pacScriptContent")}
sx={{ padding: "3px 0" }}
/>
<Button
@@ -588,12 +611,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setEditorOpen(true);
}}
>
{t("Edit")} PAC
{t("settings.modals.sysproxy.actions.editPac")}
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
title={t("settings.modals.sysproxy.actions.editPac")}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
@@ -23,7 +23,7 @@ export const ThemeModeSwitch = (props: Props) => {
onClick={() => onChange?.(mode)}
sx={{ textTransform: "capitalize" }}
>
{t(`theme.${mode}`)}
{t(`settings.sections.appearance.${mode}`)}
</Button>
))}
</ButtonGroup>
+56 -27
View File
@@ -9,7 +9,7 @@ import {
useTheme,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { useImperativeHandle, useState } from "react";
import { useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef } from "@/components/base";
@@ -50,8 +50,8 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
try {
await patchVerge({ theme_setting: theme });
setOpen(false);
} catch (err: any) {
showNotice("error", err.toString());
} catch (err) {
showNotice.error(err);
}
});
@@ -62,9 +62,48 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
type ThemeKey = keyof typeof theme & keyof typeof defaultTheme;
const renderItem = (label: string, key: ThemeKey) => {
const fieldDefinitions: Array<{ labelKey: string; key: ThemeKey }> = useMemo(
() => [
{
labelKey: "settings.components.verge.theme.fields.primaryColor",
key: "primary_color",
},
{
labelKey: "settings.components.verge.theme.fields.secondaryColor",
key: "secondary_color",
},
{
labelKey: "settings.components.verge.theme.fields.primaryText",
key: "primary_text",
},
{
labelKey: "settings.components.verge.theme.fields.secondaryText",
key: "secondary_text",
},
{
labelKey: "settings.components.verge.theme.fields.infoColor",
key: "info_color",
},
{
labelKey: "settings.components.verge.theme.fields.warningColor",
key: "warning_color",
},
{
labelKey: "settings.components.verge.theme.fields.errorColor",
key: "error_color",
},
{
labelKey: "settings.components.verge.theme.fields.successColor",
key: "success_color",
},
],
[],
);
const renderItem = (labelKey: string, key: ThemeKey) => {
const label = t(labelKey);
return (
<Item>
<Item key={key}>
<ListItemText primary={label} />
<Round sx={{ background: theme[key] || dt[key] }} />
<TextField
@@ -81,33 +120,21 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
return (
<BaseDialog
open={open}
title={t("Theme Setting")}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
title={t("settings.components.verge.theme.title")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
contentSx={{ width: 400, maxHeight: 505, overflow: "auto", pb: 0 }}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List sx={{ pt: 0 }}>
{renderItem(t("Primary Color"), "primary_color")}
{renderItem(t("Secondary Color"), "secondary_color")}
{renderItem(t("Primary Text"), "primary_text")}
{renderItem(t("Secondary Text"), "secondary_text")}
{renderItem(t("Info Color"), "info_color")}
{renderItem(t("Warning Color"), "warning_color")}
{renderItem(t("Error Color"), "error_color")}
{renderItem(t("Success Color"), "success_color")}
{fieldDefinitions.map((field) => renderItem(field.labelKey, field.key))}
<Item>
<ListItemText primary={t("Font Family")} />
<ListItemText
primary={t("settings.components.verge.theme.fields.fontFamily")}
/>
<TextField
{...textProps}
value={theme.font_family ?? ""}
@@ -116,7 +143,9 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
/>
</Item>
<Item>
<ListItemText primary={t("CSS Injection")} />
<ListItemText
primary={t("settings.components.verge.theme.fields.cssInjection")}
/>
<Button
startIcon={<EditRounded />}
variant="outlined"
@@ -124,12 +153,12 @@ export function ThemeViewer(props: { ref?: React.Ref<DialogRef> }) {
setEditorOpen(true);
}}
>
{t("Edit")} CSS
{t("settings.components.verge.theme.actions.editCss")}
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} CSS`}
title={t("settings.components.verge.theme.dialogs.editCssTitle")}
initialData={Promise.resolve(theme.css_injection ?? "")}
language="css"
onSave={(_prev, curr) => {
+17 -15
View File
@@ -80,13 +80,13 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
);
try {
await enhanceProfiles();
showNotice("success", t("Settings Applied"));
showNotice.success("settings.modals.tun.messages.applied");
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -95,7 +95,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Tun Mode")}</Typography>
<Typography variant="h6">{t("settings.modals.tun.title")}</Typography>
<Button
variant="outlined"
size="small"
@@ -128,20 +128,20 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
);
}}
>
{t("Reset to Default")}
{t("shared.actions.resetToDefault")}
</Button>
</Box>
}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Stack")} />
<ListItemText primary={t("settings.modals.tun.fields.stack")} />
<StackModeSwitch
value={values.stack}
onChange={(value) => {
@@ -154,7 +154,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} />
<ListItemText primary={t("settings.modals.tun.fields.device")} />
<TextField
autoComplete="new-password"
size="small"
@@ -171,7 +171,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Route")} />
<ListItemText primary={t("settings.modals.tun.fields.autoRoute")} />
<Switch
edge="end"
checked={values.autoRoute}
@@ -180,7 +180,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Strict Route")} />
<ListItemText primary={t("settings.modals.tun.fields.strictRoute")} />
<Switch
edge="end"
checked={values.strictRoute}
@@ -189,7 +189,9 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Detect Interface")} />
<ListItemText
primary={t("settings.modals.tun.fields.autoDetectInterface")}
/>
<Switch
edge="end"
checked={values.autoDetectInterface}
@@ -200,7 +202,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} />
<ListItemText primary={t("settings.modals.tun.fields.dnsHijack")} />
<TextField
autoComplete="new-password"
size="small"
@@ -209,7 +211,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
spellCheck="false"
sx={{ width: 250 }}
value={values.dnsHijack.join(",")}
placeholder="Please use , to separate multiple DNS servers"
placeholder={t("settings.modals.tun.tooltips.dnsHijack")}
onChange={(e) =>
setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))
}
@@ -217,7 +219,7 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} />
<ListItemText primary={t("settings.modals.tun.fields.mtu")} />
<TextField
autoComplete="new-password"
size="small"
@@ -58,12 +58,12 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const onUpdate = useLockFn(async () => {
if (portableFlag) {
showNotice("error", t("Portable Updater Error"));
showNotice.error("settings.modals.update.messages.portableError");
return;
}
if (!updateInfo?.body) return;
if (breakChangeFlag) {
showNotice("error", t("Break Change Update Error"));
showNotice.error("settings.modals.update.messages.breakChangeError");
return;
}
if (updateState) return;
@@ -89,7 +89,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
await updateInfo.downloadAndInstall();
await relaunch();
} catch (err: any) {
showNotice("error", err?.message || err.toString());
showNotice.error(err);
} finally {
setUpdateState(false);
if (progressListener) {
@@ -113,7 +113,9 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between">
{`New Version v${updateInfo?.version}`}
{t("settings.modals.update.title", {
version: updateInfo?.version ?? "",
})}
<Box>
<Button
variant="contained"
@@ -124,14 +126,14 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
);
}}
>
{t("Go to Release Page")}
{t("settings.modals.update.actions.goToRelease")}
</Button>
</Box>
</Box>
}
contentSx={{ minWidth: 360, maxWidth: 400, height: "50vh" }}
okBtn={t("Update")}
cancelBtn={t("Cancel")}
okBtn={t("settings.modals.update.actions.update")}
cancelBtn={t("shared.actions.cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onUpdate}
+8 -6
View File
@@ -56,11 +56,13 @@ export const WebUIItem = (props: Props) => {
size="small"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
placeholder={t("Support %host, %port, %secret")}
placeholder={t(
"settings.modals.webUI.messages.supportedPlaceholders",
)}
/>
<IconButton
size="small"
title={t("Save")}
title={t("shared.actions.save")}
color="inherit"
onClick={() => {
onChange(editValue);
@@ -71,7 +73,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
title={t("Cancel")}
title={t("shared.actions.cancel")}
color="inherit"
onClick={() => {
onCancel?.();
@@ -127,7 +129,7 @@ export const WebUIItem = (props: Props) => {
</Typography>
<IconButton
size="small"
title={t("Open URL")}
title={t("settings.modals.webUI.actions.openUrl")}
color="inherit"
onClick={() => onOpenUrl?.(value)}
>
@@ -135,7 +137,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
title={t("Edit")}
title={t("shared.actions.edit")}
color="inherit"
onClick={() => {
setEditing(true);
@@ -146,7 +148,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
title={t("Delete")}
title={t("shared.actions.delete")}
color="inherit"
onClick={onDelete}
>
@@ -92,7 +92,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
await openWebUrl(url);
} catch (e: any) {
showNotice("error", e.message || e.toString());
showNotice.error(e);
}
});
@@ -101,14 +101,14 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Web UI")}
{t("settings.modals.webUI.title")}
<Button
variant="contained"
size="small"
disabled={editing}
onClick={() => setEditing(true)}
>
{t("New")}
{t("shared.actions.new")}
</Button>
</Box>
}
@@ -119,7 +119,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
overflowY: "auto",
userSelect: "text",
}}
cancelBtn={t("Close")}
cancelBtn={t("shared.actions.close")}
disableOk
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
@@ -128,7 +128,7 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
<BaseEmpty
extra={
<Typography mt={2} sx={{ fontSize: "12px" }}>
{t("Replace host, port, secret with %host, %port, %secret")}
{t("settings.modals.webUI.messages.placeholderInstruction")}
</Typography>
}
/>
+46 -25
View File
@@ -67,9 +67,11 @@ const SettingClash = ({ onError }: Props) => {
const onUpdateGeo = async () => {
try {
await updateGeo();
showNotice("success", t("GeoData Updated"));
showNotice.success(
"settings.feedback.notifications.clash.geoDataUpdated",
);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
};
@@ -86,14 +88,14 @@ const SettingClash = ({ onError }: Props) => {
} catch (err: any) {
setDnsSettingsEnabled(!enable);
localStorage.setItem("dns_settings_enabled", String(!enable));
showNotice("error", err.message || err.toString());
showNotice.error(err);
await patchVerge({ enable_dns_settings: !enable }).catch(() => {});
throw err;
}
});
return (
<SettingList title={t("Clash Setting")}>
<SettingList title={t("settings.sections.clash.title")}>
<WebUIViewer ref={webRef} />
<ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} />
@@ -103,10 +105,10 @@ const SettingClash = ({ onError }: Props) => {
<HeaderConfiguration ref={corsRef} />
<SettingItem
label={t("Allow Lan")}
label={t("settings.sections.clash.form.fields.allowLan")}
extra={
<TooltipIcon
title={t("Network Interface")}
title={t("settings.sections.clash.form.tooltips.networkInterface")}
color={"inherit"}
icon={LanRounded}
onClick={() => {
@@ -128,7 +130,7 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
label={t("DNS Overwrite")}
label={t("settings.sections.clash.form.fields.dnsOverwrite")}
extra={
<TooltipIcon
icon={SettingsRounded}
@@ -143,7 +145,7 @@ const SettingClash = ({ onError }: Props) => {
/>
</SettingItem>
<SettingItem label={t("IPv6")}>
<SettingItem label={t("settings.sections.clash.form.fields.ipv6")}>
<GuardState
value={ipv6 ?? false}
valueProps="checked"
@@ -157,10 +159,10 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
label={t("Unified Delay")}
label={t("settings.sections.clash.form.fields.unifiedDelay")}
extra={
<TooltipIcon
title={t("Unified Delay Info")}
title={t("settings.sections.clash.form.tooltips.unifiedDelay")}
sx={{ opacity: "0.7" }}
/>
}
@@ -178,9 +180,12 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
label={t("Log Level")}
label={t("settings.sections.clash.form.fields.logLevel")}
extra={
<TooltipIcon title={t("Log Level Info")} sx={{ opacity: "0.7" }} />
<TooltipIcon
title={t("settings.sections.clash.form.tooltips.logLevel")}
sx={{ opacity: "0.7" }}
/>
}
>
<GuardState
@@ -194,16 +199,26 @@ const SettingClash = ({ onError }: Props) => {
}}
>
<Select size="small" sx={{ width: 100, "> div": { py: "7.5px" } }}>
<MenuItem value="debug">Debug</MenuItem>
<MenuItem value="info">Info</MenuItem>
<MenuItem value="warning">Warn</MenuItem>
<MenuItem value="error">Error</MenuItem>
<MenuItem value="silent">Silent</MenuItem>
<MenuItem value="debug">
{t("settings.sections.clash.form.options.logLevel.debug")}
</MenuItem>
<MenuItem value="info">
{t("settings.sections.clash.form.options.logLevel.info")}
</MenuItem>
<MenuItem value="warning">
{t("settings.sections.clash.form.options.logLevel.warning")}
</MenuItem>
<MenuItem value="error">
{t("settings.sections.clash.form.options.logLevel.error")}
</MenuItem>
<MenuItem value="silent">
{t("settings.sections.clash.form.options.logLevel.silent")}
</MenuItem>
</Select>
</GuardState>
</SettingItem>
<SettingItem label={t("Port Config")}>
<SettingItem label={t("settings.sections.clash.form.fields.portConfig")}>
<TextField
autoComplete="new-password"
disabled={false}
@@ -218,10 +233,10 @@ const SettingClash = ({ onError }: Props) => {
</SettingItem>
<SettingItem
label={<>{t("External")}</>}
label={t("settings.sections.clash.form.fields.external")}
extra={
<TooltipIcon
title={t("External Cors Settings")}
title={t("settings.sections.externalCors.tooltips.open")}
icon={SettingsRounded}
onClick={(e) => {
e.stopPropagation();
@@ -234,10 +249,13 @@ const SettingClash = ({ onError }: Props) => {
}}
/>
<SettingItem onClick={() => webRef.current?.open()} label={t("Web UI")} />
<SettingItem
onClick={() => webRef.current?.open()}
label={t("settings.sections.clash.form.fields.webUI")}
/>
<SettingItem
label={t("Clash Core")}
label={t("settings.sections.clash.form.fields.clashCore")}
extra={
<TooltipIcon
icon={SettingsRounded}
@@ -251,17 +269,20 @@ const SettingClash = ({ onError }: Props) => {
{isWIN && (
<SettingItem
onClick={invoke_uwp_tool}
label={t("Open UWP tool")}
label={t("settings.sections.clash.form.fields.openUwpTool")}
extra={
<TooltipIcon
title={t("Open UWP tool Info")}
title={t("settings.sections.clash.form.tooltips.openUwpTool")}
sx={{ opacity: "0.7" }}
/>
}
/>
)}
<SettingItem onClick={onUpdateGeo} label={t("Update GeoData")} />
<SettingItem
onClick={onUpdateGeo}
label={t("settings.sections.clash.form.fields.updateGeoData")}
/>
</SettingList>
);
};
+18 -10
View File
@@ -41,20 +41,26 @@ const SettingSystem = ({ onError }: Props) => {
};
return (
<SettingList title={t("System Setting")}>
<SettingList title={t("settings.sections.system.title")}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
<ProxyControlSwitches label={t("Tun Mode")} onError={onError} />
<ProxyControlSwitches
label={t("settings.sections.system.toggles.tunMode")}
onError={onError}
/>
<ProxyControlSwitches label={t("System Proxy")} onError={onError} />
<ProxyControlSwitches
label={t("settings.sections.system.toggles.systemProxy")}
onError={onError}
/>
<SettingItem
label={t("Auto Launch")}
label={t("settings.sections.system.fields.autoLaunch")}
extra={
isAdminMode && (
<Tooltip
title={t("Administrator mode may not support auto launch")}
title={t("settings.sections.system.tooltips.autoLaunchAdmin")}
>
<WarningRounded sx={{ color: "warning.main", mr: 1 }} />
</Tooltip>
@@ -72,9 +78,8 @@ const SettingSystem = ({ onError }: Props) => {
}}
onGuard={async (e) => {
if (isAdminMode) {
showNotice(
"info",
t("Administrator mode may not support auto launch"),
showNotice.info(
"settings.sections.system.tooltips.autoLaunchAdmin",
);
}
@@ -96,9 +101,12 @@ const SettingSystem = ({ onError }: Props) => {
</SettingItem>
<SettingItem
label={t("Silent Start")}
label={t("settings.sections.system.fields.silentStart")}
extra={
<TooltipIcon title={t("Silent Start Info")} sx={{ opacity: "0.7" }} />
<TooltipIcon
title={t("settings.sections.system.tooltips.silentStart")}
sx={{ opacity: "0.7" }}
/>
}
>
<GuardState
@@ -47,28 +47,36 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
try {
const info = await checkUpdate();
if (!info?.available) {
showNotice("success", t("Currently on the Latest Version"));
showNotice.success(
"settings.components.verge.advanced.notifications.latestVersion",
);
} else {
updateRef.current?.open();
}
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
};
const onExportDiagnosticInfo = useCallback(async () => {
await exportDiagnosticInfo();
showNotice("success", t("Copy Success"), 1000);
}, [t]);
showNotice.success(
"shared.feedback.notifications.common.copySuccess",
1000,
);
}, []);
const copyVersion = useCallback(() => {
navigator.clipboard.writeText(`v${version}`).then(() => {
showNotice("success", t("Version copied to clipboard"), 1000);
showNotice.success(
"settings.components.verge.advanced.notifications.versionCopied",
1000,
);
});
}, [t]);
}, []);
return (
<SettingList title={t("Verge Advanced Setting")}>
<SettingList title={t("settings.components.verge.advanced.title")}>
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} />
@@ -80,10 +88,10 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
label={t("settings.components.verge.advanced.fields.backupSetting")}
extra={
<TooltipIcon
title={t("Backup Setting Info")}
title={t("settings.components.verge.advanced.tooltips.backupInfo")}
sx={{ opacity: "0.7" }}
/>
}
@@ -91,33 +99,45 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
label={t("settings.components.verge.advanced.fields.runtimeConfig")}
/>
<SettingItem
onClick={openAppDir}
label={t("Open Conf Dir")}
label={t("settings.components.verge.advanced.fields.openConfDir")}
extra={
<TooltipIcon
title={t("Open Conf Dir Info")}
title={t("settings.components.verge.advanced.tooltips.openConfDir")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
<SettingItem
onClick={openCoreDir}
label={t("settings.components.verge.advanced.fields.openCoreDir")}
/>
<SettingItem
label={t("LightWeight Mode Settings")}
onClick={openLogsDir}
label={t("settings.components.verge.advanced.fields.openLogsDir")}
/>
<SettingItem
onClick={onCheckUpdate}
label={t("settings.components.verge.advanced.fields.checkUpdates")}
/>
<SettingItem
onClick={openDevTools}
label={t("settings.components.verge.advanced.fields.openDevTools")}
/>
<SettingItem
label={t("settings.components.verge.advanced.fields.liteModeSettings")}
extra={
<TooltipIcon
title={t("LightWeight Mode Info")}
title={t("settings.components.verge.advanced.tooltips.liteMode")}
sx={{ opacity: "0.7" }}
/>
}
@@ -128,11 +148,11 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
onClick={() => {
exitApp();
}}
label={t("Exit")}
label={t("settings.components.verge.advanced.fields.exit")}
/>
<SettingItem
label={t("Export Diagnostic Info")}
label={t("settings.components.verge.advanced.fields.exportDiagnostics")}
extra={
<TooltipIcon
icon={ContentCopyRounded}
@@ -142,12 +162,12 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
></SettingItem>
<SettingItem
label={t("Verge Version")}
label={t("settings.components.verge.advanced.fields.vergeVersion")}
extra={
<TooltipIcon
icon={ContentCopyRounded}
onClick={copyVersion}
title={t("Copy Version")}
title={t("settings.components.verge.advanced.actions.copyVersion")}
/>
}
>
+43 -20
View File
@@ -76,11 +76,14 @@ const SettingVergeBasic = ({ onError }: Props) => {
const onCopyClashEnv = useCallback(async () => {
await copyClashEnv();
showNotice("success", t("Copy Success"), 1000);
}, [t]);
showNotice.success(
"shared.feedback.notifications.common.copySuccess",
1000,
);
}, []);
return (
<SettingList title={t("Verge Basic Setting")}>
<SettingList title={t("settings.components.verge.basic.title")}>
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} />
@@ -89,7 +92,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem label={t("Language")}>
<SettingItem label={t("settings.components.verge.basic.fields.language")}>
<GuardState
value={language ?? "en"}
onCatch={onError}
@@ -107,7 +110,9 @@ const SettingVergeBasic = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Theme Mode")}>
<SettingItem
label={t("settings.components.verge.basic.fields.themeMode")}
>
<GuardState
value={theme_mode}
onCatch={onError}
@@ -119,7 +124,9 @@ const SettingVergeBasic = ({ onError }: Props) => {
</SettingItem>
{OS !== "linux" && (
<SettingItem label={t("Tray Click Event")}>
<SettingItem
label={t("settings.components.verge.basic.fields.trayClickEvent")}
>
<GuardState
value={tray_event ?? "main_window"}
onCatch={onError}
@@ -128,18 +135,30 @@ const SettingVergeBasic = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ tray_event: e })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
<MenuItem value="main_window">{t("Show Main Window")}</MenuItem>
<MenuItem value="tray_menu">{t("Show Tray Menu")}</MenuItem>
<MenuItem value="system_proxy">{t("System Proxy")}</MenuItem>
<MenuItem value="tun_mode">{t("Tun Mode")}</MenuItem>
<MenuItem value="disable">{t("Disable")}</MenuItem>
<MenuItem value="main_window">
{t(
"settings.components.verge.basic.trayOptions.showMainWindow",
)}
</MenuItem>
<MenuItem value="tray_menu">
{t("settings.components.verge.basic.trayOptions.showTrayMenu")}
</MenuItem>
<MenuItem value="system_proxy">
{t("settings.sections.system.toggles.systemProxy")}
</MenuItem>
<MenuItem value="tun_mode">
{t("settings.sections.system.toggles.tunMode")}
</MenuItem>
<MenuItem value="disable">
{t("settings.components.verge.basic.trayOptions.disable")}
</MenuItem>
</Select>
</GuardState>
</SettingItem>
)}
<SettingItem
label={t("Copy Env Type")}
label={t("settings.components.verge.basic.fields.copyEnvType")}
extra={
<TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} />
}
@@ -161,7 +180,9 @@ const SettingVergeBasic = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Start Page")}>
<SettingItem
label={t("settings.components.verge.basic.fields.startPage")}
>
<GuardState
value={start_page ?? "/"}
onCatch={onError}
@@ -181,7 +202,9 @@ const SettingVergeBasic = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Startup Script")}>
<SettingItem
label={t("settings.components.verge.basic.fields.startupScript")}
>
<GuardState
value={startup_script ?? ""}
onCatch={onError}
@@ -214,7 +237,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
}
}}
>
{t("Browse")}
{t("settings.components.verge.basic.actions.browse")}
</Button>
{startup_script && (
<Button
@@ -223,7 +246,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
patchVerge({ startup_script: "" });
}}
>
{t("Clear")}
{t("shared.actions.clear")}
</Button>
)}
</>
@@ -234,22 +257,22 @@ const SettingVergeBasic = ({ onError }: Props) => {
<SettingItem
onClick={() => themeRef.current?.open()}
label={t("Theme Setting")}
label={t("settings.components.verge.basic.fields.themeSetting")}
/>
<SettingItem
onClick={() => layoutRef.current?.open()}
label={t("Layout Setting")}
label={t("settings.components.verge.basic.fields.layoutSetting")}
/>
<SettingItem
onClick={() => miscRef.current?.open()}
label={t("Miscellaneous")}
label={t("settings.components.verge.basic.fields.misc")}
/>
<SettingItem
onClick={() => hotkeyRef.current?.open()}
label={t("Hotkey Setting")}
label={t("settings.components.verge.basic.fields.hotkeySetting")}
/>
</SettingList>
);
+23 -16
View File
@@ -126,15 +126,15 @@ const ProxyControlSwitches = ({
const { enable_tun_mode, enable_system_proxy } = verge ?? {};
const showErrorNotice = useCallback(
(msg: string) => showNotice("error", t(msg)),
[t],
(msg: string) => showNotice.error(msg),
[],
);
const handleTunToggle = async (value: boolean) => {
if (!isTunModeAvailable) {
const msg = "TUN requires Service Mode or Admin Mode";
showErrorNotice(msg);
throw new Error(t(msg));
const msgKey = "settings.sections.proxyControl.tooltips.tunUnavailable";
showErrorNotice(msgKey);
throw new Error(t(msgKey));
}
mutateVerge({ ...verge, enable_tun_mode: value }, false);
await patchVerge({ enable_tun_mode: value });
@@ -145,7 +145,7 @@ const ProxyControlSwitches = ({
await installServiceAndRestartCore();
await mutateSystemState();
} catch (err) {
showNotice("error", (err as Error).message || String(err));
showNotice.error(err);
}
});
@@ -157,20 +157,21 @@ const ProxyControlSwitches = ({
await uninstallServiceAndRestartCore();
await mutateSystemState();
} catch (err) {
showNotice("error", (err as Error).message || String(err));
showNotice.error(err);
}
});
const isSystemProxyMode = label === t("System Proxy") || !label;
const isTunMode = label === t("Tun Mode");
const isSystemProxyMode =
label === t("settings.sections.system.toggles.systemProxy") || !label;
const isTunMode = label === t("settings.sections.system.toggles.tunMode");
return (
<Box sx={{ width: "100%", pr: noRightPadding ? 1 : 2 }}>
{isSystemProxyMode && (
<SwitchRow
label={t("System Proxy")}
label={t("settings.sections.proxyControl.fields.systemProxy")}
active={systemProxyActualState}
infoTitle={t("System Proxy Info")}
infoTitle={t("settings.sections.proxyControl.tooltips.systemProxy")}
onInfoClick={() => sysproxyRef.current?.open()}
onToggle={(value) => toggleSystemProxy(value)}
onError={onError}
@@ -180,9 +181,9 @@ const ProxyControlSwitches = ({
{isTunMode && (
<SwitchRow
label={t("Tun Mode")}
label={t("settings.sections.proxyControl.fields.tunMode")}
active={enable_tun_mode || false}
infoTitle={t("Tun Mode Info")}
infoTitle={t("settings.sections.proxyControl.tooltips.tunMode")}
onInfoClick={() => tunRef.current?.open()}
onToggle={handleTunToggle}
onError={onError}
@@ -193,12 +194,16 @@ const ProxyControlSwitches = ({
{!isTunModeAvailable && (
<>
<TooltipIcon
title={t("TUN requires Service Mode or Admin Mode")}
title={t(
"settings.sections.proxyControl.tooltips.tunUnavailable",
)}
icon={WarningRounded}
sx={{ color: "warning.main", ml: 1 }}
/>
<TooltipIcon
title={t("Install Service")}
title={t(
"settings.sections.proxyControl.actions.installService",
)}
icon={BuildRounded}
color="primary"
onClick={onInstallService}
@@ -208,7 +213,9 @@ const ProxyControlSwitches = ({
)}
{isServiceOk && (
<TooltipIcon
title={t("Uninstall Service")}
title={t(
"settings.sections.proxyControl.actions.uninstallService",
)}
icon={DeleteForeverRounded}
color="secondary"
onClick={onUninstallService}
+2 -2
View File
@@ -82,7 +82,7 @@ export const TestItem = ({
try {
removeTest(uid);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
}
});
@@ -192,7 +192,7 @@ export const TestItem = ({
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
})}
>
{t("Test")}
{t("tests.components.item.actions.test")}
</Widget>
)}
+11 -7
View File
@@ -101,7 +101,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
setLoading(false);
setTimeout(() => formIns.reset(), 500);
} catch (err: any) {
showNotice("error", err.message || err.toString());
showNotice.error(err);
setLoading(false);
}
}),
@@ -124,10 +124,14 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
return (
<BaseDialog
open={open}
title={openType === "new" ? t("Create Test") : t("Edit Test")}
title={
openType === "new"
? t("tests.modals.test.title.create")
: t("tests.modals.test.title.edit")
}
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
okBtn={t("shared.actions.save")}
cancelBtn={t("shared.actions.cancel")}
onClose={handleClose}
onCancel={handleClose}
onOk={handleOk}
@@ -137,7 +141,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
name="name"
control={control}
render={({ field }) => (
<TextField {...text} {...field} label={t("Name")} />
<TextField {...text} {...field} label={t("shared.labels.name")} />
)}
/>
<Controller
@@ -149,7 +153,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
{...field}
multiline
maxRows={5}
label={t("Icon")}
label={t("shared.labels.icon")}
/>
)}
/>
@@ -162,7 +166,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
{...field}
multiline
maxRows={3}
label={t("Test URL")}
label={t("tests.modals.test.fields.url")}
/>
)}
/>