fix(i18n): replace hardcoded Chinese messages with i18n keys and extract AppError

This commit is contained in:
VirtualHotBar
2026-05-03 22:42:22 +08:00
parent 5be20c1855
commit 55ffcbb4c1
7 changed files with 138 additions and 335 deletions

View File

@@ -708,6 +708,37 @@
"confirm_import": "Confirm Import",
"export_import_config_description": "Export current configuration or restore configuration from file",
"confirm_import_description": "Importing configuration will overwrite all current settings and automatically restart the software. Continue?",
"password_obscured_hint": "Password is obscured by rclone and cannot be displayed. Please re-enter to modify"
"password_obscured_hint": "Password is obscured by rclone and cannot be displayed. Please re-enter to modify",
"error_boundary_title": "An error occurred",
"error_boundary_subtitle": "The application encountered a problem, but your data is safe",
"reload": "Reload",
"validation_storage_name_empty": "Storage name cannot be empty",
"validation_storage_name_too_long": "Storage name length cannot exceed 128 characters",
"validation_storage_name_invalid_chars": "Storage name contains invalid characters",
"validation_storage_type_empty": "Storage type cannot be empty",
"validation_storage_params_empty": "Storage parameters cannot be empty",
"validation_input_invalid": "Invalid input parameters",
"error_unsupported_storage_type": "Unsupported storage type",
"error_storage_params_serialization": "Storage parameter serialization failed",
"error_storage_id_not_found": "Cannot get storage ID",
"error_operation_failed": "Operation failed",
"error_unsupported_framework": "Unsupported storage framework",
"error_storage_network_failure": "Storage operation failed, please check network connection",
"error_network": "Network connection failed, please check network settings",
"error_api": "Server request failed, please try again later",
"error_validation": "Invalid input data, please check and try again",
"error_auth": "Login expired, please login again",
"error_forbidden": "No permission to perform this operation",
"error_not_found": "Requested resource not found",
"error_timeout": "Request timeout, please try again later",
"error_file_system": "File operation failed",
"error_config": "Configuration error",
"error_unknown": "An unknown error occurred, please try again later",
"error_auth_failed": "Authentication failed",
"error_no_permission": "No permission",
"error_resource_not_found": "{{resource}} not found",
"error_operation_timeout": "{{operation}} operation timeout",
"no_logs": "No logs"
}

View File

@@ -718,5 +718,36 @@
"proxy_range": "Proxy Range",
"app_id": "App ID",
"sign_key": "Sign Key",
"password_obscured_hint": "密码已被rclone混淆存储无法显示原始值如需修改请重新输入"
"password_obscured_hint": "密码已被rclone混淆存储无法显示原始值如需修改请重新输入",
"error_boundary_title": "出现错误",
"error_boundary_subtitle": "应用遇到了问题,但别担心,数据是安全的",
"reload": "重新加载",
"validation_storage_name_empty": "存储名称不能为空",
"validation_storage_name_too_long": "存储名称长度不能超过128字符",
"validation_storage_name_invalid_chars": "存储名称包含非法字符",
"validation_storage_type_empty": "存储类型不能为空",
"validation_storage_params_empty": "存储参数不能为空",
"validation_input_invalid": "输入参数无效",
"error_unsupported_storage_type": "不支持的存储类型",
"error_storage_params_serialization": "存储参数序列化失败",
"error_storage_id_not_found": "无法获取存储 ID",
"error_operation_failed": "操作失败",
"error_unsupported_framework": "不支持的存储框架",
"error_storage_network_failure": "存储操作失败,请检查网络连接",
"error_network": "网络连接失败,请检查网络设置",
"error_api": "服务器请求失败,请稍后重试",
"error_validation": "输入数据有误,请检查后重试",
"error_auth": "登录已过期,请重新登录",
"error_forbidden": "没有权限执行此操作",
"error_not_found": "请求的资源不存在",
"error_timeout": "请求超时,请稍后重试",
"error_file_system": "文件操作失败",
"error_config": "配置错误",
"error_unknown": "发生未知错误,请稍后重试",
"error_auth_failed": "认证失败",
"error_no_permission": "没有权限",
"error_resource_not_found": "{{resource}} 不存在",
"error_operation_timeout": "{{operation}} 操作超时",
"no_logs": "暂无日志"
}

