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
+4 -2
View File
@@ -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>
+40 -34
View File
@@ -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];
+8 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -48,7 +48,7 @@ const RulesPage = () => {
return (
<BasePage
full
title={t("Rules")}
title={t("rules.page.title")}
contentStyle={{
height: "100%",
display: "flex",
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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)}