error-messages.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. /**
  2. * Error Code Mapping System for i18n Error Messages
  3. *
  4. * This module provides a centralized error code system that enables dynamic error message
  5. * translation across the application. Error codes are mapped to translated messages on the
  6. * client side, supporting parameter interpolation for context-specific errors.
  7. *
  8. * @example
  9. * // Server action returns error code
  10. * return { ok: false, errorCode: "USER_NAME_REQUIRED" };
  11. *
  12. * // Client translates error code
  13. * const t = useTranslations("errors");
  14. * const message = t(result.errorCode); // "User name is required"
  15. */
  16. /**
  17. * Error Code Categories
  18. *
  19. * Organized by domain for maintainability:
  20. * - VALIDATION_*: Form validation errors
  21. * - AUTH_*: Authentication/authorization errors
  22. * - SERVER_*: Server-side errors
  23. * - NETWORK_*: Network/connection errors
  24. * - BUSINESS_*: Business logic errors
  25. */
  26. // Validation Error Codes
  27. export const VALIDATION_ERRORS = {
  28. // Required field errors
  29. REQUIRED_FIELD: "REQUIRED_FIELD",
  30. USER_NAME_REQUIRED: "USER_NAME_REQUIRED",
  31. API_KEY_REQUIRED: "API_KEY_REQUIRED",
  32. PROVIDER_NAME_REQUIRED: "PROVIDER_NAME_REQUIRED",
  33. PROVIDER_URL_REQUIRED: "PROVIDER_URL_REQUIRED",
  34. // String validation errors
  35. MIN_LENGTH: "MIN_LENGTH",
  36. MAX_LENGTH: "MAX_LENGTH",
  37. INVALID_EMAIL: "INVALID_EMAIL",
  38. INVALID_URL: "INVALID_URL",
  39. // Number validation errors
  40. MIN_VALUE: "MIN_VALUE",
  41. MAX_VALUE: "MAX_VALUE",
  42. MUST_BE_INTEGER: "MUST_BE_INTEGER",
  43. MUST_BE_POSITIVE: "MUST_BE_POSITIVE",
  44. // Type errors
  45. INVALID_TYPE: "INVALID_TYPE",
  46. INVALID_FORMAT: "INVALID_FORMAT",
  47. // Business validation
  48. DUPLICATE_NAME: "DUPLICATE_NAME",
  49. INVALID_RANGE: "INVALID_RANGE",
  50. EMPTY_UPDATE: "EMPTY_UPDATE",
  51. } as const;
  52. // Authentication Error Codes
  53. export const AUTH_ERRORS = {
  54. UNAUTHORIZED: "UNAUTHORIZED",
  55. INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
  56. SESSION_EXPIRED: "SESSION_EXPIRED",
  57. PERMISSION_DENIED: "PERMISSION_DENIED",
  58. TOKEN_REQUIRED: "TOKEN_REQUIRED",
  59. INVALID_TOKEN: "INVALID_TOKEN",
  60. } as const;
  61. // Server Error Codes
  62. export const SERVER_ERRORS = {
  63. INTERNAL_ERROR: "INTERNAL_ERROR",
  64. DATABASE_ERROR: "DATABASE_ERROR",
  65. NOT_FOUND: "NOT_FOUND",
  66. OPERATION_FAILED: "OPERATION_FAILED",
  67. CREATE_FAILED: "CREATE_FAILED",
  68. UPDATE_FAILED: "UPDATE_FAILED",
  69. DELETE_FAILED: "DELETE_FAILED",
  70. } as const;
  71. // Network Error Codes
  72. export const NETWORK_ERRORS = {
  73. CONNECTION_FAILED: "CONNECTION_FAILED",
  74. TIMEOUT: "TIMEOUT",
  75. NETWORK_ERROR: "NETWORK_ERROR",
  76. } as const;
  77. // Business Logic Error Codes
  78. export const BUSINESS_ERRORS = {
  79. QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
  80. RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
  81. RESOURCE_BUSY: "RESOURCE_BUSY",
  82. INVALID_STATE: "INVALID_STATE",
  83. CONFLICT: "CONFLICT",
  84. } as const;
  85. // Rate Limit Error Codes
  86. export const RATE_LIMIT_ERRORS = {
  87. RATE_LIMIT_RPM_EXCEEDED: "RATE_LIMIT_RPM_EXCEEDED",
  88. RATE_LIMIT_5H_EXCEEDED: "RATE_LIMIT_5H_EXCEEDED",
  89. RATE_LIMIT_WEEKLY_EXCEEDED: "RATE_LIMIT_WEEKLY_EXCEEDED",
  90. RATE_LIMIT_MONTHLY_EXCEEDED: "RATE_LIMIT_MONTHLY_EXCEEDED",
  91. RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED: "RATE_LIMIT_CONCURRENT_SESSIONS_EXCEEDED",
  92. RATE_LIMIT_DAILY_QUOTA_EXCEEDED: "RATE_LIMIT_DAILY_QUOTA_EXCEEDED",
  93. } as const;
  94. /**
  95. * All Error Codes Union
  96. */
  97. export const ERROR_CODES = {
  98. ...VALIDATION_ERRORS,
  99. ...AUTH_ERRORS,
  100. ...SERVER_ERRORS,
  101. ...NETWORK_ERRORS,
  102. ...BUSINESS_ERRORS,
  103. ...RATE_LIMIT_ERRORS,
  104. } as const;
  105. export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
  106. /**
  107. * Error Message Parameters
  108. *
  109. * Type-safe parameters for dynamic error messages.
  110. * Each error code can accept specific parameters for interpolation.
  111. */
  112. export type ErrorParams = {
  113. // String validation parameters
  114. MIN_LENGTH: { field?: string; min: number };
  115. MAX_LENGTH: { field?: string; max: number };
  116. // Number validation parameters
  117. MIN_VALUE: { field?: string; min: number };
  118. MAX_VALUE: { field?: string; max: number };
  119. // General parameters
  120. INVALID_RANGE: { field?: string; min: number; max: number };
  121. DUPLICATE_NAME: { name: string };
  122. // Default (no parameters)
  123. [key: string]: Record<string, string | number> | undefined;
  124. };
  125. /**
  126. * Get translated error message (Client-side)
  127. *
  128. * @param t - Translation function from useTranslations("errors")
  129. * @param code - Error code
  130. * @param params - Optional parameters for interpolation
  131. * @returns Translated error message
  132. *
  133. * @example
  134. * const t = useTranslations("errors");
  135. * const message = getErrorMessage(t, "MIN_LENGTH", { field: "username", min: 3 });
  136. * // Returns: "Username must be at least 3 characters"
  137. */
  138. export function getErrorMessage(
  139. t: (key: string, params?: Record<string, string | number>) => string,
  140. code: ErrorCode | string,
  141. params?: Record<string, string | number>
  142. ): string {
  143. try {
  144. return t(code, params);
  145. } catch (error) {
  146. console.warn("Translation missing for error code", code, error);
  147. // Fallback to generic error message if translation key not found
  148. return t("INTERNAL_ERROR");
  149. }
  150. }
  151. /**
  152. * Get translated error message (Server-side)
  153. *
  154. * @param locale - Current locale (e.g., "zh-CN", "en")
  155. * @param code - Error code
  156. * @param params - Optional parameters for interpolation
  157. * @returns Translated error message
  158. *
  159. * @example
  160. * import { getTranslations } from "next-intl/server";
  161. *
  162. * const locale = await getLocale();
  163. * const message = await getErrorMessageServer(locale, "MIN_LENGTH", { field: "username", min: 3 });
  164. */
  165. export async function getErrorMessageServer(
  166. locale: string,
  167. code: ErrorCode | string,
  168. params?: Record<string, string | number>
  169. ): Promise<string> {
  170. try {
  171. const { getTranslations } = await import("next-intl/server");
  172. const t = await getTranslations({ locale, namespace: "errors" });
  173. return t(code, params);
  174. } catch (error) {
  175. console.error("getErrorMessageServer failed", { locale, code, error });
  176. // Fallback to generic error message
  177. return "An error occurred";
  178. }
  179. }
  180. /**
  181. * Helper: Convert Zod error to error code
  182. *
  183. * Maps Zod's internal error types to our error code system.
  184. *
  185. * @param zodErrorCode - Zod error code (e.g., "too_small", "invalid_type")
  186. * @param params - Zod error parameters
  187. * @returns Error code and parameters
  188. */
  189. export function zodErrorToCode(
  190. zodErrorCode: string,
  191. params: Record<string, unknown>
  192. ): { code: ErrorCode; params?: Record<string, string | number> } {
  193. const { minimum, maximum, type, path } = params;
  194. const field = Array.isArray(path) ? path.join(".") : undefined;
  195. switch (zodErrorCode) {
  196. case "invalid_type":
  197. if (type === "string" && params.received === "undefined") {
  198. return { code: ERROR_CODES.REQUIRED_FIELD, params: { field: field || "field" } };
  199. }
  200. return { code: ERROR_CODES.INVALID_TYPE, params: { field: field || "field" } };
  201. case "too_small":
  202. if (typeof minimum === "number") {
  203. // 区分字符串长度和数值范围
  204. const isStringType = type === "string";
  205. return {
  206. code: isStringType ? ERROR_CODES.MIN_LENGTH : ERROR_CODES.MIN_VALUE,
  207. params: { field: field || "field", min: minimum },
  208. };
  209. }
  210. return { code: ERROR_CODES.MIN_VALUE };
  211. case "too_big":
  212. if (typeof maximum === "number") {
  213. // 区分字符串长度和数值范围
  214. const isStringType = type === "string";
  215. return {
  216. code: isStringType ? ERROR_CODES.MAX_LENGTH : ERROR_CODES.MAX_VALUE,
  217. params: { field: field || "field", max: maximum },
  218. };
  219. }
  220. return { code: ERROR_CODES.MAX_VALUE };
  221. case "invalid_string":
  222. if (params.validation === "email") {
  223. return { code: ERROR_CODES.INVALID_EMAIL };
  224. }
  225. if (params.validation === "url") {
  226. return { code: ERROR_CODES.INVALID_URL };
  227. }
  228. return { code: ERROR_CODES.INVALID_FORMAT };
  229. default:
  230. return { code: ERROR_CODES.INVALID_FORMAT };
  231. }
  232. }