View File

@@ -708,6 +708,37 @@
"confirm_import": "確認導入",
"export_import_config_description": "導出當前配置或者從文件恢復配置",
"confirm_import_description": "導入配置將覆蓋當前所有配置並自動重啓軟件,是否繼續?",
"password_obscured_hint": "密碼已被rclone混淆存儲無法顯示原始值如需修改請重新輸入"
"password_obscured_hint": "密碼已被rclone混淆存儲無法顯示原始值如需修改請重新輸入",
"error_boundary_title": "出現錯誤",
"error_boundary_subtitle": "應用遇到了問題,但不用擔心,數據是安全的",
"reload": "重新載入",
"validation_storage_name_empty": "存儲名稱不能為空",
"validation_storage_name_too_long": "存儲名稱長度不能超過128字符",
"validation_storage_name_invalid_chars": "存儲名稱包含非法字符",
"validation_storage_type_empty": "存儲類型不能為空",
"validation_storage_params_empty": "存儲參數不能為空",
"validation_input_invalid": "輸入參數無效",
"error_unsupported_storage_type": "不支持的存儲類型",
"error_storage_params_serialization": "存儲參數序列化失敗",
"error_storage_id_not_found": "無法獲取存儲 ID",
"error_operation_failed": "操作失敗",
"error_unsupported_framework": "不支持的存儲框架",
"error_storage_network_failure": "存儲操作失敗,請檢查網絡連接",
"error_network": "網絡連接失敗,請檢查網絡設置",
"error_api": "服務器請求失敗,請稍後重試",
"error_validation": "輸入數據有誤,請檢查後重試",
"error_auth": "登錄已過期,請重新登錄",
"error_forbidden": "沒有權限執行此操作",
"error_not_found": "請求的資源不存在",
"error_timeout": "請求超時,請稍後重試",
"error_file_system": "文件操作失敗",
"error_config": "配置錯誤",
"error_unknown": "發生未知錯誤,請稍後重試",
"error_auth_failed": "認證失敗",
"error_no_permission": "沒有權限",
"error_resource_not_found": "{{resource}} 不存在",
"error_operation_timeout": "{{operation}} 操作超時",
"no_logs": "暫無日誌"
}

View File

