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:
2026-06-10 13:36:38 +08:00
parent 03f54ca689
commit 641c92a5f5
23 changed files with 1059 additions and 234 deletions

View File

@@ -1,6 +1,7 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Decimal } from '@prisma/client/runtime/library';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { appBadRequest } from '../../shared/common/app-error';
export type BettingLimits = {
minStake: number;
@@ -89,18 +90,18 @@ export class BettingLimitsService {
const stake = params.stake;
if (stake < limits.minStake) {
throw new BadRequestException(`Minimum stake is ${limits.minStake}`);
throw appBadRequest('MIN_STAKE', { minStake: limits.minStake });
}
const maxStake = params.betType === 'PARLAY' ? limits.maxStakeParlay : limits.maxStakeSingle;
if (stake > maxStake) {
throw new BadRequestException(`Maximum stake is ${maxStake}`);
throw appBadRequest('MAX_STAKE', { maxStake });
}
const maxPayout =
params.betType === 'PARLAY' ? limits.maxPayoutParlay : limits.maxPayoutSingle;
if (params.potentialReturn.gt(maxPayout)) {
throw new BadRequestException(`Potential return exceeds limit of ${maxPayout}`);
throw appBadRequest('MAX_PAYOUT', { maxPayout });
}
if (limits.dailyStakeLimit > 0) {
@@ -119,7 +120,7 @@ export class BettingLimitsService {
});
const todayStake = new Decimal(agg._sum.stake ?? 0);
if (todayStake.add(stake).gt(limits.dailyStakeLimit)) {
throw new BadRequestException(`Daily stake limit of ${limits.dailyStakeLimit} exceeded`);
throw appBadRequest('DAILY_STAKE_LIMIT', { limit: limits.dailyStakeLimit });
}
}
}