thread-master/apps/web/src/lib/apiErrors.ts

192 lines
5.9 KiB
TypeScript
Raw Normal View History

2026-07-10 12:54:45 +00:00
/**
* envelope code / data.message
* UI
*/
import { useCallback } from "react";
import { useI18n } from "../i18n/I18nContext";
import { translate } from "./i18n/messages";
import { loadUiPrefs } from "./i18n/prefs";
import type { AppLocale } from "./i18n/types";
/** 與 backend internal/response + middleware 對齊 */
export const API_CODES = {
SUCCESS: 102000,
BAD_REQUEST: 400001,
WEAK_PASSWORD: 400003,
INVALID_CODE: 400004,
SELF_SUSPEND: 400020,
LAST_ADMIN: 400021,
MISSING_AUTH: 401001,
INVALID_TOKEN: 401002,
MEMBER_NOT_FOUND_AUTH: 401003,
BAD_PASSWORD: 401010,
SUSPENDED: 403001,
ADMIN_REQUIRED: 403002,
NOT_FOUND: 404001,
EMAIL_NOT_REGISTERED: 404002,
EMAIL_TAKEN: 409001,
SERVER: 500000,
NOT_IMPLEMENTED: 501000,
NETWORK: 0,
} as const;
/** code → messages.ts 的 key */
export const API_ERROR_I18N_KEY: Record<number, string> = {
[API_CODES.BAD_REQUEST]: "api.err.400001",
[API_CODES.WEAK_PASSWORD]: "api.err.400003",
[API_CODES.INVALID_CODE]: "api.err.400004",
[API_CODES.SELF_SUSPEND]: "api.err.400020",
[API_CODES.LAST_ADMIN]: "api.err.400021",
[API_CODES.MISSING_AUTH]: "api.err.401001",
[API_CODES.INVALID_TOKEN]: "api.err.401002",
[API_CODES.MEMBER_NOT_FOUND_AUTH]: "api.err.401003",
[API_CODES.BAD_PASSWORD]: "api.err.401010",
[API_CODES.SUSPENDED]: "api.err.403001",
[API_CODES.ADMIN_REQUIRED]: "api.err.403002",
[API_CODES.NOT_FOUND]: "api.err.404001",
[API_CODES.EMAIL_NOT_REGISTERED]: "api.err.404002",
[API_CODES.EMAIL_TAKEN]: "api.err.409001",
[API_CODES.SERVER]: "api.err.500000",
[API_CODES.NOT_IMPLEMENTED]: "api.err.501000",
[API_CODES.NETWORK]: "api.err.network",
};
export function i18nKeyForApiCode(code: number): string | undefined {
return API_ERROR_I18N_KEY[code];
}
/**
* data.message key
* apps/backend internal/logic/** Message
*/
const API_OK_MESSAGE_KEY: Record<string, string> = {
"verification code sent": "api.ok.verifySent",
"verification code issued": "api.ok.verifyIssued",
"reset code sent": "api.ok.resetSent",
"password updated": "api.ok.passwordUpdated",
"logged out": "api.ok.loggedOut",
"unbound": "api.ok.unbound",
// 舊文案相容
"if the account exists, a reset code was issued": "api.ok.resetSent",
};
export function i18nKeyForApiOkMessage(raw: string): string | undefined {
const k = raw.trim().toLowerCase();
if (!k) return undefined;
// exact
if (API_OK_MESSAGE_KEY[raw.trim()]) return API_OK_MESSAGE_KEY[raw.trim()];
if (API_OK_MESSAGE_KEY[k]) return API_OK_MESSAGE_KEY[k];
// case-insensitive scan
for (const [en, key] of Object.entries(API_OK_MESSAGE_KEY)) {
if (en.toLowerCase() === k) return key;
}
return undefined;
}
/** 成功 data.message → 目前語系文案;未知則回原文或 fallbackKey */
export function messageForApiOk(
raw: string | undefined | null,
fallbackKey = "common.success",
locale?: AppLocale,
): string {
const loc = locale ?? currentLocale();
const text = (raw || "").trim();
if (!text) return translate(loc, fallbackKey);
const key = i18nKeyForApiOkMessage(text);
if (key) return translate(loc, key);
return text;
}
function currentLocale(): AppLocale {
try {
return loadUiPrefs().locale;
} catch {
return "zh-TW";
}
}
/**
* + React http client
* code fallback api.err.unknown
*/
export function messageForApiCode(
code: number,
_fallback?: string,
locale?: AppLocale,
): string {
const loc = locale ?? currentLocale();
const key = i18nKeyForApiCode(code);
if (key) return translate(loc, key);
return translate(loc, "api.err.unknown");
}
type ErrLike = {
code?: number;
message?: string;
name?: string;
};
/** 從 thrown value 取可顯示字串(依目前語言包) */
export function formatThrownError(err: unknown, fallbackKey = "common.error"): string {
const loc = currentLocale();
const e = err as ErrLike | null;
if (e && typeof e === "object" && e.code != null && !Number.isNaN(Number(e.code))) {
const code = Number(e.code);
const key = i18nKeyForApiCode(code);
if (key) return translate(loc, key);
// 未知業務碼:不顯示後端英文,改通用錯誤
return translate(loc, "api.err.unknown");
}
// mock 前端 throw 的中文 Error或無 code 的例外
if (err instanceof Error && err.message && err.name !== "ApiError") {
return err.message;
}
if (err instanceof Error && err.name === "ApiError") {
// 有 ApiError 但沒 code 時
return translate(loc, "api.err.unknown");
}
return translate(loc, fallbackKey);
}
/**
* React hook I18n t / locale
* catch UI
*/
export function useFormatApiError() {
const { t, locale } = useI18n();
return useCallback(
(err: unknown, fallbackKey = "common.error"): string => {
const e = err as ErrLike | null;
if (e && typeof e === "object" && e.code != null && !Number.isNaN(Number(e.code))) {
const key = i18nKeyForApiCode(Number(e.code));
if (key) return t(key);
return t("api.err.unknown");
}
if (err instanceof Error && err.message && err.name !== "ApiError") {
return err.message;
}
return t(fallbackKey);
},
[t, locale],
);
}
/**
* React hook message
* verification code sent
*/
export function useFormatApiOk() {
const { t, locale } = useI18n();
return useCallback(
(raw: string | undefined | null, fallbackKey = "common.success"): string => {
const text = (raw || "").trim();
if (!text) return t(fallbackKey);
const key = i18nKeyForApiOkMessage(text);
if (key) return t(key);
return text;
},
[t, locale],
);
}