148 lines
4.3 KiB
TypeScript
Executable File
148 lines
4.3 KiB
TypeScript
Executable File
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<T> implements NestInterceptor<
|
|
T,
|
|
ApiResponse<T>
|
|
> {
|
|
constructor(private readonly i18n: I18nService) {}
|
|
|
|
intercept(
|
|
context: ExecutionContext,
|
|
next: CallHandler,
|
|
): Observable<ApiResponse<T>> {
|
|
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<T>;
|
|
}
|
|
|
|
// 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
|
|
);
|
|
}
|
|
}
|