import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from "@nestjs/common"; import { Observable } from "rxjs"; import { map } from "rxjs/operators"; import { ApiResponse, createSuccessResponse } from "../types/api-response.type"; /** * Response interceptor that wraps all successful responses * in the standard ApiResponse format */ import { I18nService, I18nContext } from "nestjs-i18n"; @Injectable() export class ResponseInterceptor implements NestInterceptor< T, ApiResponse > { constructor(private readonly i18n: I18nService) {} intercept( context: ExecutionContext, next: CallHandler, ): Observable> { return next.handle().pipe( map((data: unknown) => { const request = context.switchToHttp().getRequest(); // Determine language const i18nContext = I18nContext.current(); let lang = i18nContext?.lang; if (!lang) { const acceptLanguage = request.headers["accept-language"]; const xLang = request.headers["x-lang"]; if (xLang) { lang = Array.isArray(xLang) ? xLang[0] : xLang; } else if (acceptLanguage) { lang = acceptLanguage.split(",")[0].split(";")[0].split("-")[0]; } } lang = lang || "en"; // If data is already an ApiResponse, we should still translate its 'data' property // But first let's just do it directly on 'data' below before returning if (this.isApiResponse(data)) { if (data !== null) { try { this.translateReasons(data, lang); } catch { // Ignore if object is not extensible } } return data as ApiResponse; } // Recursively translate reasons arrays in the response body if (data !== null) { try { this.translateReasons(data, lang); } catch { // Ignore if object is not extensible } } const message = this.i18n.translate("common.success", { lang, }); // Wrap in success response return createSuccessResponse(data as T, message); }), ); } private translateReasons(data: any, lang: string) { if (!data || typeof data !== "object") { return; } if (Array.isArray(data)) { data.forEach((item) => this.translateReasons(item, lang)); return; } Object.keys(data).forEach((key) => { const val = data[key]; if ( (key === "reasons" || key === "decision_reasons" || key === "reasoning_factors") && Array.isArray(val) ) { data[key] = val.map((r: any) => { if (typeof r !== "string") return r; const translationKey = `predictions.reasons.${r}`; const translated = this.i18n.translate(translationKey, { lang, }); return translated === translationKey ? r : translated; }); } else if (key === "reason" && typeof val === "string") { const translationKey = `predictions.reasons.${val}`; const translated = this.i18n.translate(translationKey, { lang, }); data[key] = translated === translationKey ? val : translated; } else if (key === "flags" && Array.isArray(val)) { data[key] = val.map((r: any) => { if (typeof r !== "string") return r; const translationKey = `predictions.flags.${r}`; const translated = this.i18n.translate(translationKey, { lang, }); return translated === translationKey ? r : translated; }); } else if (key === "warnings" && Array.isArray(val)) { data[key] = val.map((r: any) => { if (typeof r !== "string") return r; const translationKey = `predictions.warnings.${r}`; const translated = this.i18n.translate(translationKey, { lang, }); return translated === translationKey ? r : translated; }); } else if (typeof val === "object" && val !== null) { this.translateReasons(val, lang); } }); } private isApiResponse(data: unknown): boolean { return ( data !== null && typeof data === "object" && "success" in data && "status" in data && "message" in data && "data" in data ); } }