Sline c8aa72186e
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>
2025-11-08 19:40:38 +08:00

442 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
DnsOutlined,
HelpOutlineRounded,
HistoryEduOutlined,
RouterOutlined,
SettingsOutlined,
SpeedOutlined,
} from "@mui/icons-material";
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
FormGroup,
Grid,
IconButton,
Skeleton,
Tooltip,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { Suspense, lazy, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { BasePage } from "@/components/base";
import { ClashModeCard } from "@/components/home/clash-mode-card";
import { CurrentProxyCard } from "@/components/home/current-proxy-card";
import { EnhancedCard } from "@/components/home/enhanced-card";
import { EnhancedTrafficStats } from "@/components/home/enhanced-traffic-stats";
import { HomeProfileCard } from "@/components/home/home-profile-card";
import { ProxyTunCard } from "@/components/home/proxy-tun-card";
import { useProfiles } from "@/hooks/use-profiles";
import { useVerge } from "@/hooks/use-verge";
import { entry_lightweight_mode, openWebUrl } from "@/services/cmds";
const LazyTestCard = lazy(() =>
import("@/components/home/test-card").then((module) => ({
default: module.TestCard,
})),
);
const LazyIpInfoCard = lazy(() =>
import("@/components/home/ip-info-card").then((module) => ({
default: module.IpInfoCard,
})),
);
const LazyClashInfoCard = lazy(() =>
import("@/components/home/clash-info-card").then((module) => ({
default: module.ClashInfoCard,
})),
);
const LazySystemInfoCard = lazy(() =>
import("@/components/home/system-info-card").then((module) => ({
default: module.SystemInfoCard,
})),
);
// 定义首页卡片设置接口
interface HomeCardsSettings {
profile: boolean;
proxy: boolean;
network: boolean;
mode: boolean;
traffic: boolean;
info: boolean;
clashinfo: boolean;
systeminfo: boolean;
test: boolean;
ip: boolean;
[key: string]: boolean;
}
// 首页设置对话框组件接口
interface HomeSettingsDialogProps {
open: boolean;
onClose: () => void;
homeCards: HomeCardsSettings;
onSave: (cards: HomeCardsSettings) => void;
}
const serializeCardFlags = (cards: HomeCardsSettings) =>
Object.keys(cards)
.sort()
.map((key) => `${key}:${cards[key] ? 1 : 0}`)
.join("|");
// 首页设置对话框组件
const HomeSettingsDialog = ({
open,
onClose,
homeCards,
onSave,
}: HomeSettingsDialogProps) => {
const { t } = useTranslation();
const [cards, setCards] = useState<HomeCardsSettings>(homeCards);
const { patchVerge } = useVerge();
const handleToggle = (key: string) => {
setCards((prev: HomeCardsSettings) => ({
...prev,
[key]: !prev[key],
}));
};
const handleSave = async () => {
await patchVerge({ home_cards: cards });
onSave(cards);
onClose();
};
return (
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>{t("home.page.settings.title")}</DialogTitle>
<DialogContent>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={cards.profile || false}
onChange={() => handleToggle("profile")}
/>
}
label={t("home.page.settings.cards.profile")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.proxy || false}
onChange={() => handleToggle("proxy")}
/>
}
label={t("home.page.settings.cards.currentProxy")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.network || false}
onChange={() => handleToggle("network")}
/>
}
label={t("home.page.settings.cards.network")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.mode || false}
onChange={() => handleToggle("mode")}
/>
}
label={t("home.page.settings.cards.proxyMode")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.traffic || false}
onChange={() => handleToggle("traffic")}
/>
}
label={t("home.page.settings.cards.traffic")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.test || false}
onChange={() => handleToggle("test")}
/>
}
label={t("home.page.settings.cards.tests")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.ip || false}
onChange={() => handleToggle("ip")}
/>
}
label={t("home.page.settings.cards.ip")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.clashinfo || false}
onChange={() => handleToggle("clashinfo")}
/>
}
label={t("home.page.settings.cards.clashInfo")}
/>
<FormControlLabel
control={
<Checkbox
checked={cards.systeminfo || false}
onChange={() => handleToggle("systeminfo")}
/>
}
label={t("home.page.settings.cards.systemInfo")}
/>
</FormGroup>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t("shared.actions.cancel")}</Button>
<Button onClick={handleSave} color="primary">
{t("shared.actions.save")}
</Button>
</DialogActions>
</Dialog>
);
};
const HomePage = () => {
const { t } = useTranslation();
const { verge } = useVerge();
const { current, mutateProfiles } = useProfiles();
// 设置弹窗的状态
const [settingsOpen, setSettingsOpen] = useState(false);
const [localHomeCards, setLocalHomeCards] = useState<{
value: HomeCardsSettings;
baseSignature: string;
} | null>(null);
// 卡片显示状态
const defaultCards = useMemo<HomeCardsSettings>(
() => ({
info: false,
profile: true,
proxy: true,
network: true,
mode: true,
traffic: true,
clashinfo: true,
systeminfo: true,
test: true,
ip: true,
}),
[],
);
const vergeHomeCards = useMemo<HomeCardsSettings | null>(
() => (verge?.home_cards as HomeCardsSettings | undefined) ?? null,
[verge],
);
const remoteHomeCards = useMemo<HomeCardsSettings>(
() => vergeHomeCards ?? defaultCards,
[defaultCards, vergeHomeCards],
);
const remoteSignature = useMemo(
() => serializeCardFlags(remoteHomeCards),
[remoteHomeCards],
);
const pendingLocalCards = useMemo<HomeCardsSettings | null>(() => {
if (!localHomeCards) return null;
return localHomeCards.baseSignature === remoteSignature
? localHomeCards.value
: null;
}, [localHomeCards, remoteSignature]);
const effectiveHomeCards = pendingLocalCards ?? remoteHomeCards;
// 文档链接函数
const toGithubDoc = useLockFn(() => {
return openWebUrl("https://clash-verge-rev.github.io/index.html");
});
// 新增:打开设置弹窗
const openSettings = useCallback(() => {
setSettingsOpen(true);
}, []);
const renderCard = useCallback(
(cardKey: string, component: React.ReactNode, size: number = 6) => {
if (!effectiveHomeCards[cardKey]) return null;
return (
<Grid size={size} key={cardKey}>
{component}
</Grid>
);
},
[effectiveHomeCards],
);
const criticalCards = useMemo(
() => [
renderCard(
"profile",
<HomeProfileCard current={current} onProfileUpdated={mutateProfiles} />,
),
renderCard("proxy", <CurrentProxyCard />),
renderCard("network", <NetworkSettingsCard />),
renderCard("mode", <ClashModeEnhancedCard />),
],
[current, mutateProfiles, renderCard],
);
// 新增保存设置时用requestIdleCallback/setTimeout
const handleSaveSettings = (newCards: HomeCardsSettings) => {
if (window.requestIdleCallback) {
window.requestIdleCallback(() =>
setLocalHomeCards({
value: newCards,
baseSignature: remoteSignature,
}),
);
} else {
setTimeout(
() =>
setLocalHomeCards({
value: newCards,
baseSignature: remoteSignature,
}),
0,
);
}
};
const nonCriticalCards = useMemo(
() => [
renderCard(
"traffic",
<EnhancedCard
title={t("home.page.cards.trafficStats")}
icon={<SpeedOutlined />}
iconColor="secondary"
>
<EnhancedTrafficStats />
</EnhancedCard>,
12,
),
renderCard(
"test",
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
<LazyTestCard />
</Suspense>,
),
renderCard(
"ip",
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
<LazyIpInfoCard />
</Suspense>,
),
renderCard(
"clashinfo",
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
<LazyClashInfoCard />
</Suspense>,
),
renderCard(
"systeminfo",
<Suspense fallback={<Skeleton variant="rectangular" height={200} />}>
<LazySystemInfoCard />
</Suspense>,
),
],
[t, renderCard],
);
const dialogKey = useMemo(
() => `${serializeCardFlags(effectiveHomeCards)}:${settingsOpen ? 1 : 0}`,
[effectiveHomeCards, settingsOpen],
);
return (
<BasePage
title={t("home.page.title")}
contentStyle={{ padding: 2 }}
header={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Tooltip title={t("home.page.tooltips.lightweightMode")} arrow>
<IconButton
onClick={async () => await entry_lightweight_mode()}
size="small"
color="inherit"
>
<HistoryEduOutlined />
</IconButton>
</Tooltip>
<Tooltip title={t("home.page.tooltips.manual")} arrow>
<IconButton onClick={toGithubDoc} size="small" color="inherit">
<HelpOutlineRounded />
</IconButton>
</Tooltip>
<Tooltip title={t("home.page.tooltips.settings")} arrow>
<IconButton onClick={openSettings} size="small" color="inherit">
<SettingsOutlined />
</IconButton>
</Tooltip>
</Box>
}
>
<Grid container spacing={1.5} columns={{ xs: 6, sm: 6, md: 12 }}>
{criticalCards}
{nonCriticalCards}
</Grid>
{/* 首页设置弹窗 */}
<HomeSettingsDialog
key={dialogKey}
open={settingsOpen}
onClose={() => setSettingsOpen(false)}
homeCards={effectiveHomeCards}
onSave={handleSaveSettings}
/>
</BasePage>
);
};
// 增强版网络设置卡片组件
const NetworkSettingsCard = () => {
const { t } = useTranslation();
return (
<EnhancedCard
title={t("home.page.cards.networkSettings")}
icon={<DnsOutlined />}
iconColor="primary"
action={null}
>
<ProxyTunCard />
</EnhancedCard>
);
};
// 增强版 Clash 模式卡片组件
const ClashModeEnhancedCard = () => {
const { t } = useTranslation();
return (
<EnhancedCard
title={t("home.page.cards.proxyMode")}
icon={<RouterOutlined />}
iconColor="info"
action={null}
>
<ClashModeCard />
</EnhancedCard>
);
};
export default HomePage;