feat: internationalize API error responses by locale
Add shared error codes with zh/en/ms messages, coded app exceptions, and locale-aware global filter. Frontends send X-Locale so error text matches the active UI language. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
717
packages/shared/src/api-errors.ts
Normal file
717
packages/shared/src/api-errors.ts
Normal file
@@ -0,0 +1,717 @@
|
||||
const DEFAULT_LOCALE = 'zh-CN' as const;
|
||||
const SUPPORTED_LOCALES = ['zh-CN', 'ms-MY', 'en-US'] as const;
|
||||
type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||
|
||||
/** 后端错误码 → 三语文案({param} 占位) */
|
||||
export const API_ERROR_MESSAGES = {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
'zh-CN': '服务器内部错误',
|
||||
'en-US': 'Internal server error',
|
||||
'ms-MY': 'Ralat pelayan dalaman',
|
||||
},
|
||||
INVALID_CREDENTIALS: {
|
||||
'zh-CN': '用户名或密码错误',
|
||||
'en-US': 'Invalid username or password',
|
||||
'ms-MY': 'Nama pengguna atau kata laluan tidak sah',
|
||||
},
|
||||
ACCOUNT_DISABLED: {
|
||||
'zh-CN': '账号已停用',
|
||||
'en-US': 'Account disabled',
|
||||
'ms-MY': 'Akaun telah dinyahaktifkan',
|
||||
},
|
||||
AGENT_ACCOUNT_SUSPENDED: {
|
||||
'zh-CN': '代理账号已停用',
|
||||
'en-US': 'Agent account suspended',
|
||||
'ms-MY': 'Akaun ejen digantung',
|
||||
},
|
||||
PARENT_AGENT_SUSPENDED: {
|
||||
'zh-CN': '上级代理已停用,暂无法登录',
|
||||
'en-US': 'Parent agent is suspended; login unavailable',
|
||||
'ms-MY': 'Ejen induk digantung; log masuk tidak tersedia',
|
||||
},
|
||||
ACCOUNT_LOCKED: {
|
||||
'zh-CN': '账号已锁定,请稍后再试',
|
||||
'en-US': 'Account locked, try again later',
|
||||
'ms-MY': 'Akaun dikunci, cuba lagi nanti',
|
||||
},
|
||||
USER_NOT_FOUND: {
|
||||
'zh-CN': '用户不存在',
|
||||
'en-US': 'User not found',
|
||||
'ms-MY': 'Pengguna tidak dijumpai',
|
||||
},
|
||||
PASSWORD_CHANGE_DISABLED: {
|
||||
'zh-CN': '当前平台未开放玩家自行修改密码',
|
||||
'en-US': 'Password change is disabled for players',
|
||||
'ms-MY': 'Pertukaran kata laluan pemain tidak dibenarkan',
|
||||
},
|
||||
INVALID_OLD_PASSWORD: {
|
||||
'zh-CN': '当前密码不正确',
|
||||
'en-US': 'Current password is incorrect',
|
||||
'ms-MY': 'Kata laluan semasa tidak betul',
|
||||
},
|
||||
ADMIN_ACCESS_REQUIRED: {
|
||||
'zh-CN': '需要管理员权限',
|
||||
'en-US': 'Admin access required',
|
||||
'ms-MY': 'Akses pentadbir diperlukan',
|
||||
},
|
||||
INSUFFICIENT_PERMISSIONS: {
|
||||
'zh-CN': '权限不足',
|
||||
'en-US': 'Insufficient permissions',
|
||||
'ms-MY': 'Kebenaran tidak mencukupi',
|
||||
},
|
||||
ACCESS_DENIED_PORTAL: {
|
||||
'zh-CN': '无权访问该门户',
|
||||
'en-US': 'Access denied for this portal',
|
||||
'ms-MY': 'Akses portal ditolak',
|
||||
},
|
||||
PLAYER_ACCESS_ONLY: {
|
||||
'zh-CN': '仅玩家可访问',
|
||||
'en-US': 'Player access only',
|
||||
'ms-MY': 'Akses pemain sahaja',
|
||||
},
|
||||
ADMIN_ACCESS_ONLY: {
|
||||
'zh-CN': '仅管理员可访问',
|
||||
'en-US': 'Admin access only',
|
||||
'ms-MY': 'Akses pentadbir sahaja',
|
||||
},
|
||||
AGENT_ACCESS_ONLY: {
|
||||
'zh-CN': '仅代理可访问',
|
||||
'en-US': 'Agent access only',
|
||||
'ms-MY': 'Akses ejen sahaja',
|
||||
},
|
||||
LEAGUE_NAME_REQUIRED: {
|
||||
'zh-CN': '请填写联赛名称(中文、英文或马来文至少一项)',
|
||||
'en-US': 'League name required (Chinese, English or Malay)',
|
||||
'ms-MY': 'Nama liga diperlukan (Cina, Inggeris atau Melayu)',
|
||||
},
|
||||
LEAGUE_NOT_FOUND: {
|
||||
'zh-CN': '联赛不存在',
|
||||
'en-US': 'League not found',
|
||||
'ms-MY': 'Liga tidak dijumpai',
|
||||
},
|
||||
LEAGUE_UNPUBLISH_FORBIDDEN: {
|
||||
'zh-CN': '已发布的联赛不可下架',
|
||||
'en-US': 'Published leagues cannot be unpublished',
|
||||
'ms-MY': 'Liga yang diterbitkan tidak boleh ditarik',
|
||||
},
|
||||
TEAM_CODE_REQUIRED: {
|
||||
'zh-CN': '请填写球队代码',
|
||||
'en-US': 'Team code is required',
|
||||
'ms-MY': 'Kod pasukan diperlukan',
|
||||
},
|
||||
TEAMS_NAME_REQUIRED: {
|
||||
'zh-CN': '请填写主客队名称(中文、英文或马来文至少一项)',
|
||||
'en-US': 'Home and away team names required',
|
||||
'ms-MY': 'Nama pasukan home/away diperlukan',
|
||||
},
|
||||
TEAMS_SAME: {
|
||||
'zh-CN': '主客队不能相同,请填写不同的队名',
|
||||
'en-US': 'Home and away teams must be different',
|
||||
'ms-MY': 'Pasukan home dan away mesti berbeza',
|
||||
},
|
||||
MATCH_NOT_FOUND: {
|
||||
'zh-CN': '赛事不存在',
|
||||
'en-US': 'Match not found',
|
||||
'ms-MY': 'Perlawanan tidak dijumpai',
|
||||
},
|
||||
OUTRIGHT_EDIT_VIA_MARKETS: {
|
||||
'zh-CN': '冠军盘请通过盘口管理维护',
|
||||
'en-US': 'Edit outright markets in the markets page',
|
||||
'ms-MY': 'Urus outright melalui halaman pasaran',
|
||||
},
|
||||
MATCH_NOT_EDITABLE: {
|
||||
'zh-CN': '当前状态不可编辑',
|
||||
'en-US': 'Match cannot be edited in current status',
|
||||
'ms-MY': 'Perlawanan tidak boleh diedit dalam status semasa',
|
||||
},
|
||||
OUTRIGHT_DELETE_FORBIDDEN: {
|
||||
'zh-CN': '冠军盘不可删除',
|
||||
'en-US': 'Outright events cannot be deleted',
|
||||
'ms-MY': 'Acara outright tidak boleh dipadam',
|
||||
},
|
||||
MATCH_DELETE_DRAFT_ONLY: {
|
||||
'zh-CN': '仅草稿状态可删除',
|
||||
'en-US': 'Only draft matches can be deleted',
|
||||
'ms-MY': 'Hanya draf boleh dipadam',
|
||||
},
|
||||
MATCH_HAS_BETS: {
|
||||
'zh-CN': '该赛事已有注单关联,无法删除',
|
||||
'en-US': 'Match has bets and cannot be deleted',
|
||||
'ms-MY': 'Perlawanan mempunyai pertaruhan dan tidak boleh dipadam',
|
||||
},
|
||||
MATCHES_ARRAY_REQUIRED: {
|
||||
'zh-CN': '请提供 matches 数组',
|
||||
'en-US': 'matches array is required',
|
||||
'ms-MY': 'Tatasusunan matches diperlukan',
|
||||
},
|
||||
SELECTION_NOT_FOUND: {
|
||||
'zh-CN': '投注选项不存在',
|
||||
'en-US': 'Selection not found',
|
||||
'ms-MY': 'Pilihan tidak dijumpai',
|
||||
},
|
||||
SELECTION_CLOSED: {
|
||||
'zh-CN': '投注选项已关闭',
|
||||
'en-US': 'Selection closed',
|
||||
'ms-MY': 'Pilihan ditutup',
|
||||
},
|
||||
MARKET_CLOSED: {
|
||||
'zh-CN': '盘口已关闭',
|
||||
'en-US': 'Market closed',
|
||||
'ms-MY': 'Pasaran ditutup',
|
||||
},
|
||||
MATCH_NOT_BETTING: {
|
||||
'zh-CN': '该赛事暂不可投注',
|
||||
'en-US': 'Match not available for betting',
|
||||
'ms-MY': 'Perlawanan tidak tersedia untuk pertaruhan',
|
||||
},
|
||||
FOOTBALL_ONLY: {
|
||||
'zh-CN': '仅支持足球投注',
|
||||
'en-US': 'Only football betting is supported',
|
||||
'ms-MY': 'Hanya pertaruhan bola sepak disokong',
|
||||
},
|
||||
PRE_MATCH_ONLY: {
|
||||
'zh-CN': '仅支持赛前投注,比赛已开始',
|
||||
'en-US': 'Pre-match betting only; match has started',
|
||||
'ms-MY': 'Pertaruhan pra-perlawanan sahaja; perlawanan telah bermula',
|
||||
},
|
||||
ODDS_CHANGED: {
|
||||
'zh-CN': '赔率已变更,请重新确认',
|
||||
'en-US': 'Odds changed, please confirm again',
|
||||
'ms-MY': 'Odds berubah, sila sahkan semula',
|
||||
},
|
||||
INVALID_STAKE: {
|
||||
'zh-CN': '投注金额无效',
|
||||
'en-US': 'Invalid stake',
|
||||
'ms-MY': 'Stake tidak sah',
|
||||
},
|
||||
PARLAY_LEG_COUNT_INVALID: {
|
||||
'zh-CN': '串关场次须在 {min}–{max} 场之间',
|
||||
'en-US': 'Parlay must have {min}–{max} legs',
|
||||
'ms-MY': 'Parlay mesti {min}–{max} pilihan',
|
||||
},
|
||||
PARLAY_OUTRIGHT_FORBIDDEN: {
|
||||
'zh-CN': '冠军盘不可加入串关',
|
||||
'en-US': 'Outright cannot be in parlay',
|
||||
'ms-MY': 'Outright tidak boleh dalam parlay',
|
||||
},
|
||||
PARLAY_QUARTER_LINE_FORBIDDEN: {
|
||||
'zh-CN': '四分之一盘口不可加入串关',
|
||||
'en-US': 'Quarter line markets cannot be in parlay',
|
||||
'ms-MY': 'Pasaran suku baris tidak boleh dalam parlay',
|
||||
},
|
||||
PARLAY_MARKET_NOT_ALLOWED: {
|
||||
'zh-CN': '该盘口不可加入串关',
|
||||
'en-US': 'Market not allowed in parlay',
|
||||
'ms-MY': 'Pasaran tidak dibenarkan dalam parlay',
|
||||
},
|
||||
BET_NOT_FOUND: {
|
||||
'zh-CN': '注单不存在',
|
||||
'en-US': 'Bet not found',
|
||||
'ms-MY': 'Pertaruhan tidak dijumpai',
|
||||
},
|
||||
MIN_STAKE: {
|
||||
'zh-CN': '最低投注额为 {minStake}',
|
||||
'en-US': 'Minimum stake is {minStake}',
|
||||
'ms-MY': 'Stake minimum ialah {minStake}',
|
||||
},
|
||||
MAX_STAKE: {
|
||||
'zh-CN': '最高投注额为 {maxStake}',
|
||||
'en-US': 'Maximum stake is {maxStake}',
|
||||
'ms-MY': 'Stake maksimum ialah {maxStake}',
|
||||
},
|
||||
MAX_PAYOUT: {
|
||||
'zh-CN': '潜在派彩超过限额 {maxPayout}',
|
||||
'en-US': 'Potential return exceeds limit of {maxPayout}',
|
||||
'ms-MY': 'Bayaran melebihi had {maxPayout}',
|
||||
},
|
||||
DAILY_STAKE_LIMIT: {
|
||||
'zh-CN': '已超过日投注限额 {limit}',
|
||||
'en-US': 'Daily stake limit of {limit} exceeded',
|
||||
'ms-MY': 'Had stake harian {limit} telah melebihi',
|
||||
},
|
||||
WALLET_NOT_FOUND: {
|
||||
'zh-CN': '钱包不存在',
|
||||
'en-US': 'Wallet not found',
|
||||
'ms-MY': 'Dompet tidak dijumpai',
|
||||
},
|
||||
AMOUNT_MUST_BE_POSITIVE: {
|
||||
'zh-CN': '金额须大于 0',
|
||||
'en-US': 'Amount must be positive',
|
||||
'ms-MY': 'Jumlah mesti positif',
|
||||
},
|
||||
INSUFFICIENT_BALANCE: {
|
||||
'zh-CN': '余额不足',
|
||||
'en-US': 'Insufficient balance',
|
||||
'ms-MY': 'Baki tidak mencukupi',
|
||||
},
|
||||
AGENT_PROFILE_NOT_FOUND: {
|
||||
'zh-CN': '代理资料不存在',
|
||||
'en-US': 'Agent profile not found',
|
||||
'ms-MY': 'Profil ejen tidak dijumpai',
|
||||
},
|
||||
AGENT_NOT_FOUND: {
|
||||
'zh-CN': '代理不存在',
|
||||
'en-US': 'Agent not found',
|
||||
'ms-MY': 'Ejen tidak dijumpai',
|
||||
},
|
||||
CREDIT_LIMIT_NEGATIVE: {
|
||||
'zh-CN': '授信额度不能为负',
|
||||
'en-US': 'Credit limit cannot be negative',
|
||||
'ms-MY': 'Had kredit tidak boleh negatif',
|
||||
},
|
||||
PLAYER_NOT_FOUND: {
|
||||
'zh-CN': '玩家不存在',
|
||||
'en-US': 'Player not found',
|
||||
'ms-MY': 'Pemain tidak dijumpai',
|
||||
},
|
||||
MANAGE_DIRECT_PLAYERS_ONLY: {
|
||||
'zh-CN': '仅可管理直属玩家',
|
||||
'en-US': 'Can only manage direct players',
|
||||
'ms-MY': 'Hanya pemain terus boleh diurus',
|
||||
},
|
||||
PARENT_AGENT_NOT_FOUND: {
|
||||
'zh-CN': '上级代理不存在',
|
||||
'en-US': 'Parent agent not found',
|
||||
'ms-MY': 'Ejen induk tidak dijumpai',
|
||||
},
|
||||
CREDIT_EXCEEDS_PARENT: {
|
||||
'zh-CN': '下级代理授信不能超过上级授信额度',
|
||||
'en-US': 'Sub-agent credit cannot exceed parent limit',
|
||||
'ms-MY': 'Kredit sub-ejen tidak boleh melebihi induk',
|
||||
},
|
||||
CASHBACK_RATE_NEGATIVE: {
|
||||
'zh-CN': '回水比例不能为负',
|
||||
'en-US': 'Cashback rate cannot be negative',
|
||||
'ms-MY': 'Kadar rebat tidak boleh negatif',
|
||||
},
|
||||
CASHBACK_RATE_EXCEEDS_PARENT: {
|
||||
'zh-CN': '下级代理回水比例不能超过上级',
|
||||
'en-US': 'Sub-agent cashback rate cannot exceed parent',
|
||||
'ms-MY': 'Kadar rebat sub-ejen tidak boleh melebihi induk',
|
||||
},
|
||||
BET_LIMIT_EXCEEDS_PARENT: {
|
||||
'zh-CN': '下级代理单笔限额不能超过上级',
|
||||
'en-US': 'Sub-agent bet limit cannot exceed parent',
|
||||
'ms-MY': 'Had pertaruhan sub-ejen tidak boleh melebihi induk',
|
||||
},
|
||||
BET_LIMIT_NEGATIVE: {
|
||||
'zh-CN': '单笔限额不能为负',
|
||||
'en-US': 'Bet limit cannot be negative',
|
||||
'ms-MY': 'Had pertaruhan tidak boleh negatif',
|
||||
},
|
||||
DAILY_LIMIT_EXCEEDS_PARENT: {
|
||||
'zh-CN': '下级代理日限额不能超过上级',
|
||||
'en-US': 'Sub-agent daily limit cannot exceed parent',
|
||||
'ms-MY': 'Had harian sub-ejen tidak boleh melebihi induk',
|
||||
},
|
||||
DAILY_LIMIT_NEGATIVE: {
|
||||
'zh-CN': '日限额不能为负',
|
||||
'en-US': 'Daily limit cannot be negative',
|
||||
'ms-MY': 'Had harian tidak boleh negatif',
|
||||
},
|
||||
CREDIT_TOPUP_EXCEEDED: {
|
||||
'zh-CN': '超过玩家上级代理可用授信,无法上分',
|
||||
'en-US': 'Exceeds parent agent available credit',
|
||||
'ms-MY': 'Melebihi kredit ejen induk yang tersedia',
|
||||
},
|
||||
AGENT_SINGLE_TOPUP_LIMIT: {
|
||||
'zh-CN': '超过代理单笔上分限额',
|
||||
'en-US': 'Exceeds agent single top-up limit',
|
||||
'ms-MY': 'Melebihi had top-up tunggal ejen',
|
||||
},
|
||||
AGENT_DAILY_TOPUP_LIMIT: {
|
||||
'zh-CN': '超过代理日上分限额',
|
||||
'en-US': 'Exceeds agent daily top-up limit',
|
||||
'ms-MY': 'Melebihi had top-up harian ejen',
|
||||
},
|
||||
INSUFFICIENT_AGENT_CREDIT: {
|
||||
'zh-CN': '代理可用授信不足',
|
||||
'en-US': 'Insufficient agent credit',
|
||||
'ms-MY': 'Kredit ejen tidak mencukupi',
|
||||
},
|
||||
INVALID_STATUS: {
|
||||
'zh-CN': '无效状态',
|
||||
'en-US': 'Invalid status',
|
||||
'ms-MY': 'Status tidak sah',
|
||||
},
|
||||
USERNAME_REQUIRED: {
|
||||
'zh-CN': '账号名称不能为空',
|
||||
'en-US': 'Username is required',
|
||||
'ms-MY': 'Nama pengguna diperlukan',
|
||||
},
|
||||
USERNAME_TAKEN: {
|
||||
'zh-CN': '账号名称已被占用',
|
||||
'en-US': 'Username already taken',
|
||||
'ms-MY': 'Nama pengguna sudah digunakan',
|
||||
},
|
||||
USERNAME_FORMAT_INVALID: {
|
||||
'zh-CN': '玩家用户名仅可使用英文字母和数字(3–32 位),不可含中文或特殊符号',
|
||||
'en-US': 'Username must be 3–32 letters or digits only',
|
||||
'ms-MY': 'Nama pengguna mesti 3–32 huruf atau digit sahaja',
|
||||
},
|
||||
PASSWORD_MIN_LENGTH: {
|
||||
'zh-CN': '密码至少 8 位',
|
||||
'en-US': 'Password must be at least 8 characters',
|
||||
'ms-MY': 'Kata laluan sekurang-kurangnya 8 aksara',
|
||||
},
|
||||
AUTH_INFO_MISSING: {
|
||||
'zh-CN': '账号认证信息缺失',
|
||||
'en-US': 'Account auth info missing',
|
||||
'ms-MY': 'Maklumat auth akaun tiada',
|
||||
},
|
||||
USERNAME_CHANGE_DISABLED: {
|
||||
'zh-CN': '当前平台未开放玩家自行修改账号名称',
|
||||
'en-US': 'Username change is disabled for players',
|
||||
'ms-MY': 'Pertukaran nama pengguna pemain tidak dibenarkan',
|
||||
},
|
||||
INVALID_AVATAR: {
|
||||
'zh-CN': '无效头像',
|
||||
'en-US': 'Invalid avatar',
|
||||
'ms-MY': 'Avatar tidak sah',
|
||||
},
|
||||
UNSUPPORTED_LOCALE: {
|
||||
'zh-CN': '不支持的语言',
|
||||
'en-US': 'Unsupported locale',
|
||||
'ms-MY': 'Locale tidak disokong',
|
||||
},
|
||||
NOT_SUB_AGENT: {
|
||||
'zh-CN': '非您的下级代理',
|
||||
'en-US': 'Not your sub-agent',
|
||||
'ms-MY': 'Bukan sub-ejen anda',
|
||||
},
|
||||
PROMOTE_PLAYER_ONLY: {
|
||||
'zh-CN': '仅玩家账号可设为代理',
|
||||
'en-US': 'Only player accounts can be promoted to agent',
|
||||
'ms-MY': 'Hanya pemain boleh dinaikkan ke ejen',
|
||||
},
|
||||
ALREADY_AGENT: {
|
||||
'zh-CN': '该用户已是代理',
|
||||
'en-US': 'User is already an agent',
|
||||
'ms-MY': 'Pengguna sudah menjadi ejen',
|
||||
},
|
||||
AGENT_LEVEL_INVALID: {
|
||||
'zh-CN': '代理级别须为 1 或 2',
|
||||
'en-US': 'Agent level must be 1 or 2',
|
||||
'ms-MY': 'Tahap ejen mesti 1 atau 2',
|
||||
},
|
||||
LEVEL2_REQUIRES_PARENT: {
|
||||
'zh-CN': '二级代理必须指定上级代理',
|
||||
'en-US': 'Level 2 agent requires parent',
|
||||
'ms-MY': 'Ejen peringkat 2 memerlukan induk',
|
||||
},
|
||||
TIER1_NO_PARENT_PLAYER: {
|
||||
'zh-CN': '一级代理不可设置上级玩家',
|
||||
'en-US': 'Tier-1 agents cannot have a parent player',
|
||||
'ms-MY': 'Ejen peringkat 1 tidak boleh ada pemain induk',
|
||||
},
|
||||
PROMOTE_USE_CREDIT_NOT_BALANCE: {
|
||||
'zh-CN': '设为代理时请使用授信额度,勿填玩家初始余额',
|
||||
'en-US': 'Use credit limit when promoting to agent, not initial balance',
|
||||
'ms-MY': 'Guna had kredit apabila naik taraf ke ejen, bukan baki awal',
|
||||
},
|
||||
TIER2_REQUIRES_PARENT_AGENT: {
|
||||
'zh-CN': '二级代理必须指定上级代理',
|
||||
'en-US': 'Tier-2 agent must specify parent agent',
|
||||
'ms-MY': 'Ejen peringkat 2 mesti nyatakan ejen induk',
|
||||
},
|
||||
PARENT_MUST_BE_AGENT: {
|
||||
'zh-CN': '上级必须为代理账号',
|
||||
'en-US': 'Parent must be an agent account',
|
||||
'ms-MY': 'Induk mesti akaun ejen',
|
||||
},
|
||||
CREATE_DIRECT_PLAYERS_ONLY: {
|
||||
'zh-CN': '仅可创建直属玩家',
|
||||
'en-US': 'Can only create direct players',
|
||||
'ms-MY': 'Hanya pemain terus boleh dicipta',
|
||||
},
|
||||
DB_RESET_FORBIDDEN: {
|
||||
'zh-CN': '生产环境禁止重置数据库(需设置 ALLOW_DB_RESET=true)',
|
||||
'en-US': 'Database reset forbidden in production (set ALLOW_DB_RESET=true)',
|
||||
'ms-MY': 'Reset DB dilarang di produksi (set ALLOW_DB_RESET=true)',
|
||||
},
|
||||
CASHBACK_DATE_RANGE_INVALID: {
|
||||
'zh-CN': '开始日期不能晚于结束日期',
|
||||
'en-US': 'Start date cannot be after end date',
|
||||
'ms-MY': 'Tarikh mula tidak boleh selepas tarikh tamat',
|
||||
},
|
||||
CASHBACK_ALREADY_ISSUED: {
|
||||
'zh-CN': '该统计周期已发放返水,不可重复生成预览',
|
||||
'en-US': 'Cashback already issued for this period',
|
||||
'ms-MY': 'Rebat telah dikeluarkan untuk tempoh ini',
|
||||
},
|
||||
CASHBACK_NO_ELIGIBLE_BETS: {
|
||||
'zh-CN': '该周期内无符合条件的返水,无法生成预览',
|
||||
'en-US': 'No eligible bets in period for cashback preview',
|
||||
'ms-MY': 'Tiada pertaruhan layak untuk pratonton rebat',
|
||||
},
|
||||
CASHBACK_BETS_IN_OTHER_BATCH: {
|
||||
'zh-CN': '该周期内的有效注单均已计入其他返水批次,无法生成预览',
|
||||
'en-US': 'Eligible bets already in another cashback batch',
|
||||
'ms-MY': 'Pertaruhan layak sudah dalam batch rebat lain',
|
||||
},
|
||||
CASHBACK_BATCH_NOT_FOUND: {
|
||||
'zh-CN': '返水批次不存在',
|
||||
'en-US': 'Cashback batch not found',
|
||||
'ms-MY': 'Batch rebat tidak dijumpai',
|
||||
},
|
||||
CASHBACK_BATCH_NOT_ISSUABLE: {
|
||||
'zh-CN': '该批次不可发放',
|
||||
'en-US': 'Batch cannot be issued',
|
||||
'ms-MY': 'Batch tidak boleh dikeluarkan',
|
||||
},
|
||||
CASHBACK_NO_AMOUNT: {
|
||||
'zh-CN': '批次无有效返水金额',
|
||||
'en-US': 'Batch has no valid cashback amount',
|
||||
'ms-MY': 'Batch tiada jumlah rebat sah',
|
||||
},
|
||||
CASHBACK_PERIOD_ALREADY_ISSUED: {
|
||||
'zh-CN': '该统计周期已发放返水',
|
||||
'en-US': 'Cashback already issued for this period',
|
||||
'ms-MY': 'Rebat tempoh ini telah dikeluarkan',
|
||||
},
|
||||
CASHBACK_BETS_ALREADY_PAID: {
|
||||
'zh-CN': '部分注单已在其他批次发放返水,请作废本预览后重新生成',
|
||||
'en-US': 'Some bets paid in another batch; void preview and regenerate',
|
||||
'ms-MY': 'Sebahagian pertaruhan dibayar dalam batch lain; batalkan pratonton',
|
||||
},
|
||||
CASHBACK_PREVIEW_ONLY_VOID: {
|
||||
'zh-CN': '只能作废待发放批次',
|
||||
'en-US': 'Only preview batches can be voided',
|
||||
'ms-MY': 'Hanya batch pratonton boleh dibatalkan',
|
||||
},
|
||||
UNKNOWN_MARKET_TYPE: {
|
||||
'zh-CN': '未知盘口类型:{marketType}',
|
||||
'en-US': 'Unknown market type: {marketType}',
|
||||
'ms-MY': 'Jenis pasaran tidak diketahui: {marketType}',
|
||||
},
|
||||
ODDS_MIN: {
|
||||
'zh-CN': '赔率须大于 1.00',
|
||||
'en-US': 'Odds must be greater than 1.00',
|
||||
'ms-MY': 'Odds mesti lebih daripada 1.00',
|
||||
},
|
||||
MARKET_NOT_FOUND: {
|
||||
'zh-CN': '盘口不存在',
|
||||
'en-US': 'Market not found',
|
||||
'ms-MY': 'Pasaran tidak dijumpai',
|
||||
},
|
||||
OPERATOR_REQUIRED: {
|
||||
'zh-CN': '修改赔率须指定操作员',
|
||||
'en-US': 'Operator required for odds update',
|
||||
'ms-MY': 'Operator diperlukan untuk kemas kini odds',
|
||||
},
|
||||
OUTRIGHT_SELECTION_EXISTS: {
|
||||
'zh-CN': '该球队代码已存在选项',
|
||||
'en-US': 'Selection already exists for this team code',
|
||||
'ms-MY': 'Pilihan untuk kod pasukan ini sudah wujud',
|
||||
},
|
||||
OUTRIGHT_TEAMS_REQUIRED: {
|
||||
'zh-CN': '至少添加一支球队',
|
||||
'en-US': 'At least one team required',
|
||||
'ms-MY': 'Sekurang-kurangnya satu pasukan diperlukan',
|
||||
},
|
||||
OUTRIGHT_SELECTION_INVALID: {
|
||||
'zh-CN': '无效的夺冠选项',
|
||||
'en-US': 'Invalid selection for this outright event',
|
||||
'ms-MY': 'Pilihan tidak sah untuk outright ini',
|
||||
},
|
||||
OUTRIGHT_EVENT_NOT_FOUND: {
|
||||
'zh-CN': '冠军盘赛事不存在',
|
||||
'en-US': 'Outright event not found',
|
||||
'ms-MY': 'Acara outright tidak dijumpai',
|
||||
},
|
||||
SETTLEMENT_WINNER_REQUIRED: {
|
||||
'zh-CN': '冠军盘结算需指定获胜球队',
|
||||
'en-US': 'Outright settlement requires winner team',
|
||||
'ms-MY': 'Penyelesaian outright memerlukan pasukan pemenang',
|
||||
},
|
||||
SETTLEMENT_WINNER_NOT_FOUND: {
|
||||
'zh-CN': '获胜球队不存在',
|
||||
'en-US': 'Winner team not found',
|
||||
'ms-MY': 'Pasukan pemenang tidak dijumpai',
|
||||
},
|
||||
SETTLEMENT_WINNER_NOT_IN_MARKET: {
|
||||
'zh-CN': '该球队不在本冠军盘选项中',
|
||||
'en-US': 'Team is not in this outright market',
|
||||
'ms-MY': 'Pasukan tiada dalam pasaran outright ini',
|
||||
},
|
||||
SCORE_NOT_RECORDED: {
|
||||
'zh-CN': '尚未录入比分',
|
||||
'en-US': 'Score not recorded',
|
||||
'ms-MY': 'Skor belum direkod',
|
||||
},
|
||||
SETTLEMENT_BATCH_NOT_FOUND: {
|
||||
'zh-CN': '结算批次不存在',
|
||||
'en-US': 'Settlement batch not found',
|
||||
'ms-MY': 'Batch penyelesaian tidak dijumpai',
|
||||
},
|
||||
SETTLEMENT_BATCH_NOT_PREVIEW: {
|
||||
'zh-CN': '结算批次不在预览状态',
|
||||
'en-US': 'Batch is not in preview status',
|
||||
'ms-MY': 'Batch bukan dalam status pratonton',
|
||||
},
|
||||
SETTLEMENT_BATCH_ALREADY_CONFIRMED: {
|
||||
'zh-CN': '结算批次已确认',
|
||||
'en-US': 'Batch already confirmed',
|
||||
'ms-MY': 'Batch sudah disahkan',
|
||||
},
|
||||
SCORE_NOT_FOUND: {
|
||||
'zh-CN': '比分不存在',
|
||||
'en-US': 'Score not found',
|
||||
'ms-MY': 'Skor tidak dijumpai',
|
||||
},
|
||||
PARLAY_UNSETTLED_LEGS: {
|
||||
'zh-CN': '串关注单 {betId} 仍有未结算场次',
|
||||
'en-US': 'Parlay bet {betId} has unsettled legs',
|
||||
'ms-MY': 'Parlay {betId} masih ada pilihan belum selesai',
|
||||
},
|
||||
RESETTLE_SETTLED_ONLY: {
|
||||
'zh-CN': '仅已结算赛事可重结算',
|
||||
'en-US': 'Only settled matches can be resettled',
|
||||
'ms-MY': 'Hanya perlawanan selesai boleh diselesaikan semula',
|
||||
},
|
||||
RESETTLE_BATCH_ONLY: {
|
||||
'zh-CN': '非重结算批次',
|
||||
'en-US': 'Not a resettle batch',
|
||||
'ms-MY': 'Bukan batch penyelesaian semula',
|
||||
},
|
||||
UPLOAD_CATEGORY_UNSUPPORTED: {
|
||||
'zh-CN': '不支持的上传分类',
|
||||
'en-US': 'Unsupported upload category',
|
||||
'ms-MY': 'Kategori muat naik tidak disokong',
|
||||
},
|
||||
UPLOAD_IMAGE_REQUIRED: {
|
||||
'zh-CN': '请选择图片文件',
|
||||
'en-US': 'Image file is required',
|
||||
'ms-MY': 'Fail imej diperlukan',
|
||||
},
|
||||
UPLOAD_IMAGE_TYPE_INVALID: {
|
||||
'zh-CN': '仅支持 PNG、JPG、WEBP、GIF 或 SVG 图片',
|
||||
'en-US': 'Only PNG, JPG, WEBP, GIF or SVG images are allowed',
|
||||
'ms-MY': 'Hanya PNG, JPG, WEBP, GIF atau SVG dibenarkan',
|
||||
},
|
||||
UPLOAD_SVG_UNSAFE: {
|
||||
'zh-CN': 'SVG 内容不安全,已拒绝',
|
||||
'en-US': 'Unsafe SVG content is not allowed',
|
||||
'ms-MY': 'Kandungan SVG tidak selamat',
|
||||
},
|
||||
DB_RESET_PHRASE_INVALID: {
|
||||
'zh-CN': '确认短语不正确,请输入 RESET',
|
||||
'en-US': 'Invalid confirmation phrase; type RESET',
|
||||
'ms-MY': 'Frasa pengesahan salah; taip RESET',
|
||||
},
|
||||
IMPORT_MATCHES_REQUIRED: {
|
||||
'zh-CN': '导入数据无效:需要 matches[]',
|
||||
'en-US': 'Invalid import payload: matches[] required',
|
||||
'ms-MY': 'Payload import tidak sah: matches[] diperlukan',
|
||||
},
|
||||
WC_OUTRIGHT_NOT_FOUND: {
|
||||
'zh-CN': '未找到 WC2026 冠军盘,请先导入',
|
||||
'en-US': 'WC2026 outright not found — run import',
|
||||
'ms-MY': 'Outright WC2026 tidak dijumpai — jalankan import',
|
||||
},
|
||||
FILE_NOT_FOUND: {
|
||||
'zh-CN': '文件不存在',
|
||||
'en-US': 'File not found',
|
||||
'ms-MY': 'Fail tidak dijumpai',
|
||||
},
|
||||
URL_REQUIRED: {
|
||||
'zh-CN': '请提供 url 参数',
|
||||
'en-US': 'url is required',
|
||||
'ms-MY': 'url diperlukan',
|
||||
},
|
||||
CONTENT_TYPE_INVALID: {
|
||||
'zh-CN': '无效内容类型:{type}',
|
||||
'en-US': 'Invalid contentType: {type}',
|
||||
'ms-MY': 'Jenis kandungan tidak sah: {type}',
|
||||
},
|
||||
CONTENT_STATUS_INVALID: {
|
||||
'zh-CN': '无效状态:{status}',
|
||||
'en-US': 'Invalid status: {status}',
|
||||
'ms-MY': 'Status tidak sah: {status}',
|
||||
},
|
||||
CONTENT_END_BEFORE_START: {
|
||||
'zh-CN': '结束时间须晚于开始时间',
|
||||
'en-US': 'endTime must be after startTime',
|
||||
'ms-MY': 'endTime mesti selepas startTime',
|
||||
},
|
||||
CONTENT_TRANSLATION_REQUIRED: {
|
||||
'zh-CN': '至少填写一条翻译',
|
||||
'en-US': 'At least one translation required',
|
||||
'ms-MY': 'Sekurang-kurangnya satu terjemahan diperlukan',
|
||||
},
|
||||
CONTENT_LOCALE_REQUIRED: {
|
||||
'zh-CN': '翻译语言不能为空',
|
||||
'en-US': 'Translation locale required',
|
||||
'ms-MY': 'Locale terjemahan diperlukan',
|
||||
},
|
||||
CONTENT_LOCALE_DUPLICATE: {
|
||||
'zh-CN': '重复的语言:{locale}',
|
||||
'en-US': 'Duplicate locale: {locale}',
|
||||
'ms-MY': 'Locale pendua: {locale}',
|
||||
},
|
||||
CONTENT_LINK_TYPE_INVALID: {
|
||||
'zh-CN': '无效链接类型:{linkType}',
|
||||
'en-US': 'Invalid linkType: {linkType}',
|
||||
'ms-MY': 'Jenis pautan tidak sah: {linkType}',
|
||||
},
|
||||
CONTENT_LINK_TARGET_REQUIRED: {
|
||||
'zh-CN': '设置链接类型时须填写 linkTarget',
|
||||
'en-US': 'linkTarget required when linkType is set',
|
||||
'ms-MY': 'linkTarget diperlukan apabila linkType ditetapkan',
|
||||
},
|
||||
CONTENT_NOT_FOUND: {
|
||||
'zh-CN': '内容不存在',
|
||||
'en-US': 'Content not found',
|
||||
'ms-MY': 'Kandungan tidak dijumpai',
|
||||
},
|
||||
CONTENT_ACTIVE_BANNER_INCOMPLETE: {
|
||||
'zh-CN': '启用 Banner 须至少一种语言配置图片地址',
|
||||
'en-US': 'ACTIVE banner requires imageUrl in at least one locale',
|
||||
'ms-MY': 'Banner aktif memerlukan imageUrl sekurang-kurangnya satu locale',
|
||||
},
|
||||
CONTENT_ACTIVE_NOTICE_INCOMPLETE: {
|
||||
'zh-CN': '启用公告须至少一种语言填写标题或正文',
|
||||
'en-US': 'ACTIVE notice requires title or body in at least one locale',
|
||||
'ms-MY': 'Notis aktif memerlukan tajuk atau kandungan',
|
||||
},
|
||||
CONTENT_ACTIVE_TICKER_INCOMPLETE: {
|
||||
'zh-CN': '启用滚动公告须至少一种语言填写正文',
|
||||
'en-US': 'ACTIVE ticker requires body in at least one locale',
|
||||
'ms-MY': 'Ticker aktif memerlukan kandungan',
|
||||
},
|
||||
} as const satisfies Record<string, Record<Locale, string>>;
|
||||
|
||||
export type ApiErrorCode = keyof typeof API_ERROR_MESSAGES;
|
||||
|
||||
export type ApiErrorParams = Record<string, string | number>;
|
||||
|
||||
export function normalizeLocale(input?: string | null): Locale {
|
||||
const raw = String(input ?? '').trim();
|
||||
if (!raw) return DEFAULT_LOCALE;
|
||||
const lower = raw.toLowerCase();
|
||||
if (lower.startsWith('zh')) return 'zh-CN';
|
||||
if (lower.startsWith('ms') || lower.startsWith('my')) return 'ms-MY';
|
||||
if (lower.startsWith('en')) return 'en-US';
|
||||
if ((SUPPORTED_LOCALES as readonly string[]).includes(raw)) return raw as Locale;
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
export function formatApiErrorMessage(
|
||||
code: ApiErrorCode,
|
||||
localeInput?: string | null,
|
||||
params?: ApiErrorParams,
|
||||
): string {
|
||||
const locale = normalizeLocale(localeInput);
|
||||
const template =
|
||||
API_ERROR_MESSAGES[code]?.[locale] ??
|
||||
API_ERROR_MESSAGES[code]?.[DEFAULT_LOCALE] ??
|
||||
API_ERROR_MESSAGES.INTERNAL_SERVER_ERROR[locale];
|
||||
if (!params) return template;
|
||||
return template.replace(/\{(\w+)\}/g, (_match: string, key: string) =>
|
||||
String(params[key] ?? `{${key}}`),
|
||||
);
|
||||
}
|
||||
|
||||
export function isApiErrorCode(value: unknown): value is ApiErrorCode {
|
||||
return typeof value === 'string' && value in API_ERROR_MESSAGES;
|
||||
}
|
||||
@@ -126,4 +126,8 @@ export interface ApiResponse<T = unknown> {
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
code?: string;
|
||||
params?: Record<string, string | number> | null;
|
||||
}
|
||||
|
||||
export * from './api-errors';
|
||||
|
||||
Reference in New Issue
Block a user