Fixed FaLabels

This commit is contained in:
SepehrYahyaee
2026-09-12 11:58:07 +03:30
parent 15342e16d8
commit fc37dd78a4
3 changed files with 291 additions and 25 deletions

View File

@@ -81,6 +81,7 @@ import {
import {
getEventFaDescription,
getEventFaLabel,
localizeTimelineMetadata,
} from "./helper/timeline-fa-labels";
import { buildEnrichedDamagedParts } from "src/expert-claim/dto/claim-damaged-part.enricher";
@@ -2831,7 +2832,7 @@ export class ExpertInsurerService {
ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy),
performedByFaLabel:
ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy),
metadata: this.localizeTimelineMetadata(ev),
metadata: localizeTimelineMetadata(ev),
});
}
}
@@ -2869,7 +2870,7 @@ export class ExpertInsurerService {
ExpertInsurerService.resolveTimelinePerformedByCategory(performedBy),
performedByFaLabel:
ExpertInsurerService.resolveTimelinePerformedByFaLabel(performedBy),
metadata: this.localizeTimelineMetadata(ev),
metadata: localizeTimelineMetadata(ev),
});
}
}
@@ -2883,22 +2884,4 @@ export class ExpertInsurerService {
// Resolve insurer's client name for context if needed — just return raw events
return { publicId, events };
}
/**
* Keep the timeline metadata intact, but replace persisted system-generated
* descriptions with their Persian presentation value.
*/
private localizeTimelineMetadata(event: {
type: string;
metadata?: Record<string, unknown>;
}): Record<string, unknown> | null {
if (!event.metadata) return null;
return {
...event.metadata,
...(Object.prototype.hasOwnProperty.call(event.metadata, "description")
? { description: getEventFaDescription(event) }
: {}),
};
}
}

View File

