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,4 +1,5 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { appBadRequest } from '../../shared/common/app-error';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
@@ -12,7 +13,7 @@ export class WalletService {
async getWallet(userId: bigint) {
const wallet = await this.prisma.wallet.findUnique({ where: { userId } });
if (!wallet) throw new BadRequestException('Wallet not found');
if (!wallet) throw appBadRequest('WALLET_NOT_FOUND');
return wallet;
}
@@ -26,7 +27,7 @@ export class WalletService {
const wallets = await tx.$queryRaw<Array<{ id: bigint; available_balance: Decimal; frozen_balance: Decimal; version: number }>>`
SELECT id, available_balance, frozen_balance, version FROM wallets WHERE user_id = ${userId} FOR UPDATE
`;
if (!wallets.length) throw new BadRequestException('Wallet not found');
if (!wallets.length) throw appBadRequest('WALLET_NOT_FOUND');
return wallets[0];
}
@@ -39,7 +40,7 @@ export class WalletService {
transactionType = 'MANUAL_DEPOSIT',
) {
const amt = new Decimal(amount);
if (amt.lte(0)) throw new BadRequestException('Amount must be positive');
if (amt.lte(0)) throw appBadRequest('AMOUNT_MUST_BE_POSITIVE');
return this.prisma.$transaction(async (tx) => {
const w = await this.lockWallet(tx, userId);
@@ -84,12 +85,12 @@ export class WalletService {
referenceId?: string,
) {
const amt = new Decimal(amount);
if (amt.lte(0)) throw new BadRequestException('Amount must be positive');
if (amt.lte(0)) throw appBadRequest('AMOUNT_MUST_BE_POSITIVE');
return this.prisma.$transaction(async (tx) => {
const w = await this.lockWallet(tx, userId);
const balanceBefore = new Decimal(w.available_balance);
if (balanceBefore.lt(amt)) throw new BadRequestException('Insufficient balance');
if (balanceBefore.lt(amt)) throw appBadRequest('INSUFFICIENT_BALANCE');
const balanceAfter = balanceBefore.sub(amt);
await tx.wallet.update({
@@ -128,7 +129,7 @@ export class WalletService {
return this.prisma.$transaction(async (tx) => {
const w = await this.lockWallet(tx, userId);
const avail = new Decimal(w.available_balance);
if (avail.lt(amt)) throw new BadRequestException('Insufficient balance');
if (avail.lt(amt)) throw appBadRequest('INSUFFICIENT_BALANCE');
const balanceAfter = avail.sub(amt);
const frozenAfter = new Decimal(w.frozen_balance).add(amt);