diff --git a/public/assets/images/sections/01_personal_identity_details.png b/public/assets/images/sections/01_personal_identity_details.png new file mode 100644 index 0000000..806c33f Binary files /dev/null and b/public/assets/images/sections/01_personal_identity_details.png differ diff --git a/public/assets/images/sections/02_contact_residence_family_communication.png b/public/assets/images/sections/02_contact_residence_family_communication.png new file mode 100644 index 0000000..f876f50 Binary files /dev/null and b/public/assets/images/sections/02_contact_residence_family_communication.png differ diff --git a/public/assets/images/sections/03_physical_appearance_health.png b/public/assets/images/sections/03_physical_appearance_health.png new file mode 100644 index 0000000..d985aa8 Binary files /dev/null and b/public/assets/images/sections/03_physical_appearance_health.png differ diff --git a/public/assets/images/sections/04_education_career_economic_status.png b/public/assets/images/sections/04_education_career_economic_status.png new file mode 100644 index 0000000..9f78620 Binary files /dev/null and b/public/assets/images/sections/04_education_career_economic_status.png differ diff --git a/public/assets/images/sections/05_family_background.png b/public/assets/images/sections/05_family_background.png new file mode 100644 index 0000000..3a06d04 Binary files /dev/null and b/public/assets/images/sections/05_family_background.png differ diff --git a/public/assets/images/sections/06_marital_status_marriage_history_children.png b/public/assets/images/sections/06_marital_status_marriage_history_children.png new file mode 100644 index 0000000..38310d5 Binary files /dev/null and b/public/assets/images/sections/06_marital_status_marriage_history_children.png differ diff --git a/public/assets/images/sections/07_beliefs_lifestyle_personal_boundaries.png b/public/assets/images/sections/07_beliefs_lifestyle_personal_boundaries.png new file mode 100644 index 0000000..850d1d6 Binary files /dev/null and b/public/assets/images/sections/07_beliefs_lifestyle_personal_boundaries.png differ diff --git a/public/assets/images/sections/08_future_spouse_criteria_red_lines.png b/public/assets/images/sections/08_future_spouse_criteria_red_lines.png new file mode 100644 index 0000000..deb52de Binary files /dev/null and b/public/assets/images/sections/08_future_spouse_criteria_red_lines.png differ diff --git a/public/assets/images/sections/09_identity_verification_documents.png b/public/assets/images/sections/09_identity_verification_documents.png new file mode 100644 index 0000000..b3b7829 Binary files /dev/null and b/public/assets/images/sections/09_identity_verification_documents.png differ diff --git a/public/assets/images/sections/10_personality_test.png b/public/assets/images/sections/10_personality_test.png new file mode 100644 index 0000000..c9aaa4f Binary files /dev/null and b/public/assets/images/sections/10_personality_test.png differ diff --git a/public/assets/images/sections/11_glasser_5_needs_test.png b/public/assets/images/sections/11_glasser_5_needs_test.png new file mode 100644 index 0000000..003742d Binary files /dev/null and b/public/assets/images/sections/11_glasser_5_needs_test.png differ diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index 573258b..63ec0dd 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -106,7 +106,10 @@ function getCookieValue(cookieHeader: string, name: string) { function getRequestHeaders(request: NextRequest, targetUrl: URL) { const headers = new Headers(); - const authKey = process.env.NEXT_PUBLIC_AUTH_KEY; + const authKey = + process.env.NEXT_PUBLIC_AUTH_KEY || + process.env.NEXT_PUBLIC_DEFAULT_TOKEN || + process.env.DEFAULT_TOKEN; const cookieHeader = request.headers.get("cookie") ?? ""; for (const header of REQUEST_HEADERS_TO_FORWARD) { diff --git a/src/app/marriage-advisors/page.tsx b/src/app/marriage-advisors/page.tsx index 32823f8..fbe5d1d 100644 --- a/src/app/marriage-advisors/page.tsx +++ b/src/app/marriage-advisors/page.tsx @@ -21,7 +21,9 @@ type MarriageAdvisorsPageProps = { onClose?: () => void; }; -export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPageProps = {}) { +export default function MarriageAdvisorsPage({ + onClose, +}: MarriageAdvisorsPageProps = {}) { const { dictionary: t } = useI18n(); const [isSupportOpen, setIsSupportOpen] = useState(false); @@ -105,11 +107,6 @@ export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPagePr key={advisor.username} advisor={advisor} onOpen={() => openConsultantPage(advisor.username)} - onContact={() => setIsSupportOpen(true)} - contactLabels={{ - text: t["Text"] || "Text", - voice: t["Voice Call"] || "Voice Call", - }} /> ))} @@ -126,17 +123,20 @@ export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPagePr function AdvisorCard({ advisor, onOpen, - onContact, - contactLabels, }: { advisor: MarriageConsultant; onOpen: () => void; - onContact: () => void; - contactLabels: { text: string; voice: string }; }) { - const topics = advisor.topics.filter(Boolean); - const rating = Number(advisor.avg_rate ?? 0).toFixed(1); + const { dictionary: t, locale } = useI18n(); + const isRtl = locale === "fa" || locale === "ar" || locale === "ur"; + const unreadCount = advisor.unread_count ?? 0; const isOnline = advisor.status === "online"; + const rating = Number(advisor.avg_rate ?? 0); + const hasChat = advisor.contact_type?.includes("chat"); + const hasVoice = advisor.contact_type?.includes("voice"); + const hasVideo = advisor.contact_type?.includes("video"); + const hasActiveContact = hasChat || hasVoice || hasVideo; + const topics = advisor.topics?.filter(Boolean) ?? []; return (
- {/* Top Frame containing Left (Avatar + Info) and Right (Rating) and Actions */} -
- {/* Header Row */} -
- {/* Left part: Avatar + Info */} -
- {/* Avatar Container */} -
+ {/* Outer Card Container */} +
0 + ? "border border-[#ECA533]" + : "border border-[#EBEBEB]" + }`} + > +
+ {/* Header Row: Avatar + Info + Rating */} +
+ {/* Avatar with Status Dot */} +
{advisor.avatar_url ? ( - // Consultant avatars come from arbitrary media hosts, which - // are not all listed in next.config remotePatterns — a plain - // img with an onError fallback keeps them rendering. // eslint-disable-next-line @next/next/no-img-element {advisor.fullname { event.currentTarget.src = FALLBACK_AVATAR; }} /> ) : ( - {advisor.fullname - )} - {/* Status indicator dot */} - {isOnline && ( -
- - +
+ + +
)} + + {/* Status indicator dot badge (top-start) */} +
+
+
+
+
- {/* Name + Title Info */} -
-

- {advisor.fullname ?? advisor.username} -

-

+ {/* Consultant Info */} +

+
+ {advisor.is_ai && ( + + AI + + )} +

+ {advisor.fullname ?? advisor.username} +

+ {rating > 0 && ( +
+ + + + + {rating.toFixed(1)} + +
+ )} +
+

{advisor.slogan ?? ""}

- {/* Right part: Rating */} -
- - - {rating} - -
-
+ {/* Contact Chips */} + {hasActiveContact && ( +
+ {hasChat && ( +
+ + + + {t["Text Message"] || "Text Message"} +
+ )} - {/* Actions Row */} -
- {/* Text Contact Button */} - + {hasVoice && ( +
+ + + + {t["Voice Call"] || "Voice Call"} +
+ )} - {/* Voice Call Contact Button */} - + {hasVideo && ( +
+ + + + {t["Video Call"] || "Video Call"} +
+ )} +
+ )} + + {/* Topics Badges */} + {topics.length > 0 && ( + <> +
+
+ {topics.map((topic) => ( + + {topic} + + ))} +
+ + )}
- {/* Divider */} -
- - {/* Tags Section */} - {topics.length > 0 && ( -
-
- {topics.map((tag) => ( - - {tag} - - ))} -
+ {/* Floating Unread Badge at Top Corner */} + {unreadCount > 0 && ( +
+ {unreadCount} + + {t["New Message"] || "New Message"} +
)}
@@ -283,35 +349,27 @@ function AdvisorsSkeleton() { {[0, 1, 2].map((index) => (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
))} diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index 2aff24c..c5e2058 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -148,66 +148,13 @@ function calculateAgeFromDob(dobString: string): number | null { return age > 0 && age < 120 ? age : null; } -function formatOptionValue( - value: MarriageFieldValue, - dictionary?: Record, -): string | null { - const base = formatFieldValue(value); - if (!base) return null; - if (!dictionary) return base; - - if (dictionary[base]) return dictionary[base]; - - // Try replacing underscores with spaces: "single;_never_married" -> "single; never married" - const withSpaces = base.replace(/_/g, " ").trim(); - if (dictionary[withSpaces]) return dictionary[withSpaces]; - - // Try capitalized first letter: "Single; never married" - const capitalized = withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); - if (dictionary[capitalized]) return dictionary[capitalized]; - - return withSpaces; -} - -function formatFieldValue(value: MarriageFieldValue) { - if (value === null || value === "") { - return null; - } - - if (isMarriagePhoneFieldValue(value)) { - return `+${value.countryCode}${value.phoneNumber}`; - } - - if (typeof value === "boolean") { - return value ? "Yes" : "No"; - } - - return String(value); -} - -function isMarriagePhoneFieldValue( - value: unknown, -): value is MarriagePhoneFieldValue { - if (!value || typeof value !== "object") { - return false; - } - - const phoneValue = value as Partial; - - return ( - typeof phoneValue.countryCode === "string" && - typeof phoneValue.phoneNumber === "string" - ); -} - -function titleFromKey(key: string) { - return key - .replace(/^q\d+[_-]?/i, "") - .replace(/[_-]+/g, " ") - .replace(/\s+/g, " ") - .trim() - .replace(/\b\w/g, (letter) => letter.toUpperCase()); -} +import { + formatFieldLabel, + formatFieldValue, + formatOptionValue, + isMarriagePhoneFieldValue, + titleFromKey, +} from "@/lib/marriage-field-formatter"; function toDisplayField( field: MarriageField, @@ -221,7 +168,7 @@ function toDisplayField( return { id: field.key || field.label || value, - label: field.label || titleFromKey(field.key), + label: formatFieldLabel(field, dictionary), value, }; } diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index 5e1bc08..58b962f 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -26,45 +26,13 @@ import { markMatchStarted } from "@/lib/match-start-grace"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; -function formatFieldValue(value: MarriageFieldValue) { - if (value === null || value === "") { - return null; - } - - if (isMarriagePhoneFieldValue(value)) { - return `+${value.countryCode}${value.phoneNumber}`; - } - - if (typeof value === "boolean") { - return value ? "Yes" : "No"; - } - - return String(value); -} - -function isMarriagePhoneFieldValue( - value: unknown, -): value is MarriagePhoneFieldValue { - if (!value || typeof value !== "object") { - return false; - } - - const phoneValue = value as Partial; - - return ( - typeof phoneValue.countryCode === "string" && - typeof phoneValue.phoneNumber === "string" - ); -} - -function titleFromKey(key: string) { - return key - .replace(/^q\d+[_-]?/i, "") - .replace(/[_-]+/g, " ") - .replace(/\s+/g, " ") - .trim() - .replace(/\b\w/g, (letter) => letter.toUpperCase()); -} +import { + formatFieldLabel, + formatFieldValue, + formatOptionValue, + isMarriagePhoneFieldValue, + titleFromKey, +} from "@/lib/marriage-field-formatter"; function isImageField(field: MarriageField) { return /(avatar|image|photo|picture|portrait|upload)/i.test( @@ -90,17 +58,19 @@ function canAcceptProfile( function MatchField({ field, isCandidateFemale, + dictionary, }: { field: MarriageField; isCandidateFemale: boolean; + dictionary?: Record; }) { - const value = formatFieldValue(field.value); + const value = formatOptionValue(field.value, dictionary); if (!value || isImageField(field)) { return null; } - const label = field.label || titleFromKey(field.key); + const label = formatFieldLabel(field, dictionary); if (isCandidateFemale) { return ( @@ -132,9 +102,11 @@ function MatchField({ function MatchPublicProfileFields({ publicInfo, isCandidateFemale, + dictionary, }: { publicInfo: MarriageField[] | null | undefined; isCandidateFemale: boolean; + dictionary?: Record; }) { const visibleFields = useMemo(() => { if (!publicInfo) return []; @@ -153,7 +125,8 @@ function MatchPublicProfileFields({ return (

- اطلاعات عمومی قابل نمایشی ثبت نشده است. + {dictionary?.["No public information is available to display."] || + "No public information is available to display."}

); @@ -163,7 +136,12 @@ function MatchPublicProfileFields({ return (
{visibleFields.map((field) => ( - + ))}
); @@ -172,11 +150,17 @@ function MatchPublicProfileFields({ return (

- اطلاعات عمومی و مشخصات فردی + {dictionary?.["General Information & Personal Details"] || + "General Information & Personal Details"}

{visibleFields.map((field) => ( - + ))}
@@ -341,18 +325,44 @@ export default function NewMatchProfilePage({ const candidateName = useMemo(() => { const publicInfo = profile?.match_summary?.public_info ?? []; + const firstNameIdx = publicInfo.findIndex( + (f) => + f.key === "personal_identity.first_name" || + f.key?.endsWith(".first_name"), + ); + const lastNameIdx = publicInfo.findIndex( + (f) => + f.key === "personal_identity.last_name" || + f.key?.endsWith(".last_name"), + ); + + if (firstNameIdx !== -1 && publicInfo[firstNameIdx].value) { + const firstName = formatFieldValue(publicInfo[firstNameIdx].value); + if (lastNameIdx !== -1 && publicInfo[lastNameIdx].value) { + const lastName = formatFieldValue(publicInfo[lastNameIdx].value); + return `${firstName} ${lastName}`.trim(); + } + return firstName || ""; + } + const nameField = publicInfo.find( (f) => f.key === "q1_full_name" || - f.key.toLowerCase().includes("name") || - f.key.toLowerCase().includes("nam") || - f.label.includes("نام"), + f.key.endsWith(".full_name") || + f.key.toLowerCase().includes("fullname") || + f.key.toLowerCase().includes("display_name"), ); const formatted = formatFieldValue(nameField?.value ?? null); if (formatted) return formatted; + const firstVal = publicInfo.find((f) => Boolean(f.value))?.value ?? null; - return formatFieldValue(firstVal) || "نامشخص"; - }, [profile?.match_summary?.public_info]); + return ( + formatFieldValue(firstVal) || + (profile?.match_summary?.id + ? `Profile #${profile.match_summary.id}` + : "Profile") + ); + }, [profile?.match_summary]); const isCandidateFemale = profile?.match_summary?.gender === "female" || profile?.gender === "male"; @@ -668,6 +678,7 @@ export default function NewMatchProfilePage({ diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index ca31df0..739633a 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -325,6 +325,7 @@ export function QuestionAnswersProvider({ const storageKeyRef = useRef(storageKey); const slugRef = useRef(slug); const backendFieldsRef = useRef([]); + const versionRef = useRef(undefined); const dirtyKeysRef = useRef(new Set()); const { data: profile } = useMarriageProfileQuery(); @@ -339,12 +340,14 @@ export function QuestionAnswersProvider({ slugRef.current = slug; questionsRef.current = questions; backendFieldsRef.current = serverSectionData?.data || []; + versionRef.current = serverSectionData?.version; }, [ answers, hasPendingSync, slug, questions, serverSectionData?.data, + serverSectionData?.version, ]); useEffect(() => { @@ -560,6 +563,7 @@ export function QuestionAnswersProvider({ const payload = { ...fullPayload, fields: pendingFields, + version: versionRef.current, }; const revision = answersRevisionRef.current; @@ -603,6 +607,10 @@ export function QuestionAnswersProvider({ } } + payload.fields.forEach((field) => { + dirtyKeysRef.current.delete(field.key); + }); + // Do not mark a newer edit as synced just because an older request // completed. A forced exit waits for and saves that newer revision too. if (revision !== answersRevisionRef.current) { @@ -612,9 +620,6 @@ export function QuestionAnswersProvider({ return; } - payload.fields.forEach((field) => { - dirtyKeysRef.current.delete(field.key); - }); if (dirtyKeysRef.current.size > 0) { await flushAnswersRef.current(); @@ -658,7 +663,7 @@ export function QuestionAnswersProvider({ const pendingFields = fullPayload.fields.filter((field) => dirtyKeysRef.current.has(field.key), ); - const payload = { ...fullPayload, fields: pendingFields }; + const payload = { ...fullPayload, fields: pendingFields, version: versionRef.current }; const revision = answersRevisionRef.current; if (payload.fields.length === 0) { @@ -683,6 +688,7 @@ export function QuestionAnswersProvider({ fetch(getKeepalivePatchUrl(slugRef.current), { body: JSON.stringify({ answers: answersPayload, + version: payload.version, }), credentials: "include", headers, diff --git a/src/components/Componentes/question-card.tsx b/src/components/Componentes/question-card.tsx index 5955276..3ca0deb 100644 --- a/src/components/Componentes/question-card.tsx +++ b/src/components/Componentes/question-card.tsx @@ -1,3 +1,4 @@ +import Image from "next/image"; import Link from "next/link"; import { useEffect, useRef } from "react"; import { IoInformation } from "react-icons/io5"; @@ -41,6 +42,21 @@ const iconNameMap: Record = { family_marital: "marriage", }; +const sectionIconImageMap: Partial> = { + profile: "/assets/images/sections/01_personal_identity_details.png", + contact: "/assets/images/sections/02_contact_residence_family_communication.png", + health: "/assets/images/sections/03_physical_appearance_health.png", + education: "/assets/images/sections/04_education_career_economic_status.png", + family: "/assets/images/sections/05_family_background.png", + marriage: "/assets/images/sections/06_marital_status_marriage_history_children.png", + family_marital: "/assets/images/sections/06_marital_status_marriage_history_children.png", + lifestyle: "/assets/images/sections/07_beliefs_lifestyle_personal_boundaries.png", + criteria: "/assets/images/sections/08_future_spouse_criteria_red_lines.png", + verification: "/assets/images/sections/09_identity_verification_documents.png", + personality: "/assets/images/sections/10_personality_test.png", + glasser: "/assets/images/sections/10_personality_test.png", +}; + export function QuestionCard({ item, progress = item.progress, @@ -56,6 +72,7 @@ export function QuestionCard({ : 0; const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE; const resolvedIconKey = item.icon || resolveSectionIcon(item.slug); + const iconImageSrc = sectionIconImageMap[resolvedIconKey]; const iconName = iconNameMap[resolvedIconKey] ?? "details"; const cardRef = useRef(null); @@ -105,11 +122,22 @@ export function QuestionCard({ >
-
diff --git a/src/components/Componentes/question-phone.test.tsx b/src/components/Componentes/question-phone.test.tsx index a272ee3..c9c18e3 100644 --- a/src/components/Componentes/question-phone.test.tsx +++ b/src/components/Componentes/question-phone.test.tsx @@ -18,7 +18,11 @@ const mockSetAnswerValue = vi.fn((q: QuestionField, val: unknown) => { vi.mock("@/translations/provider", () => ({ useI18n: () => ({ locale: "en", - dictionary: { "Select country": "Select country" }, + dictionary: { + "Select country": "Select country", + Close: "Close", + Confirm: "Confirm", + }, }), })); @@ -58,6 +62,8 @@ const phoneQuestion2: QuestionField = { options: [], }; + + describe("QuestionPhone IP country detection and shimmer", () => { beforeEach(() => { answerMap = {}; @@ -89,22 +95,17 @@ describe("QuestionPhone IP country detection and shimmer", () => { const { container } = render(); - // While IP is pending, shimmer should be present inside the country button const shimmerElements = container.querySelectorAll(".shimmer-bg"); expect(shimmerElements.length).toBeGreaterThan(0); - // The input itself should NOT have shimmer const input = screen.getByRole("textbox"); expect(input.classList.contains("shimmer-bg")).toBe(false); - // Resolve IP fetch with Iran code await act(async () => { resolveIpFetch({ country_calling_code: "+98" }); }); await waitFor(() => { - // Shimmer elements should be gone expect(container.querySelectorAll(".shimmer-bg").length).toBe(0); - // Country code +98 should now be visible expect(screen.getByText("+98")).toBeDefined(); expect(screen.getByText("🇮🇷")).toBeDefined(); }); @@ -149,7 +150,6 @@ describe("QuestionPhone IP country detection and shimmer", () => { , ); - // Only 1 fetch call should be triggered for both components expect(fetchSpy).toHaveBeenCalledTimes(1); await act(async () => { @@ -174,7 +174,6 @@ describe("QuestionPhone IP country detection and shimmer", () => { const { container } = render(); - // No shimmer because saved value is present expect(container.querySelectorAll(".shimmer-bg").length).toBe(0); expect(screen.getByText("+1")).toBeDefined(); expect(screen.getByDisplayValue("2025550143")).toBeDefined(); @@ -199,16 +198,13 @@ describe("QuestionPhone IP country detection and shimmer", () => { render(); - // User starts typing before IP request resolves const input = screen.getByRole("textbox"); fireEvent.change(input, { target: { value: "123456" } }); - // Resolve IP fetch with Iran code await act(async () => { resolveIpFetch({ country_calling_code: "+98" }); }); - // Should retain the manual input expect(screen.getByDisplayValue("123456")).toBeDefined(); }); }); diff --git a/src/lib/auth-bridge.ts b/src/lib/auth-bridge.ts index 4bfc8a7..5853a15 100644 --- a/src/lib/auth-bridge.ts +++ b/src/lib/auth-bridge.ts @@ -57,13 +57,20 @@ class AuthBridge { return false; } - if (!token || token.trim() === "") { + const effectiveToken = + token && token.trim() !== "" + ? token + : process.env.NEXT_PUBLIC_DEFAULT_TOKEN || + process.env.NEXT_PUBLIC_AUTH_KEY || + null; + + if (!effectiveToken || effectiveToken === "NO_TOKEN") { this.token = null; setCachedMarriageEntryPath(null); return false; } - this.token = token; + this.token = effectiveToken; this.coins = coinsValue ?? Number(coinsCookie ?? 0); return true; } @@ -258,6 +265,20 @@ class AuthBridge { return effectiveCookie; } + const defaultDevToken = + process.env.NEXT_PUBLIC_DEFAULT_TOKEN || + process.env.NEXT_PUBLIC_AUTH_KEY; + + if ( + defaultDevToken && + defaultDevToken.trim() !== "" && + defaultDevToken !== "NO_TOKEN" && + this.token !== "NO_TOKEN" && + cookieToken !== "NO_TOKEN" + ) { + return defaultDevToken; + } + return null; } diff --git a/src/lib/marriage-field-formatter.ts b/src/lib/marriage-field-formatter.ts new file mode 100644 index 0000000..25d0c74 --- /dev/null +++ b/src/lib/marriage-field-formatter.ts @@ -0,0 +1,151 @@ +import type { + MarriageField, + MarriageFieldValue, + MarriagePhoneFieldValue, +} from "@/hooks/marriage/types"; +import { dictionaries } from "@/translations/dictionaries"; + +const persianToEnglishMap: Record = {}; +for (const [enKey, faVal] of Object.entries(dictionaries.fa)) { + if (typeof faVal === "string") { + persianToEnglishMap[faVal.trim()] = enKey; + } +} + +const normalizedEnglishKeys: Record = {}; +for (const enKey of Object.keys(dictionaries.en)) { + const norm = enKey.toLowerCase().replace(/[^a-z0-9]/g, ""); + if (norm) { + normalizedEnglishKeys[norm] = enKey; + } +} + +export function isMarriagePhoneFieldValue( + value: unknown, +): value is MarriagePhoneFieldValue { + if (!value || typeof value !== "object") { + return false; + } + + const phoneValue = value as Partial; + + return ( + typeof phoneValue.countryCode === "string" && + typeof phoneValue.phoneNumber === "string" + ); +} + +export function formatFieldValue(value: MarriageFieldValue): string | null { + if (value === null || value === "") { + return null; + } + + if (isMarriagePhoneFieldValue(value)) { + return `+${value.countryCode}${value.phoneNumber}`; + } + + if (typeof value === "boolean") { + return value ? "Yes" : "No"; + } + + return String(value); +} + +export function titleFromKey(key: string): string { + const leafKey = key.includes(".") ? key.split(".").pop()! : key; + return leafKey + .replace(/^q\d+[_-]?/i, "") + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " ") + .trim() + .replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +export function formatFieldLabel( + field: MarriageField, + dictionary?: Record, +): string { + const englishTitle = titleFromKey(field.key); + const rawLabel = (field.label || "").trim(); + + // 1. Direct dictionary match for field.label + if (rawLabel && dictionary?.[rawLabel]) { + return dictionary[rawLabel]; + } + + // 2. If rawLabel is in Persian, convert it to its canonical English key via reverse map + const enKeyFromFa = rawLabel ? persianToEnglishMap[rawLabel] : null; + if (enKeyFromFa) { + if (dictionary?.[enKeyFromFa]) { + return dictionary[enKeyFromFa]; + } + return enKeyFromFa; + } + + // 3. Direct match for English title derived from key + if (dictionary?.[englishTitle]) { + return dictionary[englishTitle]; + } + + // 4. Normalized match for key or label (e.g. ignoring punctuation/spaces like apostrophes and parentheses) + const normKey = (field.key || "").split(".").pop()?.toLowerCase().replace(/[^a-z0-9]/g, "") || ""; + const matchedCanonicalKey = normalizedEnglishKeys[normKey]; + if (matchedCanonicalKey) { + if (dictionary?.[matchedCanonicalKey]) { + return dictionary[matchedCanonicalKey]; + } + return matchedCanonicalKey; + } + + // 5. Fallback + return rawLabel || englishTitle; +} + +export function formatOptionValue( + value: MarriageFieldValue, + dictionary?: Record, +): string | null { + if (Array.isArray(value)) { + const formattedItems = value + .map((item) => formatOptionValue(item as MarriageFieldValue, dictionary)) + .filter(Boolean); + return formattedItems.length ? formattedItems.join(", ") : null; + } + + const base = formatFieldValue(value); + if (!base) return null; + + const trimmed = base.trim(); + + // 1. Direct dictionary match + if (dictionary?.[trimmed]) return dictionary[trimmed]; + + // 2. Reverse map if base is in Persian + const enKeyFromFa = persianToEnglishMap[trimmed]; + if (enKeyFromFa) { + if (dictionary?.[enKeyFromFa]) { + return dictionary[enKeyFromFa]; + } + return enKeyFromFa; + } + + // 3. Try replacing underscores with spaces: "single;_never_married" -> "single; never married" + const withSpaces = trimmed.replace(/_/g, " ").trim(); + if (dictionary?.[withSpaces]) return dictionary[withSpaces]; + + // 4. Try capitalized first letter + const capitalized = withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1); + if (dictionary?.[capitalized]) return dictionary[capitalized]; + + // 5. Normalized match for option value + const normVal = withSpaces.toLowerCase().replace(/[^a-z0-9]/g, ""); + const matchedCanonicalKey = normalizedEnglishKeys[normVal]; + if (matchedCanonicalKey) { + if (dictionary?.[matchedCanonicalKey]) { + return dictionary[matchedCanonicalKey]; + } + return matchedCanonicalKey; + } + + return withSpaces; +} diff --git a/src/lib/schema-adapter-overview.test.ts b/src/lib/schema-adapter-overview.test.ts index 5563bbc..9f07425 100644 --- a/src/lib/schema-adapter-overview.test.ts +++ b/src/lib/schema-adapter-overview.test.ts @@ -6,6 +6,7 @@ describe("convertOverviewToFrontendItems", () => { it("maps overview metadata without materializing questions", () => { const overview: FormOverviewResponse = { form_id: "profile", + version: 1, progress: { current_step: 1, total_steps: 2, diff --git a/src/lib/ssr-fetch.ts b/src/lib/ssr-fetch.ts index 8fe6da3..3b704d0 100644 --- a/src/lib/ssr-fetch.ts +++ b/src/lib/ssr-fetch.ts @@ -8,19 +8,33 @@ function getApiBaseUrl(): string { return process.env.NEXT_PUBLIC_API_BASE_URL || "http://127.0.0.1:8001"; } +/** + * Default timeout for SSR requests. + * In development mode (`npm run dev`), backend endpoints may take longer to respond, + * so we use a higher timeout (15s) to prevent AbortError. + * In production, we keep the strict 1500ms timeout for optimal TTFB. + */ +const DEFAULT_SSR_TIMEOUT_MS = + Number(process.env.SSR_FETCH_TIMEOUT_MS) || + (process.env.NODE_ENV === "development" ? 15000 : 1500); + /** * Fetches the marriage profile server-side. * Runs only on the server, typically inside a Next.js Server Component. * Returns null if the request fails, allowing the client-side React Query to handle retries. * * @param token - The user authentication token + * @param timeoutMs - Timeout in milliseconds (defaults to 15s in dev, 1.5s in prod) * @returns Profile data or null on failure */ export async function fetchProfileSSR( token?: string | null, - timeoutMs = 1500, + timeoutMs = DEFAULT_SSR_TIMEOUT_MS, ): Promise { - if (!token || token === "NO_TOKEN" || token.trim() === "") return null; + if (!token || token === "NO_TOKEN" || token.trim() === "") { + console.log("🖥️ [SSR fetchProfileSSR] No valid token found in cookies."); + return null; + } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); @@ -28,6 +42,9 @@ export async function fetchProfileSSR( try { const baseUrl = getApiBaseUrl(); const url = `${baseUrl}/api/marriage/profile/main/`; + console.log( + `🖥️ [SSR fetchProfileSSR] Fetching profile from ${url} with token: ${token.slice(0, 8)}...`, + ); const response = await fetch(url, { method: "GET", @@ -40,11 +57,24 @@ export async function fetchProfileSSR( }); if (!response.ok) { + console.warn( + `🖥️ [SSR fetchProfileSSR] Response not OK: status ${response.status}`, + ); return null; } - return await response.json(); + const data = await response.json(); + console.log("🖥️ [SSR fetchProfileSSR] Received profile data:", { + id: data?.id, + status: data?.status, + active_case: data?.active_case, + has_match_summary: Boolean(data?.match_summary), + match_summary_id: data?.match_summary?.id, + public_info_count: data?.match_summary?.public_info?.length, + }); + return data; } catch (error) { + console.error("🖥️ [SSR fetchProfileSSR] Fetch failed or timed out:", error); return null; } finally { clearTimeout(timeoutId); @@ -57,7 +87,7 @@ export async function fetchProfileSSR( * local fallback images. */ export async function fetchConfigSSR( - timeoutMs = 1500, + timeoutMs = DEFAULT_SSR_TIMEOUT_MS, ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); @@ -86,4 +116,3 @@ export async function fetchConfigSSR( clearTimeout(timeoutId); } } - diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index 1309b75..5513099 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -779,5 +779,11 @@ "Clear": "مسح", "This field is required": "هذه الخانة مطلوبة", "Habib Coins": "حبيب كوين", - "{} Habib Coins": "{} حبيب كوين" + "{} Habib Coins": "{} حبيب كوين", + "New Message": "رسالة جديدة", + "Text Message": "محادثة نصية", + "Voice Call": "مكالمة صوتية", + "Video Call": "مكالمة فيديو", + "No public information is available to display.": "لا توجد معلومات عامة متاحة للعرض.", + "General Information & Personal Details": "المعلومات العامة والتفاصيل الشخصية" } diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index a51d998..9bbafab 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -779,5 +779,11 @@ "Clear": "Təmizlə", "This field is required": "Bu sahə zəruridir", "Habib Coins": "Həbib Koin", - "{} Habib Coins": "{} Həbib Koin" + "{} Habib Coins": "{} Həbib Koin", + "New Message": "Yeni Mesajlar", + "Text Message": "Mətn Mesajı", + "Voice Call": "Səsli Zəng", + "Video Call": "Video Zəng", + "No public information is available to display.": "Göstəriləcək heç bir ictimai məlumat qeyd edilməyib.", + "General Information & Personal Details": "Ümumi məlumat və fərdi xüsusiyyətlər" } diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index 3c50511..24ed516 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -779,5 +779,11 @@ "Clear": "মুছুন", "This field is required": "এই ক্ষেত্রটি আবশ্যক", "Habib Coins": "হাবিব কয়েন", - "{} Habib Coins": "{} হাবিব কয়েন" + "{} Habib Coins": "{} হাবিব কয়েন", + "New Message": "নতুন মেসেজ", + "Text Message": "টেক্সট বার্তা", + "Voice Call": "ভয়েস কল", + "Video Call": "ভিডিও কল", + "No public information is available to display.": "প্রদর্শনের জন্য কোনও সর্বজনীন তথ্য পাওয়া যায়নি।", + "General Information & Personal Details": "সাধারণ তথ্য এবং ব্যক্তিগত বিবরণ" } diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 865a79c..9b6464d 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -779,5 +779,11 @@ "Clear": "Ryd", "This field is required": "Dette felt er påkrævet", "Habib Coins": "Habib-mønter", - "{} Habib Coins": "{} Habib-mønter" + "{} Habib Coins": "{} Habib-mønter", + "New Message": "Ny besked", + "Text Message": "Tekstbesked", + "Voice Call": "Taleopkald", + "Video Call": "Videoopkald", + "No public information is available to display.": "Ingen offentlige oplysninger er tilgængelige.", + "General Information & Personal Details": "Generelle oplysninger og personlige detaljer" } diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index abffdd8..7ddbdf4 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -779,5 +779,11 @@ "Clear": "Löschen", "This field is required": "Dieses Feld ist erforderlich", "Habib Coins": "Habib-Münzen", - "{} Habib Coins": "{} Habib-Münzen" + "{} Habib Coins": "{} Habib-Münzen", + "New Message": "Neue Nachricht", + "Text Message": "Textnachricht", + "Voice Call": "Sprachanruf", + "Video Call": "Videoanruf", + "No public information is available to display.": "Keine öffentlichen Informationen zur Anzeige verfügbar.", + "General Information & Personal Details": "Allgemeine Informationen und persönliche Details" } diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 84c59ac..99fa0d7 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -828,5 +828,10 @@ "Clear": "Clear", "This field is required": "This field is required", "Habib Coins": "Habib Coins", - "{} Habib Coins": "{} Habib Coins" + "{} Habib Coins": "{} Habib Coins", + "New Message": "New Message", + "Text Message": "Text Message", + "Video Call": "Video Call", + "No public information is available to display.": "No public information is available to display.", + "General Information & Personal Details": "General Information & Personal Details" } diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index c2a992f..36130b7 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -779,5 +779,11 @@ "Clear": "Borrar", "This field is required": "Este campo es obligatorio", "Habib Coins": "Monedas Habib", - "{} Habib Coins": "{} Monedas Habib" + "{} Habib Coins": "{} Monedas Habib", + "New Message": "Nuevos mensajes", + "Text Message": "Mensaje de Texto", + "Voice Call": "Llamada de Voz", + "Video Call": "Llamada de Video", + "No public information is available to display.": "No hay información pública disponible para mostrar.", + "General Information & Personal Details": "Información general y detalles personales" } diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 6dc130c..5445a0a 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -828,5 +828,10 @@ "Clear": "پاک کردن", "This field is required": "این فیلد ضروری است", "Habib Coins": "حبیب کوین", - "{} Habib Coins": "{} حبیب کوین" + "{} Habib Coins": "{} حبیب کوین", + "New Message": "پیام متنی جدید", + "Text Message": "پیام متنی", + "Video Call": "تماس تصویری", + "No public information is available to display.": "اطلاعات عمومی قابل نمایشی ثبت نشده است.", + "General Information & Personal Details": "اطلاعات عمومی و مشخصات فردی" } diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 85260fd..5800140 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -779,5 +779,11 @@ "Clear": "Effacer", "This field is required": "Ce champ est requis", "Habib Coins": "Pièces Habib", - "{} Habib Coins": "{} Pièces Habib" + "{} Habib Coins": "{} Pièces Habib", + "New Message": "Nouveaux messages", + "Text Message": "Message texte", + "Voice Call": "Appel vocal", + "Video Call": "Appel vidéo", + "No public information is available to display.": "Aucune information publique disponible à afficher.", + "General Information & Personal Details": "Informations générales et détails personnels" } diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index c639095..208f5b2 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -779,5 +779,11 @@ "Clear": "સાફ કરો", "This field is required": "આ ક્ષેત્ર આવશ્યક છે", "Habib Coins": "હબીબ સિક્કા", - "{} Habib Coins": "{} હબીબ કોઈન્સ" + "{} Habib Coins": "{} હબીબ કોઈન્સ", + "New Message": "New Message", + "Text Message": "ટેક્સ્ટ સંદેશ", + "Voice Call": "વૉઇસ કૉલ", + "Video Call": "વિડિયો કૉલ", + "No public information is available to display.": "પ્રદર્શિત કરવા માટે કોઈ જાહેર માહિતી ઉપલબ્ધ નથી.", + "General Information & Personal Details": "સામાન્ય માહિતી અને વ્યક્તિગત વિગતો" } diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index e90559f..d39d20e 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -779,5 +779,11 @@ "Clear": "Share", "This field is required": "Ana buƙatar wannan filin", "Habib Coins": "Kuɗin Habib", - "{} Habib Coins": "Tsabar Kudin Habib {}" + "{} Habib Coins": "Tsabar Kudin Habib {}", + "New Message": "Sabon Saƙo", + "Text Message": "Sakon Rubutu", + "Voice Call": "Kiran Murya", + "Video Call": "Kiran Bidiyo", + "No public information is available to display.": "Babu bayanan jama'a da za a iya nunawa.", + "General Information & Personal Details": "Bayanai na gama-gari da cikakkun bayanan sirri" } diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index 40af5b4..61ce359 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -316,5 +316,11 @@ "Clear": "Clear", "This field is required": "This field is required", "Habib Coins": "Habib Coins", - "{} Habib Coins": "{} Habib Coins" + "{} Habib Coins": "{} Habib Coins", + "New Message": "New Message", + "Text Message": "Text Message", + "Voice Call": "Voice Call", + "Video Call": "Video Call", + "No public information is available to display.": "אין מידע ציבורי זמין להצגה.", + "General Information & Personal Details": "מידע כללי ופרטים אישיים" } diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index 9b3c954..c8d07f8 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -779,5 +779,11 @@ "Clear": "साफ़ करें", "This field is required": "यह फ़ील्ड आवश्यक है", "Habib Coins": "हबीब कॉइन्स", - "{} Habib Coins": "{} हबीब कॉइन्स" + "{} Habib Coins": "{} हबीब कॉइन्स", + "New Message": "नया संदेश", + "Text Message": "टेक्स्ट मैसेज", + "Voice Call": "वॉइस कॉल", + "Video Call": "वीडियो कॉल", + "No public information is available to display.": "प्रदर्शित करने के लिए कोई सार्वजनिक जानकारी उपलब्ध नहीं है।", + "General Information & Personal Details": "सामान्य जानकारी और व्यक्तिगत विवरण" } diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index 95a4897..51dbe6e 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -316,5 +316,11 @@ "Clear": "Hapus", "This field is required": "Bidang ini wajib diisi", "Habib Coins": "Koin Habib", - "{} Habib Coins": "{} Koin Habib" + "{} Habib Coins": "{} Koin Habib", + "New Message": "Pesan Baru", + "Text Message": "Pesan Teks", + "Voice Call": "Panggilan Suara", + "Video Call": "Panggilan Video", + "No public information is available to display.": "Tidak ada informasi publik yang tersedia untuk ditampilkan.", + "General Information & Personal Details": "Informasi Umum & Rincian Pribadi" } diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index 764c489..1fc72dd 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -316,5 +316,11 @@ "Clear": "صاف کٔرِو", "This field is required": "یہ فیلڈ ضروری چھُ", "Habib Coins": "حبیب کوینس", - "{} Habib Coins": "{} حبیب کوین" + "{} Habib Coins": "{} حبیب کوین", + "New Message": "New Message", + "Text Message": "ٹیکسٹ پیغام", + "Voice Call": "آوازی کال", + "Video Call": "ویڈیو کال", + "No public information is available to display.": "ڈسپلے کرنہٕ خٲطرٕ کانہہ عام معلومات دٔستیاب چُھنہٕ۔", + "General Information & Personal Details": "عام معلومات تہٕ ذٲتی تفصیٖل" } diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index 0ba23fb..cd10a27 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -316,5 +316,11 @@ "Clear": "Limpar", "This field is required": "Este campo é obrigatório", "Habib Coins": "Moedas Habib", - "{} Habib Coins": "{} Moedas Habib" + "{} Habib Coins": "{} Moedas Habib", + "New Message": "Nova Mensagem", + "Text Message": "Mensagem de Texto", + "Voice Call": "Chamada de Voz", + "Video Call": "Chamada de Vídeo", + "No public information is available to display.": "Nenhuma informação pública disponível para exibição.", + "General Information & Personal Details": "Informações Gerais e Detalhes Pessoais" } diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 7a62198..508d103 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -783,5 +783,11 @@ "Clear": "Очистить", "This field is required": "Это поле обязательно", "Habib Coins": "Хабиб Коины", - "{} Habib Coins": "{} Хабиб Коины" + "{} Habib Coins": "{} Хабиб Коины", + "New Message": "Новые сообщения", + "Text Message": "Текстовое сообщение", + "Voice Call": "Голосовой звонок", + "Video Call": "Видеозвонок", + "No public information is available to display.": "Нет общедоступной информации для отображения.", + "General Information & Personal Details": "Общая информация и личные данные" } diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 0973979..adec3d4 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -316,5 +316,11 @@ "Clear": "Futa", "This field is required": "Sehemu hii inahitajika", "Habib Coins": "Sarafu za Habib", - "{} Habib Coins": "{} Sarafu za Habib" + "{} Habib Coins": "{} Sarafu za Habib", + "New Message": "Ujumbe Mpya", + "Text Message": "Ujumbe wa Maandishi", + "Voice Call": "Simu ya Sauti", + "Video Call": "Simu ya Video", + "No public information is available to display.": "Hakuna taarifa za umma zinazoweza kuonyeshwa.", + "General Information & Personal Details": "Taarifa za Jumla na Maelezo Binafsi" } diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index 4b74fd5..c103341 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -316,5 +316,11 @@ "Clear": "Покардан", "This field is required": "Ин майдон ҳатмист", "Habib Coins": "Тангаҳои Ҳабиб", - "{} Habib Coins": "{} Тангаҳои Ҳабиб" + "{} Habib Coins": "{} Тангаҳои Ҳабиб", + "New Message": "Паёми нав", + "Text Message": "Паёми матнӣ", + "Voice Call": "Зангҳои овозӣ", + "Video Call": "Зангҳои видеоӣ", + "No public information is available to display.": "Маълумоти умумии дастрас барои намоиш нест.", + "General Information & Personal Details": "Маълумоти умумӣ ва мушаххасоти инфиродӣ" } diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index 02b7df8..5e69373 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -316,5 +316,11 @@ "Clear": "Temizle", "This field is required": "Bu alan gereklidir", "Habib Coins": "Habib Coin", - "{} Habib Coins": "{} Habib Coin" + "{} Habib Coins": "{} Habib Coin", + "New Message": "Yeni Mesaj", + "Text Message": "Yazılı Mesaj", + "Voice Call": "Sesli Arama", + "Video Call": "Görüntülü Arama", + "No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.", + "General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar" } diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index c5ce5e9..e562cc9 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -316,5 +316,11 @@ "Clear": "Saaf karein", "This field is required": "Ye Field Zaroori Hai", "Habib Coins": "Habib Coins", - "{} Habib Coins": "{} Habib Coins" + "{} Habib Coins": "{} Habib Coins", + "New Message": "Naya paigham", + "Text Message": "Text Message", + "Voice Call": "Voice Call", + "Video Call": "Video Call", + "No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.", + "General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar" } diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 4a366b9..ec760a4 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -316,5 +316,11 @@ "Clear": "صاف کریں", "This field is required": "یہ فیلڈ ضروری ہے", "Habib Coins": "حبیب کوائنز", - "{} Habib Coins": "{} حبیب کوائن" + "{} Habib Coins": "{} حبیب کوائن", + "New Message": "نئے پیغامات", + "Text Message": "پیغام", + "Voice Call": "وائس کال", + "Video Call": "ویڈیو کال", + "No public information is available to display.": "دکھانے کے لیے کوئی عوامی معلومات دستیاب نہیں ہے۔", + "General Information & Personal Details": "عام معلومات اور ذاتی تفصیلات" } diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index 1ead0fa..c0f2c72 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -316,5 +316,11 @@ "Clear": "Tozalash", "This field is required": "Бу майдон тўлдирилиши шарт", "Habib Coins": "Habib tangalari", - "{} Habib Coins": "{} Habib tangalari" + "{} Habib Coins": "{} Habib tangalari", + "New Message": "Янги хабар", + "Text Message": "Матнли хабар", + "Voice Call": "Овозли қўнғироқ", + "Video Call": "Видео қўнғироқ", + "No public information is available to display.": "Ko'rsatish uchun umumiy ma'lumot mavjud emas.", + "General Information & Personal Details": "Umumiy ma'lumotlar va shaxsiy tafsilotlar" } diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index 48088f6..8fa35b9 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -779,5 +779,11 @@ "Clear": "清除", "This field is required": "此字段为必填项", "Habib Coins": "哈比卜币", - "{} Habib Coins": "{} 哈比布金币" + "{} Habib Coins": "{} 哈比布金币", + "New Message": "新消息", + "Text Message": "短信", + "Voice Call": "语音通话", + "Video Call": "视频通话", + "No public information is available to display.": "没有可显示的公开信息。", + "General Information & Personal Details": "一般信息和个人资料" }