@@ -17,6 +17,7 @@
*/
import { Component, ErrorInfo, ReactNode, JSX } from 'react'
import { useTranslation } from 'react-i18next'
import { Button, Card, Result, Space, Typography } from '@arco-design/web-react'
import { IconRefresh, IconBug } from '@arco-design/web-react/icon'
import { logger } from '../services/LoggerService'
@@ -54,6 +55,7 @@ interface DefaultErrorFallbackProps {
}
function DefaultErrorFallback({ error, onReset }: DefaultErrorFallbackProps): JSX.Element {
const { t } = useTranslation()
return (
<div
style={{
@@ -70,8 +72,8 @@ function DefaultErrorFallback({ error, onReset }: DefaultErrorFallbackProps): JS
<Result
status="error"
icon={<IconBug style={{ fontSize: '3rem', color: 'var(--color-danger)' }} />}
title="出现错误"
subTitle="应用遇到了问题,但别担心,数据是安全的"
title={t('error_boundary_title')}
subTitle={t('error_boundary_subtitle')}
>
<Space direction="vertical" style={{ width: '100%' }}>
<div
@@ -94,7 +96,7 @@ function DefaultErrorFallback({ error, onReset }: DefaultErrorFallbackProps): JS
</div>
<Space style={{ justifyContent: 'center', width: '100%', marginTop: '1rem' }}>
<Button type="primary" icon={<IconRefresh />} onClick={onReset}>
{t('reload')}
</Button>
</Space>
</Space>

View File

@@ -1,4 +1,5 @@
import { Message } from '@arco-design/web-react'
import { t } from 'i18next'
import { ParametersType } from '../../type/defaults'
import { openlist_api_post } from '../../utils/openlist/request'
import { rclone_api_post } from '../../utils/rclone/request'
@@ -12,17 +13,16 @@ import { logger } from '../../services/LoggerService'
*/
function validateStorageName(name: string): string | null {
if (!name || typeof name !== 'string') {
return '存储名称不能为空'
return t('validation_storage_name_empty')
}
if (name.trim().length === 0) {
return '存储名称不能为空'
return t('validation_storage_name_empty')
}
if (name.length > 128) {
return '存储名称长度不能超过128字符'
return t('validation_storage_name_too_long')
}
// 检查非法字符
if (/[<>:"|?*/\\]/.test(name)) {
return '存储名称包含非法字符'
return t('validation_storage_name_invalid_chars')
}
return null
}
@@ -32,7 +32,7 @@ function validateStorageName(name: string): string | null {
*/
function validateStorageType(type: string): string | null {
if (!type || typeof type !== 'string') {
return '存储类型不能为空'
return t('validation_storage_type_empty')
}
return null
}
@@ -42,7 +42,7 @@ function validateStorageType(type: string): string | null {
*/
function validateParameters(parameters: ParametersType): string | null {
if (!parameters || typeof parameters !== 'object') {
return '存储参数不能为空'
return t('validation_storage_params_empty')
}
return null
}
@@ -77,14 +77,14 @@ async function createStorage(
// 输入验证
const validation = validateStorageInput(name, type, parameters)
if (!validation.valid) {
Message.error(validation.error || '输入参数无效')
Message.error(validation.error || t('validation_input_invalid'))
logger.error('Storage validation failed', undefined, 'StorageCreate', { error: validation.error })
return false
}
const storageInfo = searchStorageInfo(type)
if (!storageInfo) {
Message.error('不支持的存储类型: ' + type)
Message.error(t('error_unsupported_storage_type') + ': ' + type)
logger.error('Storage type not found', undefined, 'StorageCreate', { type })
return false
}
@@ -117,7 +117,7 @@ async function createStorage(
serializedAddition = JSON.stringify(parameters.addition)
} catch (e) {
logger.error('Failed to serialize addition', e as Error, 'StorageCreate')
Message.error('存储参数序列化失败')
Message.error(t('error_storage_params_serialization'))
return false
}
@@ -135,7 +135,7 @@ async function createStorage(
// 更新现有存储
const storageId = storage.other?.openlist?.id
if (!storageId) {
Message.error('无法获取存储 ID')
Message.error(t('error_storage_id_not_found'))
return false
}
backData = await openlist_api_post('/api/admin/storage/update', {
@@ -145,7 +145,7 @@ async function createStorage(
}
if (backData.code !== 200) {
Message.error(backData.message || '操作失败')
Message.error(backData.message || t('error_operation_failed'))
return false
}
@@ -154,12 +154,12 @@ async function createStorage(
}
default:
Message.error('不支持的存储框架: ' + storageInfo.framework)
Message.error(t('error_unsupported_framework') + ': ' + storageInfo.framework)
return false
}
} catch (error) {
logger.error('Storage operation failed', error as Error, 'StorageCreate')
Message.error('存储操作失败,请检查网络连接')
Message.error(t('error_storage_network_failure'))
return false
}
}

View File

@@ -23,7 +23,7 @@ export default function Setting_page() {
const showLogFromFileTail = async (path: string) => {
try {
const content = await readTextFileTail(path, { maxBytes: 256 * 1024, allowMissing: true })
showLog(modal, (content || '').trim() ? content : '暂无日志')
showLog(modal, (content || '').trim() ? content : t('no_logs'))
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
Message.error(msg)

View File

@@ -2,239 +2,18 @@
* Error Service - 统一的错误处理服务
*
* 特性:
* 1. 统一的错误类型和分类
* 2. 错误转换和包装
* 3. 用户友好的错误消息支持i18n
* 4. 自动UI提示Message/Notification
* 5. 内存错误日志
* 6. 与 LoggerService 集成
* 1. 统一的错误处理
* 2. 自动UI提示Message/Notification
* 3. 内存错误日志
* 4. 与 LoggerService 集成
*/
import { Message, Notification } from '@arco-design/web-react'
import { t } from 'i18next'
import { logger } from './LoggerService'
import { AppError, ErrorCategory, ErrorSeverity } from './AppError'
// ============================================
// 错误类型枚举
// ============================================
export enum ErrorCategory {
NETWORK = 'NETWORK', // 网络错误
API = 'API', // API 错误
VALIDATION = 'VALIDATION', // 验证错误
AUTHENTICATION = 'AUTH', // 认证错误
AUTHORIZATION = 'FORBIDDEN', // 权限错误
NOT_FOUND = 'NOT_FOUND', // 资源不存在
TIMEOUT = 'TIMEOUT', // 超时错误
FILE_SYSTEM = 'FILE_SYSTEM', // 文件系统错误
CONFIGURATION = 'CONFIG', // 配置错误
UNKNOWN = 'UNKNOWN', // 未知错误
}
// ============================================
// 错误严重级别
// ============================================
export enum ErrorSeverity {
LOW = 'low', // 轻微问题,可忽略
MEDIUM = 'medium', // 一般问题,需要关注
HIGH = 'high', // 严重问题,需要处理
CRITICAL = 'critical', // 关键问题,影响核心功能
}
// ============================================
// 应用错误类
// ============================================
export class AppError extends Error {
readonly category: ErrorCategory
readonly severity: ErrorSeverity
readonly code: string
readonly context?: Record<string, unknown>
readonly timestamp: Date
readonly originalError?: Error
constructor(
message: string,
category: ErrorCategory = ErrorCategory.UNKNOWN,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
code: string = 'UNKNOWN_ERROR',
context?: Record<string, unknown>,
originalError?: Error
) {
super(message)
this.name = 'AppError'
this.category = category
this.severity = severity
this.code = code
this.context = context
this.timestamp = new Date()
this.originalError = originalError
// 保持堆栈跟踪
if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, AppError)
}
}
/**
* 获取用户友好的错误消息(支持 i18n
*/
getUserMessage(): string {
// 尝试使用国际化消息
const i18nKey = `errors.${this.code}`
const i18nMessage = t(i18nKey, { defaultValue: '' })
if (i18nMessage) {
return i18nMessage
}
// 开发环境:记录缺失的 i18n key
if (import.meta.env.DEV && !i18nMessage) {
logger.warn(`Missing i18n key: ${i18nKey}`, 'ErrorService', { code: this.code, category: this.category })
}
// 回退到默认分类消息
const messages: Record<ErrorCategory, string> = {
[ErrorCategory.NETWORK]: '网络连接失败,请检查网络设置',
[ErrorCategory.API]: '服务器请求失败,请稍后重试',
[ErrorCategory.VALIDATION]: '输入数据有误,请检查后重试',
[ErrorCategory.AUTHENTICATION]: '登录已过期,请重新登录',
[ErrorCategory.AUTHORIZATION]: '没有权限执行此操作',
[ErrorCategory.NOT_FOUND]: '请求的资源不存在',
[ErrorCategory.TIMEOUT]: '请求超时,请稍后重试',
[ErrorCategory.FILE_SYSTEM]: '文件操作失败',
[ErrorCategory.CONFIGURATION]: '配置错误',
[ErrorCategory.UNKNOWN]: '发生未知错误,请稍后重试',
}
return messages[this.category] || this.message
}
/**
* 是否应该显示给用户
*/
shouldShowToUser(): boolean {
return this.severity !== ErrorSeverity.LOW
}
/**
* 是否应该上报
*/
shouldReport(): boolean {
return this.severity === ErrorSeverity.HIGH || this.severity === ErrorSeverity.CRITICAL
}
/**
* 转换为 JSON 对象(生产环境不包含 stack trace
*/
toJSON(): Record<string, unknown> {
return {
name: this.name,
message: this.message,
category: this.category,
severity: this.severity,
code: this.code,
// 仅在开发环境包含 context生产环境可能包含敏感数据
context: import.meta.env.DEV ? this.context : undefined,
timestamp: this.timestamp.toISOString(),
// 仅在开发环境包含 stack trace生产环境不暴露内部实现细节
stack: import.meta.env.DEV ? this.stack : undefined,
}
}
// ==========================================
// 工厂方法
// ==========================================
static network(message: string, original?: Error, context?: Record<string, unknown>): AppError {
return new AppError(
message,
ErrorCategory.NETWORK,
ErrorSeverity.HIGH,
'NETWORK_ERROR',
context,
original
)
}
static api(
message: string,
code: string = 'API_ERROR',
context?: Record<string, unknown>,
original?: Error
): AppError {
return new AppError(message, ErrorCategory.API, ErrorSeverity.HIGH, code, context, original)
}
static validation(
message: string,
field?: string,
context?: Record<string, unknown>
): AppError {
return new AppError(
message,
ErrorCategory.VALIDATION,
ErrorSeverity.MEDIUM,
'VALIDATION_ERROR',
{ ...context, field }
)
}
static auth(message: string = '认证失败'): AppError {
return new AppError(message, ErrorCategory.AUTHENTICATION, ErrorSeverity.HIGH, 'AUTH_ERROR')
}
static forbidden(message: string = '没有权限'): AppError {
return new AppError(
message,
ErrorCategory.AUTHORIZATION,
ErrorSeverity.HIGH,
'FORBIDDEN_ERROR'
)
}
static notFound(resource: string, context?: Record<string, unknown>): AppError {
return new AppError(
`${resource} 不存在`,
ErrorCategory.NOT_FOUND,
ErrorSeverity.MEDIUM,
'NOT_FOUND_ERROR',
context
)
}
static timeout(operation: string, timeoutMs: number): AppError {
return new AppError(
`${operation} 操作超时`,
ErrorCategory.TIMEOUT,
ErrorSeverity.HIGH,
'TIMEOUT_ERROR',
{ timeout: timeoutMs }
)
}
static fileSystem(message: string, original?: Error): AppError {
return new AppError(
message,
ErrorCategory.FILE_SYSTEM,
ErrorSeverity.HIGH,
'FILE_SYSTEM_ERROR',
undefined,
original
)
}
static config(message: string): AppError {
return new AppError(
message,
ErrorCategory.CONFIGURATION,
ErrorSeverity.CRITICAL,
'CONFIG_ERROR'
)
}
}
// ============================================
// 错误处理器类型
// ============================================
type ErrorHandler = (error: AppError) => void | Promise<void>
export { AppError, ErrorCategory, ErrorSeverity }
// ============================================
// 错误处理配置
@@ -257,15 +36,12 @@ const DEFAULT_HANDLER_CONFIG: ErrorHandlerConfig = {
// Error Service
// ============================================
class ErrorService {
private handlers: ErrorHandler[] = []
private handlers: Array<(error: AppError) => void | Promise<void>> = []
private globalHandler?: (error: AppError) => void
private errorLog: AppError[] = []
private maxLogSize = 100
/**
* 注册错误处理器
*/
onError(handler: ErrorHandler): () => void {
onError(handler: (error: AppError) => void | Promise<void>): () => void {
this.handlers.push(handler)
return () => {
const index = this.handlers.indexOf(handler)
@@ -275,16 +51,10 @@ class ErrorService {
}
}
/**
* 设置全局错误处理器
*/
setGlobalHandler(handler: (error: AppError) => void): void {
this.globalHandler = handler
}
/**
* 处理错误
*/
async handle(
error: unknown,
context?: string,
@@ -293,35 +63,25 @@ class ErrorService {
const mergedConfig = { ...DEFAULT_HANDLER_CONFIG, ...config }
const appError = this.normalize(error)
// 记录错误到日志服务
logger.error(
appError.message,
appError.originalError,
context || 'ErrorService',
{
category: appError.category,
severity: appError.severity,
code: appError.code,
...appError.context,
}
)
logger.error(appError.message, appError.originalError, context || 'ErrorService', {
category: appError.category,
severity: appError.severity,
code: appError.code,
...appError.context,
})
// 记录到内存日志
if (mergedConfig.logToMemory) {
this.logToMemory(appError)
}
// 显示UI提示
if (mergedConfig.showMessage) {
this.showMessage(appError)
}
// 显示通知(仅严重错误)
if (mergedConfig.showNotification && appError.severity === ErrorSeverity.CRITICAL) {
this.showNotification(appError)
}
// 调用注册的处理器
for (const handler of this.handlers) {
try {
await handler(appError)
@@ -330,12 +90,10 @@ class ErrorService {
}
}
// 调用全局处理器
if (this.globalHandler) {
this.globalHandler(appError)
}
// 是否重新抛出
if (mergedConfig.rethrow) {
throw appError
}
@@ -343,9 +101,6 @@ class ErrorService {
return appError
}
/**
* 记录到内存日志
*/
private logToMemory(error: AppError): void {
this.errorLog.unshift(error)
if (this.errorLog.length > this.maxLogSize) {
@@ -353,9 +108,6 @@ class ErrorService {
}
}
/**
* 显示消息提示
*/
private showMessage(error: AppError): void {
if (error.severity === ErrorSeverity.CRITICAL || error.severity === ErrorSeverity.HIGH) {
Message.error(error.getUserMessage())
@@ -364,9 +116,6 @@ class ErrorService {
}
}
/**
* 显示通知
*/
private showNotification(error: AppError): void {
Notification.error({
title: t('error'),
@@ -375,54 +124,29 @@ class ErrorService {
})
}
/**
* 获取错误日志
*/
getErrorLog(): AppError[] {
return [...this.errorLog]
}
/**
* 清除错误日志
*/
clearErrorLog(): void {
this.errorLog = []
}
/**
* 标准化错误为 AppError
*/
normalize(error: unknown): AppError {
if (error instanceof AppError) {
return error
}
if (error instanceof Error) {
return new AppError(
error.message,
ErrorCategory.UNKNOWN,
ErrorSeverity.MEDIUM,
'UNKNOWN_ERROR',
{ name: error.name },
error
)
return new AppError(error.message, ErrorCategory.UNKNOWN, ErrorSeverity.MEDIUM, 'UNKNOWN_ERROR', {
name: error.name,
}, error)
}
return new AppError(
String(error),
ErrorCategory.UNKNOWN,
ErrorSeverity.MEDIUM,
'UNKNOWN_ERROR'
)
return new AppError(String(error), ErrorCategory.UNKNOWN, ErrorSeverity.MEDIUM, 'UNKNOWN_ERROR')
}
/**
* 包装异步函数,自动处理错误
*/
wrap<T>(
fn: () => Promise<T>,
errorHandler?: (error: AppError) => void
): Promise<T | undefined> {
wrap<T>(fn: () => Promise<T>, errorHandler?: (error: AppError) => void): Promise<T | undefined> {
return fn().catch((error: unknown) => {
const appError = this.normalize(error)
this.handle(appError)
@@ -433,9 +157,6 @@ class ErrorService {
})
}
/**
* 创建安全的异步函数包装器
*/
safe<T extends (...args: unknown[]) => Promise<unknown>>(
fn: T,
context?: string
@@ -450,23 +171,12 @@ class ErrorService {
}
}
/**
* 验证条件,失败时抛出错误
*/
assert(condition: boolean, message: string, category?: ErrorCategory): void {
if (!condition) {
throw new AppError(
message,
category || ErrorCategory.VALIDATION,
ErrorSeverity.MEDIUM,
'ASSERTION_ERROR'
)
throw new AppError(message, category || ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM, 'ASSERTION_ERROR')
}
}
/**
* 验证值是否存在,不存在时抛出错误
*/
assertExists<T>(value: T | null | undefined, name: string): T {
if (value === null || value === undefined) {
throw AppError.notFound(name)
@@ -486,10 +196,8 @@ export const errorService = new ErrorService()
export const handleError = (error: unknown, context?: string): Promise<AppError> =>
errorService.handle(error, context)
export const safe = <T extends (...args: unknown[]) => Promise<unknown>>(
fn: T,
context?: string
) => errorService.safe(fn, context)
export const safe = <T extends (...args: unknown[]) => Promise<unknown>>(fn: T, context?: string) =>
errorService.safe(fn, context)
export const assert = (condition: boolean, message: string, category?: ErrorCategory): void =>
errorService.assert(condition, message, category)