chore: notice i18n

This commit is contained in:
Slinetrac
2025-10-31 13:04:21 +08:00
Unverified
parent 73e53eb33f
commit 534e8d3de6
7 changed files with 127 additions and 24 deletions

View File

@@ -1,6 +1,7 @@
import { CloseRounded } from "@mui/icons-material";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import React, { useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import {
subscribeNotices,
@@ -9,6 +10,7 @@ import {
} from "@/services/noticeService";
export const NoticeManager: React.FC = () => {
const { t } = useTranslation();
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
@@ -60,7 +62,12 @@ export const NoticeManager: React.FC = () => {
</IconButton>
}
>
{notice.message}
{notice.i18n
? t(notice.i18n.i18nKey, {
defaultValue: notice.i18n.fallback,
...(notice.i18n.params ?? {}),
})
: notice.message}
</Alert>
</Snackbar>
))}

View File

@@ -66,12 +66,16 @@ export const ProviderButton = () => {
await refreshProxy();
await refreshProxyProviders();
showNotice("success", `${name} 更新成功`);
showNotice("success", {
i18nKey: "notice.provider.updateSuccess",
params: { name },
});
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
const message = err?.message || err?.toString?.() || String(err);
showNotice("error", {
i18nKey: "notice.provider.updateFailed",
params: { name, message },
});
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
@@ -84,7 +88,9 @@ export const ProviderButton = () => {
// 获取所有provider的名称
const allProviders = Object.keys(proxyProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的代理提供者");
showNotice("info", {
i18nKey: "notice.provider.none",
});
return;
}
@@ -114,9 +120,15 @@ export const ProviderButton = () => {
await refreshProxy();
await refreshProxyProviders();
showNotice("success", "全部代理提供者更新成功");
showNotice("success", {
i18nKey: "notice.provider.allUpdated",
});
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
const message = err?.message || err?.toString?.() || String(err);
showNotice("error", {
i18nKey: "notice.provider.genericError",
params: { message },
});
} finally {
// 清除所有更新状态
setUpdating({});

View File

@@ -58,12 +58,16 @@ export const ProviderButton = () => {
await refreshRules();
await refreshRuleProviders();
showNotice("success", `${name} 更新成功`);
showNotice("success", {
i18nKey: "notice.provider.updateSuccess",
params: { name },
});
} catch (err: any) {
showNotice(
"error",
`${name} 更新失败: ${err?.message || err.toString()}`,
);
const message = err?.message || err?.toString?.() || String(err);
showNotice("error", {
i18nKey: "notice.provider.updateFailed",
params: { name, message },
});
} finally {
// 清除更新状态
setUpdating((prev) => ({ ...prev, [name]: false }));
@@ -76,7 +80,9 @@ export const ProviderButton = () => {
// 获取所有provider的名称
const allProviders = Object.keys(ruleProviders || {});
if (allProviders.length === 0) {
showNotice("info", "没有可更新的规则提供者");
showNotice("info", {
i18nKey: "notice.provider.none",
});
return;
}
@@ -106,9 +112,15 @@ export const ProviderButton = () => {
await refreshRules();
await refreshRuleProviders();
showNotice("success", "全部规则提供者更新成功");
showNotice("success", {
i18nKey: "notice.provider.allUpdated",
});
} catch (err: any) {
showNotice("error", `更新失败: ${err?.message || err.toString()}`);
const message = err?.message || err?.toString?.() || String(err);
showNotice("error", {
i18nKey: "notice.provider.genericError",
params: { message },
});
} finally {
// 清除所有更新状态
setUpdating({});

View File

@@ -203,6 +203,13 @@
"Close Connection": "Close Connection",
"Rules": "Rules",
"Rule Provider": "Rule Provider",
"notice.forceRefreshCompleted": "Force refresh completed",
"notice.emergencyRefreshFailed": "Emergency refresh failed: {{message}}",
"notice.provider.updateSuccess": "{{name}} updated successfully",
"notice.provider.updateFailed": "Failed to update {{name}}: {{message}}",
"notice.provider.genericError": "Update failed: {{message}}",
"notice.provider.none": "No providers available to update",
"notice.provider.allUpdated": "All providers updated successfully",
"Logs": "Logs",
"Pause": "Pause",
"Resume": "Resume",

View File

@@ -203,6 +203,13 @@
"Close Connection": "关闭连接",
"Rules": "规则",
"Rule Provider": "规则集合",
"notice.forceRefreshCompleted": "数据已强制刷新",
"notice.emergencyRefreshFailed": "紧急刷新失败: {{message}}",
"notice.provider.updateSuccess": "{{name}} 更新成功",
"notice.provider.updateFailed": "{{name}} 更新失败: {{message}}",
"notice.provider.genericError": "更新失败: {{message}}",
"notice.provider.none": "没有可更新的提供者",
"notice.provider.allUpdated": "全部提供者更新成功",
"Logs": "日志",
"Pause": "暂停",
"Resume": "继续",

View File

@@ -345,10 +345,18 @@ const ProfilePage = () => {
await new Promise((resolve) => setTimeout(resolve, 500));
await onEnhance(false);
showNotice("success", "数据已强制刷新", 2000);
showNotice("success", { i18nKey: "notice.forceRefreshCompleted" }, 2000);
} catch (error: any) {
console.error("[紧急刷新] 失败:", error);
showNotice("error", `紧急刷新失败: ${error.message}`, 4000);
const message = error?.message || String(error);
showNotice(
"error",
{
i18nKey: "notice.emergencyRefreshFailed",
params: { message },
},
4000,
);
}
});

View File

@@ -1,9 +1,20 @@
import { ReactNode } from "react";
type NoticeType = "success" | "error" | "info";
export interface NoticeTranslationDescriptor {
i18nKey: string;
params?: Record<string, unknown>;
fallback?: string;
}
type NoticeMessage = ReactNode | NoticeTranslationDescriptor;
interface NoticeItem {
id: number;
type: "success" | "error" | "info";
message: ReactNode;
type: NoticeType;
message?: ReactNode;
i18n?: NoticeTranslationDescriptor;
duration: number;
timerId?: ReturnType<typeof setTimeout>;
}
@@ -18,11 +29,25 @@ function notifyListeners() {
listeners.forEach((listener) => listener([...notices])); // Pass a copy
}
function isTranslationDescriptor(
message: NoticeMessage,
): message is NoticeTranslationDescriptor {
if (
typeof message === "object" &&
message !== null &&
Object.prototype.hasOwnProperty.call(message, "i18nKey")
) {
const descriptor = message as NoticeTranslationDescriptor;
return typeof descriptor.i18nKey === "string";
}
return false;
}
// Shows a notification.
export function showNotice(
type: "success" | "error" | "info",
message: ReactNode,
type: NoticeType,
message: NoticeMessage,
duration?: number,
): number {
const id = nextId++;
@@ -32,10 +57,15 @@ export function showNotice(
const newNotice: NoticeItem = {
id,
type,
message,
duration: effectiveDuration,
};
if (isTranslationDescriptor(message)) {
newNotice.i18n = message;
} else {
newNotice.message = message;
}
// Auto-hide timer (only if duration is not null/0)
if (effectiveDuration > 0) {
newNotice.timerId = setTimeout(() => {
@@ -48,6 +78,26 @@ export function showNotice(
return id;
}
export function showTranslatedNotice(
type: NoticeType,
i18nKey: string,
options?: {
params?: Record<string, unknown>;
fallback?: string;
},
duration?: number,
) {
return showNotice(
type,
{
i18nKey,
params: options?.params,
fallback: options?.fallback,
},
duration,
);
}
// Hides a specific notification by its ID.
export function hideNotice(id: number) {