mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 19:22:44 +08:00
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:
co-authored by
renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
rozan
parent
c1a2e99f63
commit
c8aa72186e
@@ -437,7 +437,7 @@ const Layout = () => {
|
||||
: theme.palette.warning.dark,
|
||||
})}
|
||||
>
|
||||
{t("Menu reorder mode")}
|
||||
{t("layout.components.navigation.menu.reorderMode")}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -514,7 +514,9 @@ const Layout = () => {
|
||||
onClick={menuUnlocked ? handleLockMenu : handleUnlockMenu}
|
||||
dense
|
||||
>
|
||||
{menuUnlocked ? t("Lock menu order") : t("Unlock menu order")}
|
||||
{menuUnlocked
|
||||
? t("layout.components.navigation.menu.lock")
|
||||
: t("layout.components.navigation.menu.unlock")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
|
||||
@@ -15,69 +15,75 @@ export const handleNoticeMessage = (
|
||||
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
|
||||
// navigate("/profile", { state: { current: msg } });
|
||||
navigate("/profile");
|
||||
showNotice("success", t("Import Subscription Successful"));
|
||||
showNotice.success(
|
||||
"shared.feedback.notifications.importSubscriptionSuccess",
|
||||
);
|
||||
},
|
||||
"import_sub_url::error": () => {
|
||||
navigate("/profile");
|
||||
showNotice("error", msg);
|
||||
showNotice.error(msg);
|
||||
},
|
||||
"set_config::error": () => showNotice("error", msg),
|
||||
// 后端暂时没有启用相关通知, 批量更新可能造成扰人提醒
|
||||
// update_success: () => showNotice("success", t("Update subscription successfully")),
|
||||
"set_config::error": () => showNotice.error(msg),
|
||||
update_with_clash_proxy: () =>
|
||||
showNotice(
|
||||
"success",
|
||||
`${t("Update with Clash proxy successfully")} ${msg}`,
|
||||
showNotice.success(
|
||||
"settings.feedback.notifications.updater.withClashProxySuccess",
|
||||
msg,
|
||||
),
|
||||
update_failed_even_with_clash: () =>
|
||||
showNotice(
|
||||
"error",
|
||||
`${t("Update failed even with Clash proxy")}: ${msg}`,
|
||||
showNotice.error(
|
||||
"settings.feedback.notifications.updater.withClashProxyFailed",
|
||||
msg,
|
||||
),
|
||||
update_failed: () => showNotice("error", msg),
|
||||
update_failed: () => showNotice.error(msg),
|
||||
"config_validate::boot_error": () =>
|
||||
showNotice("error", `${t("Boot Config Validation Failed")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.config.bootFailed", msg),
|
||||
"config_validate::core_change": () =>
|
||||
showNotice(
|
||||
"error",
|
||||
`${t("Core Change Config Validation Failed")} ${msg}`,
|
||||
showNotice.error(
|
||||
"shared.feedback.validation.config.coreChangeFailed",
|
||||
msg,
|
||||
),
|
||||
"config_validate::error": () =>
|
||||
showNotice("error", `${t("Config Validation Failed")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.config.failed", msg),
|
||||
"config_validate::process_terminated": () =>
|
||||
showNotice("error", t("Config Validation Process Terminated")),
|
||||
showNotice.error("shared.feedback.validation.config.processTerminated"),
|
||||
"config_validate::stdout_error": () =>
|
||||
showNotice("error", `${t("Config Validation Failed")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.config.failed", msg),
|
||||
"config_validate::script_error": () =>
|
||||
showNotice("error", `${t("Script File Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.script.fileError", msg),
|
||||
"config_validate::script_syntax_error": () =>
|
||||
showNotice("error", `${t("Script Syntax Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.script.syntaxError", msg),
|
||||
"config_validate::script_missing_main": () =>
|
||||
showNotice("error", `${t("Script Missing Main")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.script.missingMain", msg),
|
||||
"config_validate::file_not_found": () =>
|
||||
showNotice("error", `${t("File Not Found")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.script.fileNotFound", msg),
|
||||
"config_validate::yaml_syntax_error": () =>
|
||||
showNotice("error", `${t("YAML Syntax Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.yaml.syntaxError", msg),
|
||||
"config_validate::yaml_read_error": () =>
|
||||
showNotice("error", `${t("YAML Read Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.yaml.readError", msg),
|
||||
"config_validate::yaml_mapping_error": () =>
|
||||
showNotice("error", `${t("YAML Mapping Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.yaml.mappingError", msg),
|
||||
"config_validate::yaml_key_error": () =>
|
||||
showNotice("error", `${t("YAML Key Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.yaml.keyError", msg),
|
||||
"config_validate::yaml_error": () =>
|
||||
showNotice("error", `${t("YAML Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.yaml.generalError", msg),
|
||||
"config_validate::merge_syntax_error": () =>
|
||||
showNotice("error", `${t("Merge File Syntax Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.merge.syntaxError", msg),
|
||||
"config_validate::merge_mapping_error": () =>
|
||||
showNotice("error", `${t("Merge File Mapping Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.merge.mappingError", msg),
|
||||
"config_validate::merge_key_error": () =>
|
||||
showNotice("error", `${t("Merge File Key Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.merge.keyError", msg),
|
||||
"config_validate::merge_error": () =>
|
||||
showNotice("error", `${t("Merge File Error")} ${msg}`),
|
||||
showNotice.error("shared.feedback.validation.merge.generalError", msg),
|
||||
"config_core::change_success": () =>
|
||||
showNotice("success", `${t("Core Changed Successfully")}: ${msg}`),
|
||||
showNotice.success(
|
||||
"settings.feedback.notifications.clash.changeSuccess",
|
||||
msg,
|
||||
),
|
||||
"config_core::change_error": () =>
|
||||
showNotice("error", `${t("Failed to Change Core")}: ${msg}`),
|
||||
showNotice.error(
|
||||
"settings.feedback.notifications.clash.changeFailed",
|
||||
msg,
|
||||
),
|
||||
};
|
||||
|
||||
const handler = handlers[status];
|
||||
|
||||
@@ -29,49 +29,49 @@ import UnlockPage from "./unlock";
|
||||
|
||||
export const navItems = [
|
||||
{
|
||||
label: "Label-Home",
|
||||
label: "layout.components.navigation.tabs.home",
|
||||
path: "/",
|
||||
icon: [<HomeRoundedIcon key="mui" />, <HomeSvg key="svg" />],
|
||||
Component: HomePage,
|
||||
},
|
||||
{
|
||||
label: "Label-Proxies",
|
||||
label: "layout.components.navigation.tabs.proxies",
|
||||
path: "/proxies",
|
||||
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||
Component: ProxiesPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Profiles",
|
||||
label: "layout.components.navigation.tabs.profiles",
|
||||
path: "/profile",
|
||||
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||
Component: ProfilesPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Connections",
|
||||
label: "layout.components.navigation.tabs.connections",
|
||||
path: "/connections",
|
||||
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||
Component: ConnectionsPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Rules",
|
||||
label: "layout.components.navigation.tabs.rules",
|
||||
path: "/rules",
|
||||
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||
Component: RulesPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Logs",
|
||||
label: "layout.components.navigation.tabs.logs",
|
||||
path: "/logs",
|
||||
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||
Component: LogsPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Unlock",
|
||||
label: "layout.components.navigation.tabs.unlock",
|
||||
path: "/unlock",
|
||||
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||
Component: UnlockPage,
|
||||
},
|
||||
{
|
||||
label: "Label-Settings",
|
||||
label: "layout.components.navigation.tabs.settings",
|
||||
path: "/settings",
|
||||
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
|
||||
Component: SettingsPage,
|
||||
|
||||
+59
-30
@@ -33,13 +33,48 @@ const initConn: IConnections = {
|
||||
|
||||
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
||||
|
||||
const ORDER_OPTIONS = [
|
||||
{
|
||||
id: "default",
|
||||
labelKey: "connections.components.order.default",
|
||||
fn: (list: IConnectionsItem[]) =>
|
||||
list.sort(
|
||||
(a, b) =>
|
||||
new Date(b.start || "0").getTime()! -
|
||||
new Date(a.start || "0").getTime()!,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "uploadSpeed",
|
||||
labelKey: "connections.components.order.uploadSpeed",
|
||||
fn: (list: IConnectionsItem[]) =>
|
||||
list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||
},
|
||||
{
|
||||
id: "downloadSpeed",
|
||||
labelKey: "connections.components.order.downloadSpeed",
|
||||
fn: (list: IConnectionsItem[]) =>
|
||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||
},
|
||||
] as const;
|
||||
|
||||
type OrderKey = (typeof ORDER_OPTIONS)[number]["id"];
|
||||
|
||||
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
|
||||
(acc, option) => {
|
||||
acc[option.id] = option.fn;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<OrderKey, OrderFunc>,
|
||||
);
|
||||
|
||||
const ConnectionsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const pageVisible = useVisibility();
|
||||
const [match, setMatch] = useState<(input: string) => boolean>(
|
||||
() => () => true,
|
||||
);
|
||||
const [curOrderOpt, setCurOrderOpt] = useState("Default");
|
||||
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>("default");
|
||||
|
||||
const {
|
||||
response: { data: connections },
|
||||
@@ -49,22 +84,6 @@ const ConnectionsPage = () => {
|
||||
|
||||
const isTableLayout = setting.layout === "table";
|
||||
|
||||
const orderOpts = useMemo<Record<string, OrderFunc>>(
|
||||
() => ({
|
||||
Default: (list) =>
|
||||
list.sort(
|
||||
(a, b) =>
|
||||
new Date(b.start || "0").getTime()! -
|
||||
new Date(a.start || "0").getTime()!,
|
||||
),
|
||||
"Upload Speed": (list) =>
|
||||
list.sort((a, b) => b.curUpload! - a.curUpload!),
|
||||
"Download Speed": (list) =>
|
||||
list.sort((a, b) => b.curDownload! - a.curDownload!),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
|
||||
|
||||
@@ -90,7 +109,7 @@ const ConnectionsPage = () => {
|
||||
}, [isPaused, frozenData, connections, pageVisible]);
|
||||
|
||||
const [filterConn] = useMemo(() => {
|
||||
const orderFunc = orderOpts[curOrderOpt];
|
||||
const orderFunc = orderFunctionMap[curOrderOpt];
|
||||
let conns = displayData.connections?.filter((conn) => {
|
||||
const { host, destinationIP, process } = conn.metadata;
|
||||
return (
|
||||
@@ -101,7 +120,7 @@ const ConnectionsPage = () => {
|
||||
if (orderFunc) conns = orderFunc(conns ?? []);
|
||||
|
||||
return [conns];
|
||||
}, [displayData, match, curOrderOpt, orderOpts]);
|
||||
}, [displayData, match, curOrderOpt]);
|
||||
|
||||
const onCloseAll = useLockFn(closeAllConnections);
|
||||
|
||||
@@ -129,7 +148,11 @@ const ConnectionsPage = () => {
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>}
|
||||
title={
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{t("connections.page.title")}
|
||||
</span>
|
||||
}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
@@ -140,10 +163,12 @@ const ConnectionsPage = () => {
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Box sx={{ mx: 1 }}>
|
||||
{t("Downloaded")}: {parseTraffic(displayData.downloadTotal)}
|
||||
{t("shared.labels.downloaded")}:{" "}
|
||||
{parseTraffic(displayData.downloadTotal)}
|
||||
</Box>
|
||||
<Box sx={{ mx: 1 }}>
|
||||
{t("Uploaded")}: {parseTraffic(displayData.uploadTotal)}
|
||||
{t("shared.labels.uploaded")}:{" "}
|
||||
{parseTraffic(displayData.uploadTotal)}
|
||||
</Box>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
@@ -157,16 +182,18 @@ const ConnectionsPage = () => {
|
||||
}
|
||||
>
|
||||
{isTableLayout ? (
|
||||
<TableRowsRounded titleAccess={t("List View")} />
|
||||
<TableRowsRounded titleAccess={t("shared.actions.listView")} />
|
||||
) : (
|
||||
<TableChartRounded titleAccess={t("Table View")} />
|
||||
<TableChartRounded titleAccess={t("shared.actions.tableView")} />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={handlePauseToggle}
|
||||
title={isPaused ? t("Resume") : t("Pause")}
|
||||
title={
|
||||
isPaused ? t("shared.actions.resume") : t("shared.actions.pause")
|
||||
}
|
||||
>
|
||||
{isPaused ? (
|
||||
<PlayCircleOutlineRounded />
|
||||
@@ -175,7 +202,9 @@ const ConnectionsPage = () => {
|
||||
)}
|
||||
</IconButton>
|
||||
<Button size="small" variant="contained" onClick={onCloseAll}>
|
||||
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
|
||||
<span style={{ whiteSpace: "nowrap" }}>
|
||||
{t("shared.actions.closeAll")}
|
||||
</span>
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
@@ -197,11 +226,11 @@ const ConnectionsPage = () => {
|
||||
{!isTableLayout && (
|
||||
<BaseStyledSelect
|
||||
value={curOrderOpt}
|
||||
onChange={(e) => setCurOrderOpt(e.target.value)}
|
||||
onChange={(e) => setCurOrderOpt(e.target.value as OrderKey)}
|
||||
>
|
||||
{Object.keys(orderOpts).map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
<span style={{ fontSize: 14 }}>{t(opt)}</span>
|
||||
{ORDER_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
<span style={{ fontSize: 14 }}>{t(option.labelKey)}</span>
|
||||
</MenuItem>
|
||||
))}
|
||||
</BaseStyledSelect>
|
||||
|
||||
+19
-19
@@ -112,7 +112,7 @@ const HomeSettingsDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>{t("Home Settings")}</DialogTitle>
|
||||
<DialogTitle>{t("home.page.settings.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
@@ -122,7 +122,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("profile")}
|
||||
/>
|
||||
}
|
||||
label={t("Profile Card")}
|
||||
label={t("home.page.settings.cards.profile")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -131,7 +131,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("proxy")}
|
||||
/>
|
||||
}
|
||||
label={t("Current Proxy Card")}
|
||||
label={t("home.page.settings.cards.currentProxy")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -140,7 +140,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("network")}
|
||||
/>
|
||||
}
|
||||
label={t("Network Settings Card")}
|
||||
label={t("home.page.settings.cards.network")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -149,7 +149,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("mode")}
|
||||
/>
|
||||
}
|
||||
label={t("Proxy Mode Card")}
|
||||
label={t("home.page.settings.cards.proxyMode")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -158,7 +158,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("traffic")}
|
||||
/>
|
||||
}
|
||||
label={t("Traffic Stats Card")}
|
||||
label={t("home.page.settings.cards.traffic")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -167,7 +167,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("test")}
|
||||
/>
|
||||
}
|
||||
label={t("Website Tests Card")}
|
||||
label={t("home.page.settings.cards.tests")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -176,7 +176,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("ip")}
|
||||
/>
|
||||
}
|
||||
label={t("IP Information Card")}
|
||||
label={t("home.page.settings.cards.ip")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -185,7 +185,7 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("clashinfo")}
|
||||
/>
|
||||
}
|
||||
label={t("Clash Info Cards")}
|
||||
label={t("home.page.settings.cards.clashInfo")}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -194,14 +194,14 @@ const HomeSettingsDialog = ({
|
||||
onChange={() => handleToggle("systeminfo")}
|
||||
/>
|
||||
}
|
||||
label={t("System Info Cards")}
|
||||
label={t("home.page.settings.cards.systemInfo")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t("Cancel")}</Button>
|
||||
<Button onClick={onClose}>{t("shared.actions.cancel")}</Button>
|
||||
<Button onClick={handleSave} color="primary">
|
||||
{t("Save")}
|
||||
{t("shared.actions.save")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -323,7 +323,7 @@ const HomePage = () => {
|
||||
renderCard(
|
||||
"traffic",
|
||||
<EnhancedCard
|
||||
title={t("Traffic Stats")}
|
||||
title={t("home.page.cards.trafficStats")}
|
||||
icon={<SpeedOutlined />}
|
||||
iconColor="secondary"
|
||||
>
|
||||
@@ -364,11 +364,11 @@ const HomePage = () => {
|
||||
);
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Label-Home")}
|
||||
title={t("home.page.title")}
|
||||
contentStyle={{ padding: 2 }}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Tooltip title={t("LightWeight Mode")} arrow>
|
||||
<Tooltip title={t("home.page.tooltips.lightweightMode")} arrow>
|
||||
<IconButton
|
||||
onClick={async () => await entry_lightweight_mode()}
|
||||
size="small"
|
||||
@@ -377,12 +377,12 @@ const HomePage = () => {
|
||||
<HistoryEduOutlined />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("Manual")} arrow>
|
||||
<Tooltip title={t("home.page.tooltips.manual")} arrow>
|
||||
<IconButton onClick={toGithubDoc} size="small" color="inherit">
|
||||
<HelpOutlineRounded />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("Home Settings")} arrow>
|
||||
<Tooltip title={t("home.page.tooltips.settings")} arrow>
|
||||
<IconButton onClick={openSettings} size="small" color="inherit">
|
||||
<SettingsOutlined />
|
||||
</IconButton>
|
||||
@@ -413,7 +413,7 @@ const NetworkSettingsCard = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Network Settings")}
|
||||
title={t("home.page.cards.networkSettings")}
|
||||
icon={<DnsOutlined />}
|
||||
iconColor="primary"
|
||||
action={null}
|
||||
@@ -428,7 +428,7 @@ const ClashModeEnhancedCard = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("Proxy Mode")}
|
||||
title={t("home.page.cards.proxyMode")}
|
||||
icon={<RouterOutlined />}
|
||||
iconColor="info"
|
||||
action={null}
|
||||
|
||||
+15
-8
@@ -60,7 +60,7 @@ const LogPage = () => {
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Logs")}
|
||||
title={t("logs.page.title")}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
@@ -70,7 +70,12 @@ const LogPage = () => {
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
title={t(enableLog ? "Pause" : "Resume")}
|
||||
title={t(
|
||||
enableLog ? "shared.actions.pause" : "shared.actions.resume",
|
||||
)}
|
||||
aria-label={t(
|
||||
enableLog ? "shared.actions.pause" : "shared.actions.resume",
|
||||
)}
|
||||
size="small"
|
||||
color="inherit"
|
||||
onClick={handleToggleLog}
|
||||
@@ -89,7 +94,7 @@ const LogPage = () => {
|
||||
refreshGetClashLog(true);
|
||||
}}
|
||||
>
|
||||
{t("Clear")}
|
||||
{t("shared.actions.clear")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
@@ -108,11 +113,13 @@ const LogPage = () => {
|
||||
value={logState}
|
||||
onChange={(e) => handleLogLevelChange(e.target.value as LogFilter)}
|
||||
>
|
||||
<MenuItem value="all">ALL</MenuItem>
|
||||
<MenuItem value="debug">DEBUG</MenuItem>
|
||||
<MenuItem value="info">INFO</MenuItem>
|
||||
<MenuItem value="warn">WARN</MenuItem>
|
||||
<MenuItem value="err">ERROR</MenuItem>
|
||||
<MenuItem value="all">{t("shared.filters.logLevels.all")}</MenuItem>
|
||||
<MenuItem value="debug">
|
||||
{t("shared.filters.logLevels.debug")}
|
||||
</MenuItem>
|
||||
<MenuItem value="info">{t("shared.filters.logLevels.info")}</MenuItem>
|
||||
<MenuItem value="warn">{t("shared.filters.logLevels.warn")}</MenuItem>
|
||||
<MenuItem value="err">{t("shared.filters.logLevels.error")}</MenuItem>
|
||||
</BaseStyledSelect>
|
||||
<BaseSearchBox
|
||||
onSearch={(matcher, state) => {
|
||||
|
||||
+65
-49
@@ -145,13 +145,13 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
|
||||
showNotice(
|
||||
"info",
|
||||
`${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`,
|
||||
showNotice.info(
|
||||
"profiles.page.feedback.notifications.switchInterrupted",
|
||||
`${previousSwitching} → ${newProfile}`,
|
||||
3000,
|
||||
);
|
||||
},
|
||||
[t],
|
||||
[],
|
||||
);
|
||||
|
||||
// 清理切换状态
|
||||
@@ -191,7 +191,7 @@ const ProfilePage = () => {
|
||||
|
||||
for (const file of paths) {
|
||||
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
|
||||
showNotice("error", t("Only YAML Files Supported"));
|
||||
showNotice.error("profiles.page.feedback.errors.onlyYaml");
|
||||
continue;
|
||||
}
|
||||
const item = {
|
||||
@@ -239,10 +239,17 @@ const ProfilePage = () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await onEnhance(false);
|
||||
|
||||
showNotice("success", "数据已强制刷新", 2000);
|
||||
} catch (error: any) {
|
||||
showNotice.success(
|
||||
"profiles.page.feedback.notices.forceRefreshCompleted",
|
||||
2000,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[紧急刷新] 失败:", error);
|
||||
showNotice("error", `紧急刷新失败: ${error.message}`, 4000);
|
||||
showNotice.error(
|
||||
"profiles.page.feedback.notices.emergencyRefreshFailed",
|
||||
{ message: String(error) },
|
||||
4000,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -271,13 +278,13 @@ const ProfilePage = () => {
|
||||
if (!url) return;
|
||||
// 校验url是否为http/https
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
showNotice("error", t("Invalid Profile URL"));
|
||||
showNotice.error("profiles.page.feedback.errors.invalidUrl");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
const handleImportSuccess = async (noticeKey: string) => {
|
||||
showNotice("success", t(noticeKey));
|
||||
showNotice.success(noticeKey);
|
||||
setUrl("");
|
||||
await performRobustRefresh();
|
||||
};
|
||||
@@ -285,24 +292,25 @@ const ProfilePage = () => {
|
||||
try {
|
||||
// 尝试正常导入
|
||||
await importProfile(url);
|
||||
await handleImportSuccess("Profile Imported Successfully");
|
||||
await handleImportSuccess("shared.feedback.notifications.importSuccess");
|
||||
} catch (initialErr) {
|
||||
console.warn("[订阅导入] 首次导入失败:", initialErr);
|
||||
|
||||
showNotice("info", t("Import failed, retrying with Clash proxy..."));
|
||||
showNotice.info("profiles.page.feedback.notifications.importRetry");
|
||||
try {
|
||||
// 使用自身代理尝试导入
|
||||
await importProfile(url, {
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
});
|
||||
await handleImportSuccess("Profile Imported with Clash proxy");
|
||||
} catch (retryErr: any) {
|
||||
await handleImportSuccess(
|
||||
"shared.feedback.notifications.importWithClashProxy",
|
||||
);
|
||||
} catch (retryErr) {
|
||||
// 回退导入也失败
|
||||
const retryErrmsg = retryErr?.message || retryErr.toString();
|
||||
showNotice(
|
||||
"error",
|
||||
`${t("Import failed even with Clash proxy")}: ${retryErrmsg}`,
|
||||
showNotice.error(
|
||||
"profiles.page.feedback.notifications.importFail",
|
||||
String(retryErr),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -349,16 +357,14 @@ const ProfilePage = () => {
|
||||
// 清除SWR缓存并重新获取
|
||||
await mutate("getProfiles", getProfiles(), { revalidate: true });
|
||||
await onEnhance(false);
|
||||
showNotice(
|
||||
"error",
|
||||
t("Profile imported but may need manual refresh"),
|
||||
showNotice.error(
|
||||
"profiles.page.feedback.notifications.importNeedsRefresh",
|
||||
3000,
|
||||
);
|
||||
} catch (finalError) {
|
||||
console.error(`[导入刷新] 最终刷新尝试失败:`, finalError);
|
||||
showNotice(
|
||||
"error",
|
||||
t("Profile imported successfully, please restart if not visible"),
|
||||
showNotice.error(
|
||||
"profiles.page.feedback.notifications.importSuccess",
|
||||
5000,
|
||||
);
|
||||
}
|
||||
@@ -477,7 +483,10 @@ const ProfilePage = () => {
|
||||
closeAllConnections();
|
||||
|
||||
if (notifySuccess && success) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
showNotice.success(
|
||||
"profiles.page.feedback.notifications.profileSwitched",
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -508,7 +517,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
console.error(`[Profile] 切换失败:`, err);
|
||||
showNotice("error", err?.message || err.toString(), 4000);
|
||||
showNotice.error(err, 4000);
|
||||
} finally {
|
||||
// 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态
|
||||
if (
|
||||
@@ -529,7 +538,6 @@ const ProfilePage = () => {
|
||||
profiles,
|
||||
patchProfiles,
|
||||
mutateLogs,
|
||||
t,
|
||||
executeBackgroundTasks,
|
||||
handleProfileInterrupt,
|
||||
cleanupSwitchState,
|
||||
@@ -574,10 +582,13 @@ const ProfilePage = () => {
|
||||
await enhanceProfiles();
|
||||
mutateLogs();
|
||||
if (notifySuccess) {
|
||||
showNotice("success", t("Profile Reactivated"), 1000);
|
||||
showNotice.success(
|
||||
"profiles.page.feedback.notifications.profileReactivated",
|
||||
1000,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString(), 3000);
|
||||
showNotice.error(err, 3000);
|
||||
} finally {
|
||||
// 保留正在切换的profile,清除其他状态
|
||||
setActivatings((prev) =>
|
||||
@@ -597,7 +608,7 @@ const ProfilePage = () => {
|
||||
await onEnhance(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
showNotice.error(err);
|
||||
} finally {
|
||||
setActivatings([]);
|
||||
}
|
||||
@@ -713,17 +724,17 @@ const ProfilePage = () => {
|
||||
setSelectedProfiles(new Set());
|
||||
setBatchMode(false);
|
||||
|
||||
showNotice("success", t("Selected profiles deleted successfully"));
|
||||
showNotice.success("profiles.page.feedback.notifications.batchDeleted");
|
||||
} catch (err: any) {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
showNotice.error(err);
|
||||
} finally {
|
||||
setActivatings([]);
|
||||
}
|
||||
});
|
||||
|
||||
const mode = useThemeMode();
|
||||
const islight = mode === "light" ? true : false;
|
||||
const dividercolor = islight
|
||||
const isLight = mode === "light";
|
||||
const dividercolor = isLight
|
||||
? "rgba(0, 0, 0, 0.06)"
|
||||
: "rgba(255, 255, 255, 0.06)";
|
||||
|
||||
@@ -793,7 +804,7 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Profiles")}
|
||||
title={t("profiles.page.title")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
@@ -803,7 +814,7 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Batch Operations")}
|
||||
title={t("profiles.page.batch.title")}
|
||||
onClick={toggleBatchMode}
|
||||
>
|
||||
<CheckBoxOutlineBlankRounded />
|
||||
@@ -812,7 +823,7 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Update All Profiles")}
|
||||
title={t("profiles.page.actions.updateAll")}
|
||||
onClick={onUpdateAll}
|
||||
>
|
||||
<RefreshRounded />
|
||||
@@ -821,7 +832,7 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("View Runtime Config")}
|
||||
title={t("profiles.page.actions.viewRuntimeConfig")}
|
||||
onClick={() => configRef.current?.open()}
|
||||
>
|
||||
<TextSnippetOutlined />
|
||||
@@ -830,7 +841,7 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
title={t("Reactivate Profiles")}
|
||||
title={t("profiles.page.actions.reactivate")}
|
||||
onClick={() => onEnhance(true)}
|
||||
>
|
||||
<LocalFireDepartmentRounded />
|
||||
@@ -862,7 +873,11 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={isAllSelected() ? t("Deselect All") : t("Select All")}
|
||||
title={
|
||||
isAllSelected()
|
||||
? t("profiles.page.batch.actions.deselectAll")
|
||||
: t("profiles.page.batch.actions.selectAll")
|
||||
}
|
||||
onClick={
|
||||
isAllSelected() ? clearAllSelections : selectAllProfiles
|
||||
}
|
||||
@@ -878,19 +893,20 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
title={t("Delete Selected Profiles")}
|
||||
title={t("profiles.page.batch.actions.delete")}
|
||||
onClick={deleteSelectedProfiles}
|
||||
disabled={selectedProfiles.size === 0}
|
||||
>
|
||||
<DeleteRounded />
|
||||
</IconButton>
|
||||
<Button size="small" variant="outlined" onClick={toggleBatchMode}>
|
||||
{t("Done")}
|
||||
{t("profiles.page.batch.actions.done")}
|
||||
</Button>
|
||||
<Box
|
||||
sx={{ flex: 1, textAlign: "right", color: "text.secondary" }}
|
||||
>
|
||||
{t("Selected")} {selectedProfiles.size} {t("items")}
|
||||
{t("profiles.page.batch.summary.selected")}{" "}
|
||||
{selectedProfiles.size} {t("profiles.page.batch.summary.items")}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -923,7 +939,7 @@ const ProfilePage = () => {
|
||||
event.preventDefault();
|
||||
void onImport();
|
||||
}}
|
||||
placeholder={t("Profile URL")}
|
||||
placeholder={t("profiles.page.importForm.placeholder")}
|
||||
slotProps={{
|
||||
input: {
|
||||
sx: { pr: 1 },
|
||||
@@ -931,7 +947,7 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
title={t("Paste")}
|
||||
title={t("profiles.page.importForm.actions.paste")}
|
||||
onClick={onCopyLink}
|
||||
>
|
||||
<ContentPasteRounded fontSize="inherit" />
|
||||
@@ -940,7 +956,7 @@ const ProfilePage = () => {
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.5 }}
|
||||
title={t("Clear")}
|
||||
title={t("shared.actions.clear")}
|
||||
onClick={() => setUrl("")}
|
||||
>
|
||||
<ClearRounded fontSize="inherit" />
|
||||
@@ -957,7 +973,7 @@ const ProfilePage = () => {
|
||||
sx={{ borderRadius: "6px" }}
|
||||
onClick={onImport}
|
||||
>
|
||||
{t("Import")}
|
||||
{t("profiles.page.actions.import")}
|
||||
</LoadingButton>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -965,7 +981,7 @@ const ProfilePage = () => {
|
||||
sx={{ borderRadius: "6px" }}
|
||||
onClick={() => viewerRef.current?.create()}
|
||||
>
|
||||
{t("New")}
|
||||
{t("shared.actions.new")}
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -1002,7 +1018,7 @@ const ProfilePage = () => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
await onEnhance(false);
|
||||
// await restartCore();
|
||||
// Notice.success(t("Clash Core Restarted"), 1000);
|
||||
// Notice.success(t("settings.feedback.notifications.clash.restartSuccess"), 1000);
|
||||
}
|
||||
}}
|
||||
onDelete={() => {
|
||||
|
||||
+20
-9
@@ -15,6 +15,12 @@ import {
|
||||
updateProxyChainConfigInRuntime,
|
||||
} from "@/services/cmds";
|
||||
|
||||
const MODES = ["rule", "global", "direct"] as const;
|
||||
type Mode = (typeof MODES)[number];
|
||||
const MODE_SET = new Set<string>(MODES);
|
||||
const isMode = (value: unknown): value is Mode =>
|
||||
typeof value === "string" && MODE_SET.has(value);
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -50,11 +56,12 @@ const ProxyPage = () => {
|
||||
|
||||
const { verge } = useVerge();
|
||||
|
||||
const modeList = useMemo(() => ["rule", "global", "direct"], []);
|
||||
const modeList = useMemo(() => MODES, []);
|
||||
|
||||
const curMode = clashConfig?.mode?.toLowerCase();
|
||||
const normalizedMode = clashConfig?.mode?.toLowerCase();
|
||||
const curMode = isMode(normalizedMode) ? normalizedMode : undefined;
|
||||
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
const onChangeMode = useLockFn(async (mode: Mode) => {
|
||||
// 断开连接
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
@@ -124,16 +131,20 @@ const ProxyPage = () => {
|
||||
}, [isChainMode, updateChainConfigData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (curMode && !modeList.includes(curMode)) {
|
||||
if (normalizedMode && !isMode(normalizedMode)) {
|
||||
onChangeMode("rule");
|
||||
}
|
||||
}, [curMode, modeList, onChangeMode]);
|
||||
}, [normalizedMode, onChangeMode]);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
contentStyle={{ height: "101.5%" }}
|
||||
title={isChainMode ? t("Proxy Chain Mode") : t("Proxy Groups")}
|
||||
title={
|
||||
isChainMode
|
||||
? t("proxies.page.title.chainMode")
|
||||
: t("proxies.page.title.default")
|
||||
}
|
||||
header={
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
@@ -146,7 +157,7 @@ const ProxyPage = () => {
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
{t(`proxies.page.modes.${mode}`)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
@@ -157,13 +168,13 @@ const ProxyPage = () => {
|
||||
onClick={onToggleChainMode}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
{t("Chain Proxy")}
|
||||
{t("proxies.page.actions.toggleChain")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ProxyGroups
|
||||
mode={curMode!}
|
||||
mode={curMode ?? "rule"}
|
||||
isChainMode={isChainMode}
|
||||
chainConfigData={chainConfigData}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ const RulesPage = () => {
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Rules")}
|
||||
title={t("rules.page.title")}
|
||||
contentStyle={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
|
||||
@@ -16,7 +16,7 @@ const SettingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onError = (err: any) => {
|
||||
showNotice("error", err?.message || err.toString());
|
||||
showNotice.error(err);
|
||||
};
|
||||
|
||||
const toGithubRepo = useLockFn(() => {
|
||||
@@ -36,13 +36,13 @@ const SettingPage = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Settings")}
|
||||
title={t("settings.page.title")}
|
||||
header={
|
||||
<ButtonGroup variant="contained" aria-label="Basic button group">
|
||||
<IconButton
|
||||
size="medium"
|
||||
color="inherit"
|
||||
title={t("Manual")}
|
||||
title={t("settings.page.actions.manual")}
|
||||
onClick={toGithubDoc}
|
||||
>
|
||||
<HelpOutlineRounded fontSize="inherit" />
|
||||
@@ -50,7 +50,7 @@ const SettingPage = () => {
|
||||
<IconButton
|
||||
size="medium"
|
||||
color="inherit"
|
||||
title={t("TG Channel")}
|
||||
title={t("settings.page.actions.telegram")}
|
||||
onClick={toTelegramChannel}
|
||||
>
|
||||
<Telegram fontSize="inherit" />
|
||||
@@ -59,7 +59,7 @@ const SettingPage = () => {
|
||||
<IconButton
|
||||
size="medium"
|
||||
color="inherit"
|
||||
title={t("Github Repo")}
|
||||
title={t("settings.page.actions.github")}
|
||||
onClick={toGithubRepo}
|
||||
>
|
||||
<GitHub fontSize="inherit" />
|
||||
|
||||
+3
-3
@@ -141,7 +141,7 @@ const TestPage = () => {
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
title={t("Test")}
|
||||
title={t("tests.page.title")}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Button
|
||||
@@ -149,14 +149,14 @@ const TestPage = () => {
|
||||
size="small"
|
||||
onClick={() => emit("verge://test-all")}
|
||||
>
|
||||
{t("Test All")}
|
||||
{t("tests.page.actions.testAll")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => viewerRef.current?.create()}
|
||||
>
|
||||
{t("New")}
|
||||
{t("shared.actions.new")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
|
||||
+27
-15
@@ -37,6 +37,19 @@ interface UnlockItem {
|
||||
const UNLOCK_RESULTS_STORAGE_KEY = "clash_verge_unlock_results";
|
||||
const UNLOCK_RESULTS_TIME_KEY = "clash_verge_unlock_time";
|
||||
|
||||
const STATUS_LABEL_KEYS: Record<string, string> = {
|
||||
Pending: "tests.statuses.test.pending",
|
||||
Yes: "tests.statuses.test.yes",
|
||||
No: "tests.statuses.test.no",
|
||||
Failed: "tests.statuses.test.failed",
|
||||
Completed: "tests.statuses.test.completed",
|
||||
"Disallowed ISP": "tests.statuses.test.disallowedIsp",
|
||||
"Originals Only": "tests.statuses.test.originalsOnly",
|
||||
"No (IP Banned By Disney+)": "tests.statuses.test.noDisney",
|
||||
"Unsupported Country/Region": "tests.statuses.test.unsupportedRegion",
|
||||
"Failed (Network Connection)": "tests.statuses.test.failedNetwork",
|
||||
};
|
||||
|
||||
const UnlockPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
@@ -150,7 +163,8 @@ const UnlockPage = () => {
|
||||
invoke<T>(cmd, args),
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(t("Detection timeout or failed"))),
|
||||
() =>
|
||||
reject(new Error(t("tests.unlock.page.messages.detectionTimeout"))),
|
||||
timeout,
|
||||
),
|
||||
),
|
||||
@@ -173,10 +187,7 @@ const UnlockPage = () => {
|
||||
setIsCheckingAll(false);
|
||||
} catch (err: any) {
|
||||
setIsCheckingAll(false);
|
||||
showNotice(
|
||||
"error",
|
||||
err?.message || err?.toString() || t("Detection timeout or failed"),
|
||||
);
|
||||
showNotice.error("tests.unlock.page.messages.detectionTimeout", err);
|
||||
console.error("Failed to check media unlock:", err);
|
||||
}
|
||||
});
|
||||
@@ -206,11 +217,10 @@ const UnlockPage = () => {
|
||||
setLoadingItems((prev) => prev.filter((item) => item !== name));
|
||||
} catch (err: any) {
|
||||
setLoadingItems((prev) => prev.filter((item) => item !== name));
|
||||
showNotice(
|
||||
"error",
|
||||
err?.message ||
|
||||
err?.toString() ||
|
||||
t("Detection failed for {name}").replace("{name}", name),
|
||||
showNotice.error(
|
||||
"tests.unlock.page.messages.detectionFailedWithName",
|
||||
{ name },
|
||||
err,
|
||||
);
|
||||
console.error(`Failed to check ${name}:`, err);
|
||||
}
|
||||
@@ -258,7 +268,7 @@ const UnlockPage = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Unlock Test")}
|
||||
title={t("tests.unlock.page.title")}
|
||||
header={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Button
|
||||
@@ -274,7 +284,9 @@ const UnlockPage = () => {
|
||||
)
|
||||
}
|
||||
>
|
||||
{isCheckingAll ? t("Testing...") : t("Test All")}
|
||||
{isCheckingAll
|
||||
? t("tests.unlock.page.actions.testing")
|
||||
: t("tests.page.actions.testAll")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
@@ -288,7 +300,7 @@ const UnlockPage = () => {
|
||||
height: "50%",
|
||||
}}
|
||||
>
|
||||
<BaseEmpty text={t("No unlock test items")} />
|
||||
<BaseEmpty textKey="tests.unlock.page.empty" />
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={1.5} columns={{ xs: 1, sm: 2, md: 3 }}>
|
||||
@@ -330,7 +342,7 @@ const UnlockPage = () => {
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
<Tooltip title={t("Test")}>
|
||||
<Tooltip title={t("tests.components.item.actions.test")}>
|
||||
<span>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -372,7 +384,7 @@ const UnlockPage = () => {
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={t(item.status)}
|
||||
label={t(STATUS_LABEL_KEYS[item.status] ?? item.status)}
|
||||
color={getStatusColor(item.status)}
|
||||
size="small"
|
||||
icon={getStatusIcon(item.status)}
|
||||
|
||||
Reference in New Issue
Block a user