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,10 +1,11 @@
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
import {
FT_CORRECT_SCORE_TEMPLATE,
HT_CORRECT_SCORE_TEMPLATE,
} from '../settlement/domain/settlement-calculator';
import { appBadRequest, appNotFound } from '../../shared/common/app-error';
@Injectable()
export class MarketsService {
@@ -12,7 +13,7 @@ export class MarketsService {
async generateTemplates(matchId: bigint, marketTypes: string[]) {
const match = await this.prisma.match.findUnique({ where: { id: matchId } });
if (!match) throw new NotFoundException('Match not found');
if (!match) throw appNotFound('MATCH_NOT_FOUND');
const created = [];
@@ -181,7 +182,7 @@ export class MarketsService {
};
const config = configs[marketType];
if (!config) throw new BadRequestException(`Unknown market type: ${marketType}`);
if (!config) throw appBadRequest('UNKNOWN_MARKET_TYPE', { marketType });
return config;
}
@@ -189,8 +190,8 @@ export class MarketsService {
const selection = await this.prisma.marketSelection.findUnique({
where: { id: selectionId },
});
if (!selection) throw new NotFoundException('Selection not found');
if (newOdds <= 1) throw new BadRequestException('Odds must be > 1.00');
if (!selection) throw appNotFound('SELECTION_NOT_FOUND');
if (newOdds <= 1) throw appBadRequest('ODDS_MIN');
const newVersion = selection.oddsVersion + BigInt(1);
@@ -228,7 +229,7 @@ export class MarketsService {
data: { promoLabel?: string | null; status?: string; lineValue?: number | null },
) {
const market = await this.prisma.market.findUnique({ where: { id: marketId } });
if (!market) throw new NotFoundException('Market not found');
if (!market) throw appNotFound('MARKET_NOT_FOUND');
return this.prisma.market.update({
where: { id: marketId },
@@ -248,10 +249,10 @@ export class MarketsService {
const selection = await this.prisma.marketSelection.findUnique({
where: { id: selectionId },
});
if (!selection) throw new NotFoundException('Selection not found');
if (!selection) throw appNotFound('SELECTION_NOT_FOUND');
if (data.odds != null) {
if (!operatorId) throw new BadRequestException('Operator required for odds update');
if (!operatorId) throw appBadRequest('OPERATOR_REQUIRED');
return this.updateOdds(selectionId, data.odds, operatorId);
}