/** * 後端 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 = { [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 = { "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], ); }