@@ -1,4 +1,8 @@
import { getEventFaDescription, getEventFaLabel } from "./timeline-fa-labels";
import {
getEventFaDescription,
getEventFaLabel,
localizeTimelineMetadata,
} from "./timeline-fa-labels";
describe("insurer timeline Persian labels", () => {
it("localizes the user-submission detail shown to insurers", () => {
@@ -22,4 +26,73 @@ describe("insurer timeline Persian labels", () => {
"رویدادی در روند پرونده ثبت شد.",
);
});
it.each([
["FIRST_INITIAL_FORM_SUBMITTED", "فرم اولیه طرف اول ثبت شد"],
["FIRST_LOCATION_SUBMITTED", "موقعیت مکانی طرف اول ثبت شد"],
["FIRST_VOICE_UPLOADED", "صدای طرف اول بارگذاری شد"],
["FIRST_DESCRIPTION_SUBMITTED", "توضیحات طرف اول ثبت شد"],
["EXPERT_REPLY_SUBMITTED", "پاسخ کارشناس ثبت شد"],
])("localizes sample timeline event %s", (type, expectedLabel) => {
expect(getEventFaLabel({ type })).toBe(expectedLabel);
});
it("uses a Persian faLabel fallback for an unknown event type", () => {
expect(getEventFaLabel({ type: "UNRECOGNIZED_EVENT" })).toBe(
"رویداد پرونده",
);
});
it("localizes human-readable assignment metadata", () => {
expect(
localizeTimelineMetadata({
type: "CLAIM_ASSIGNED",
metadata: { note: "Expert assigned via review assign endpoint" },
}),
).toEqual({
note: "پرونده از طریق فرایند بررسی به کارشناس تخصیص داده شد.",
});
});
it("localizes Fanavaran errors while retaining machine-facing metadata", () => {
expect(
localizeTimelineMetadata({
type: "FANAVARAN_EXPERTISE_AUTO_SUBMIT_FAILED",
metadata: {
clientKey: "parsian",
error: "Fanavaran expertise payload is not ready.",
},
}),
).toEqual({
clientKey: "parsian",
clientFaLabel: "پارسیان",
error: "اطلاعات کارشناسی برای ارسال به فناوران آماده نیست.",
});
});
it("adds Persian labels for displayable metadata enums", () => {
expect(
localizeTimelineMetadata({
type: "STEP_COMPLETED",
metadata: {
role: "FIRST",
stepKey: "USER_SUBMISSION_COMPLETE",
carType: "sedan",
captureType: "angle",
captureKey: "front",
},
}),
).toEqual({
role: "FIRST",
roleFaLabel: "طرف اول",
stepKey: "USER_SUBMISSION_COMPLETE",
stepFaLabel: "ارسال اطلاعات توسط کاربر",
carType: "sedan",
carTypeFaLabel: "سواری",
captureType: "angle",
captureTypeFaLabel: "زاویه خودرو",
captureKey: "front",
captureKeyFaLabel: "نمای جلو",
});
});
});

View File

@@ -9,7 +9,7 @@
* Usage:
* const label = EVENT_TYPE_FA_LABELS[event.type]
* ?? resolveStepCompletedFaLabel(event)
* ?? event.type;
* ?? "رویداد پرونده";
*/
// ---------------------------------------------------------------------------
@@ -40,6 +40,16 @@ export const EVENT_TYPE_FA_LABELS: Record<string, string> = {
// First video
FIRST_VIDEO_UPLOADED: "ویدیوی اول بارگذاری شد",
// Legacy first/second-party self-service flow
FIRST_INITIAL_FORM_SUBMITTED: "فرم اولیه طرف اول ثبت شد",
SECOND_INITIAL_FORM_SUBMITTED: "فرم اولیه طرف دوم ثبت شد",
FIRST_LOCATION_SUBMITTED: "موقعیت مکانی طرف اول ثبت شد",
SECOND_LOCATION_SUBMITTED: "موقعیت مکانی طرف دوم ثبت شد",
FIRST_VOICE_UPLOADED: "صدای طرف اول بارگذاری شد",
SECOND_VOICE_UPLOADED: "صدای طرف دوم بارگذاری شد",
FIRST_DESCRIPTION_SUBMITTED: "توضیحات طرف اول ثبت شد",
SECOND_DESCRIPTION_SUBMITTED: "توضیحات طرف دوم ثبت شد",
// Second party invitation
SECOND_PARTY_INVITED: "طرف دوم دعوت شد",
@@ -101,6 +111,12 @@ export const EVENT_TYPE_FA_LABELS: Record<string, string> = {
EXPERT_RESEND_FULFILLED: "مدارک درخواستی ارسال شد",
IN_PERSON_VISIT_REQUESTED: "کارشناس بازدید حضوری درخواست کرد",
EXPERT_DAMAGED_PARTS_UPDATED: "کارشناس قطعات آسیب‌دیده را به‌روزرسانی کرد",
EXPERT_REPLY_SUBMITTED: "پاسخ کارشناس ثبت شد",
EXPERT_FINAL_REPLY_SUBMITTED: "پاسخ نهایی کارشناس ثبت شد",
FACTORS_REJECTED_REPRICED_AUTO_COMPLETED:
"بررسی فاکتورها با قیمت‌گذاری مجدد تکمیل شد",
FACTORS_VALIDATED_ALL_APPROVED_AUTO_COMPLETED:
"همه فاکتورها تأیید و پرونده تکمیل شد",
// Workflow steps (generic)
STEP_COMPLETED: "مرحله تکمیل شد",
@@ -171,6 +187,82 @@ export const STEP_KEY_FA_LABELS: Record<string, string> = {
CLAIM_COMPLETED: "پرونده خسارت تکمیل شد",
};
const TIMELINE_STEP_FA_LABELS: Record<string, string> = {
...STEP_KEY_FA_LABELS,
FIRST_VIDEO: "ویدیوی طرف اول",
SECOND_VIDEO: "ویدیوی طرف دوم",
};
const ROLE_FA_LABELS: Record<string, string> = {
FIRST: "طرف اول",
SECOND: "طرف دوم",
FIRST_PARTY: "طرف اول",
SECOND_PARTY: "طرف دوم",
};
const VEHICLE_TYPE_FA_LABELS: Record<string, string> = {
sedan: "سواری",
suv: "شاسی‌بلند",
hatchback: "هاچ‌بک",
pickup: "وانت",
van: "ون",
};
const CAPTURE_TYPE_FA_LABELS: Record<string, string> = {
angle: "زاویه خودرو",
part: "قطعه آسیب‌دیده",
};
const CAPTURE_KEY_FA_LABELS: Record<string, string> = {
front: "نمای جلو",
back: "نمای عقب",
left: "نمای چپ",
right: "نمای راست",
top: "نمای بالا",
};
const DOCUMENT_KEY_FA_LABELS: Record<string, string> = {
car_green_card: "کارت سبز خودرو",
car_certificate: "سند خودرو",
national_card: "کارت ملی",
damaged_driving_license_front: "روی گواهینامه طرف آسیب‌دیده",
damaged_driving_license_back: "پشت گواهینامه طرف آسیب‌دیده",
damaged_chassis_number: "شماره شاسی خودروی آسیب‌دیده",
damaged_engine_photo: "تصویر موتور خودروی آسیب‌دیده",
damaged_car_card_front: "روی کارت خودروی آسیب‌دیده",
damaged_car_card_back: "پشت کارت خودروی آسیب‌دیده",
damaged_metal_plate: "پلاک خودروی آسیب‌دیده",
guilty_driving_license_front: "روی گواهینامه طرف مقصر",
guilty_driving_license_back: "پشت گواهینامه طرف مقصر",
guilty_car_card_front: "روی کارت خودروی مقصر",
guilty_car_card_back: "پشت کارت خودروی مقصر",
guilty_metal_plate: "پلاک خودروی مقصر",
guilty_damage_area: "ناحیه آسیب‌دیده خودروی مقصر",
};
const CLIENT_FA_LABELS: Record<string, string> = {
parsian: "پارسیان",
};
const REPLY_FIELD_FA_LABELS: Record<string, string> = {
damageExpertReply: "پاسخ کارشناس خسارت",
damageExpertReplyFinal: "پاسخ نهایی کارشناس خسارت",
};
const TIMELINE_SYSTEM_TEXT_FA: Record<string, string> = {
"Expert assigned via review assign endpoint":
"پرونده از طریق فرایند بررسی به کارشناس تخصیص داده شد.",
"Expert assigned for factor validation review":
"پرونده برای بررسی و تأیید فاکتورها به کارشناس تخصیص داده شد.",
"Field expert portfolio lock (no status change)":
"پرونده برای بررسی در اختیار کارشناس میدانی قرار گرفت.",
"Fanavaran expertise payload is not ready.":
"اطلاعات کارشناسی برای ارسال به فناوران آماده نیست.",
};
const GENERIC_EVENT_FA_LABEL = "رویداد پرونده";
const GENERIC_EVENT_FA_DESCRIPTION = "رویدادی در روند پرونده ثبت شد.";
/**
* Persian descriptions for workflow-step events. These are deliberately
* complete sentences because the insurer timeline exposes them as the event
@@ -222,7 +314,7 @@ export function getEventFaLabel(event: {
return (
resolveStepCompletedFaLabel(event) ??
EVENT_TYPE_FA_LABELS[event.type] ??
event.type
GENERIC_EVENT_FA_LABEL
);
}
@@ -242,6 +334,124 @@ export function getEventFaDescription(event: {
return STEP_KEY_FA_DESCRIPTIONS[stepKey];
}
const label = getEventFaLabel(event);
return label === event.type ? "رویدادی در روند پرونده ثبت شد." : `${label}.`;
const label =
resolveStepCompletedFaLabel(event) ?? EVENT_TYPE_FA_LABELS[event.type];
return label ? `${label}.` : GENERIC_EVENT_FA_DESCRIPTION;
}
function containsLatinText(value: string): boolean {
return /[A-Za-z]/.test(value);
}
function localizeHumanReadableMetadataText(
value: unknown,
event: { type: string; metadata?: Record<string, unknown> },
field: "note" | "error" | "message" | "warning",
): unknown {
if (typeof value !== "string" || !containsLatinText(value)) return value;
const knownTranslation = TIMELINE_SYSTEM_TEXT_FA[value.trim()];
if (knownTranslation) return knownTranslation;
if (field === "error" || field === "warning") {
return event.type.endsWith("_FAILED")
? getEventFaDescription(event)
: "در اجرای این عملیات خطایی رخ داد.";
}
return field === "note"
? "یادداشت سیستمی این رویداد ثبت شد."
: getEventFaDescription(event);
}
/**
* Builds presentation-safe timeline metadata. Raw identifiers and enum values
* remain available for integrations; Persian companion labels are added for
* UI rendering, while persisted human-readable English text is replaced.
*/
export function localizeTimelineMetadata(event: {
type: string;
metadata?: Record<string, unknown> | null;
}): Record<string, unknown> | null {
if (!event.metadata) return null;
const metadata: Record<string, unknown> = { ...event.metadata };
if (Object.prototype.hasOwnProperty.call(metadata, "description")) {
metadata.description = getEventFaDescription(event);
}
for (const field of ["note", "error", "message", "warning"] as const) {
if (Object.prototype.hasOwnProperty.call(metadata, field)) {
metadata[field] = localizeHumanReadableMetadataText(
metadata[field],
event,
field,
);
}
}
if (Array.isArray(metadata.warnings)) {
metadata.warnings = metadata.warnings.map((warning) =>
localizeHumanReadableMetadataText(warning, event, "warning"),
);
}
const role = typeof metadata.role === "string" ? metadata.role : undefined;
if (role && ROLE_FA_LABELS[role]) {
metadata.roleFaLabel = ROLE_FA_LABELS[role];
}
const stepKey =
typeof metadata.stepKey === "string" ? metadata.stepKey : undefined;
if (stepKey && TIMELINE_STEP_FA_LABELS[stepKey]) {
metadata.stepFaLabel = TIMELINE_STEP_FA_LABELS[stepKey];
}
const carType =
typeof metadata.carType === "string" ? metadata.carType : undefined;
if (carType && VEHICLE_TYPE_FA_LABELS[carType]) {
metadata.carTypeFaLabel = VEHICLE_TYPE_FA_LABELS[carType];
}
const captureType =
typeof metadata.captureType === "string" ? metadata.captureType : undefined;
if (captureType && CAPTURE_TYPE_FA_LABELS[captureType]) {
metadata.captureTypeFaLabel = CAPTURE_TYPE_FA_LABELS[captureType];
}
const captureKey =
typeof metadata.captureKey === "string" ? metadata.captureKey : undefined;
if (captureKey && CAPTURE_KEY_FA_LABELS[captureKey]) {
metadata.captureKeyFaLabel = CAPTURE_KEY_FA_LABELS[captureKey];
}
const resolvedCarAngleKey =
typeof metadata.resolvedCarAngleKey === "string"
? metadata.resolvedCarAngleKey
: undefined;
if (resolvedCarAngleKey && CAPTURE_KEY_FA_LABELS[resolvedCarAngleKey]) {
metadata.resolvedCarAngleFaLabel =
CAPTURE_KEY_FA_LABELS[resolvedCarAngleKey];
}
const documentKey =
typeof metadata.documentKey === "string" ? metadata.documentKey : undefined;
if (documentKey && DOCUMENT_KEY_FA_LABELS[documentKey]) {
metadata.documentFaLabel = DOCUMENT_KEY_FA_LABELS[documentKey];
}
const clientKey =
typeof metadata.clientKey === "string" ? metadata.clientKey : undefined;
if (clientKey && CLIENT_FA_LABELS[clientKey]) {
metadata.clientFaLabel = CLIENT_FA_LABELS[clientKey];
}
const replyField =
typeof metadata.replyField === "string" ? metadata.replyField : undefined;
if (replyField && REPLY_FIELD_FA_LABELS[replyField]) {
metadata.replyFieldFaLabel = REPLY_FIELD_FA_LABELS[replyField];
}
return metadata;
}