From ea5d15c54c95447282480d34a5badd74d966005d Mon Sep 17 00:00:00 2001 From: mortezaei Date: Wed, 19 Aug 2026 14:35:24 +0330 Subject: [PATCH] refactor: move marriage field formatting logic to library and add configurable SSR timeouts with debug logging --- src/app/new-match/new-match-client.tsx | 93 ++------------- src/app/new-match/profile/page.tsx | 93 ++------------- src/lib/marriage-field-formatter.ts | 151 +++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 172 deletions(-) create mode 100644 src/lib/marriage-field-formatter.ts diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index 5a4f9fc..c5e2058 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -148,92 +148,13 @@ function calculateAgeFromDob(dobString: string): number | null { return age > 0 && age < 120 ? age : null; } -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; - 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) { - 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()); -} - -function formatFieldLabel( - field: MarriageField, - dictionary?: Record, -): string { - const englishTitle = titleFromKey(field.key); - if (!dictionary) return field.label || englishTitle; - - if (field.label && dictionary[field.label]) { - return dictionary[field.label]; - } - - if (dictionary[englishTitle]) { - return dictionary[englishTitle]; - } - - return field.label || englishTitle; -} +import { + formatFieldLabel, + formatFieldValue, + formatOptionValue, + isMarriagePhoneFieldValue, + titleFromKey, +} from "@/lib/marriage-field-formatter"; function toDisplayField( field: MarriageField, diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index 84031f9..58b962f 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -26,92 +26,13 @@ import { markMatchStarted } from "@/lib/match-start-grace"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; -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; - 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) { - 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()); -} - -function formatFieldLabel( - field: MarriageField, - dictionary?: Record, -): string { - const englishTitle = titleFromKey(field.key); - if (!dictionary) return field.label || englishTitle; - - if (field.label && dictionary[field.label]) { - return dictionary[field.label]; - } - - if (dictionary[englishTitle]) { - return dictionary[englishTitle]; - } - - return field.label || englishTitle; -} +import { + formatFieldLabel, + formatFieldValue, + formatOptionValue, + isMarriagePhoneFieldValue, + titleFromKey, +} from "@/lib/marriage-field-formatter"; function isImageField(field: MarriageField) { return /(avatar|image|photo|picture|portrait|upload)/i.test( 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; +}