Browse Source

feat: implement multi-language support and comprehensive question-based marriage matching workflow

front-test-2
ghorbani 3 weeks ago
parent
commit
6309fe2897
  1. 45
      scratch/add_translations.py
  2. 93
      src/app/globals.css
  3. 246
      src/app/questions-list/[slug]/question-detail-client.tsx
  4. 24
      src/app/questions-list/page.tsx
  5. 208
      src/components/questions/progress-helper.ts
  6. 6
      src/components/questions/question-answer-storage.tsx
  7. 41
      src/components/questions/question-card.tsx
  8. 18
      src/components/questions/question-checkbox.tsx
  9. 12
      src/components/questions/question-date.tsx
  10. 580
      src/components/questions/question-number.tsx
  11. 10
      src/components/questions/question-phone.tsx
  12. 6
      src/components/questions/question-snap-list.tsx
  13. 10
      src/components/questions/question-text.tsx
  14. 25
      src/components/questions/question-title.tsx
  15. 27
      src/components/questions/required-steps-card.tsx
  16. 61
      src/components/ui/help-modal.tsx
  17. 59
      src/data/question-data.ts
  18. 74
      src/data/questions/en.json
  19. 60
      src/data/questions/fa.json
  20. 4
      src/data/section-slug-map.ts
  21. 147
      src/hooks/marriage/use-section-data.ts
  22. 4
      src/translations/locales/ar.json
  23. 4
      src/translations/locales/az.json
  24. 4
      src/translations/locales/bn.json
  25. 4
      src/translations/locales/da.json
  26. 4
      src/translations/locales/de.json
  27. 4
      src/translations/locales/en.json
  28. 4
      src/translations/locales/es.json
  29. 4
      src/translations/locales/fa.json
  30. 4
      src/translations/locales/fr.json
  31. 4
      src/translations/locales/gu.json
  32. 4
      src/translations/locales/ha.json
  33. 4
      src/translations/locales/he.json
  34. 4
      src/translations/locales/hi.json
  35. 4
      src/translations/locales/id.json
  36. 4
      src/translations/locales/ks.json
  37. 4
      src/translations/locales/pt.json
  38. 4
      src/translations/locales/ru.json
  39. 4
      src/translations/locales/sw.json
  40. 4
      src/translations/locales/tg.json
  41. 4
      src/translations/locales/tr.json
  42. 4
      src/translations/locales/ul.json
  43. 4
      src/translations/locales/ur.json
  44. 4
      src/translations/locales/uz.json
  45. 4
      src/translations/locales/zh.json

45
scratch/add_translations.py

@ -0,0 +1,45 @@
import os
import json
translations = {
"ar": {"title": "الخلفية العائلية، الحالة الاجتماعية والأطفال", "estimate": "20 دقيقة"},
"az": {"title": "Ailə Keçmişi, Ailə Vəziyyəti və Uşaqlar", "estimate": "20 dəqiqə"},
"bn": {"title": "পারিবারিক পটভূমি, বৈবাহিক অবস্থা এবং সন্তানাদি", "estimate": "20 মিনিট"},
"zh": {"title": "家庭背景、婚姻状况和子女", "estimate": "20 分钟"},
"da": {"title": "Familiebaggrund, civilstand og børn", "estimate": "20 minutter"},
"de": {"title": "Familiärer Hintergrund, Familienstand und Kinder", "estimate": "20 Minuten"},
"en": {"title": "Family Background, Marital Status, and Children", "estimate": "20 minutes"},
"es": {"title": "Antecedentes familiares, estado civil e hijos", "estimate": "20 minutos"},
"fa": {"title": "پیشینه خانوادگی، وضعیت تأهل و فرزندان", "estimate": "20 دقیقه"},
"fr": {"title": "Antécédents familiaux, état civil et enfants", "estimate": "20 minutes"},
"gu": {"title": "પારિવારિક પૃષ્ઠભૂમિ, વૈવાહિક સ્થિતિ અને બાળકો", "estimate": "20 મિનિટ"},
"ha": {"title": "Tarihin Iyali, Yanayin Aure da Yara", "estimate": "Minti 20"},
"he": {"title": "רקע משפחתי, מצב משפחתי וילדים", "estimate": "20 דקות"},
"hi": {"title": "पारिवारिक पृष्ठभूमि, वैवाहिक स्थिति और बच्चे", "estimate": "20 मिनट"},
"id": {"title": "Latar Belakang Keluarga, Status Pernikahan, dan Anak-anak", "estimate": "20 menit"},
"ks": {"title": "خاندانی پس منظر، ازدواجی حیثیت تہٰ شرے", "estimate": "20 منٹ"},
"pt": {"title": "Histórico familiar, estado civil e filhos", "estimate": "20 minutos"},
"ru": {"title": "Семейное положение, история брака и дети", "estimate": "20 минут"},
"sw": {"title": "Historia ya Familia, Hali ya Ndoa na Watoto", "estimate": "Dakika 20"},
"tg": {"title": "Маълумоти оилавӣ, вазъи оилавӣ ва кӯдакон", "estimate": "20 дақиқа"},
"tr": {"title": "Aile Geçmişi, Medeni Durum ve Çocuklar", "estimate": "20 dakika"},
"ul": {"title": "Khandani Pas-manzar, Azdawaji Haisiyat aur Bacche", "estimate": "20 minutes"},
"ur": {"title": "خاندانی پس منظر، ازدواجی حیثیت اور بچے", "estimate": "20 منٹ"},
"uz": {"title": "Oila tarixi, oilaviy ahvol va bolalar", "estimate": "20 daqiqa"}
}
locales_dir = "src/translations/locales"
for lang, data in translations.items():
file_path = os.path.join(locales_dir, f"{lang}.json")
if not os.path.exists(file_path):
print(f"Skipping {file_path} - not found")
continue
with open(file_path, "r", encoding="utf-8") as f:
content = json.load(f)
if "questions" not in content:
content["questions"] = {}
content["questions"]["familyMaritalTitle"] = data["title"]
content["questions"]["familyMaritalEstimate"] = data["estimate"]
with open(file_path, "w", encoding="utf-8") as f:
json.dump(content, f, ensure_ascii=False, indent=2)
print(f"Updated {file_path}")

93
src/app/globals.css

@ -6,6 +6,69 @@
--safe-bottom: 0px;
--safe-left: 0px;
--safe-right: 0px;
/* ─── Semantic Tokens (Light) ─── */
--semantic-neutral-bg: #F1F5F9;
--semantic-neutral-text: #475569;
--semantic-neutral-border: #CBD5E1;
--semantic-neutral-icon: #64748B;
--semantic-info-bg: #EFF6FF;
--semantic-info-text: #1D4ED8;
--semantic-info-border: #BFDBFE;
--semantic-info-icon: #2563EB;
--semantic-success-bg: #ECFDF5;
--semantic-success-text: #047857;
--semantic-success-border: #A7F3D0;
--semantic-success-icon: #059669;
--semantic-warning-bg: #FFFBEB;
--semantic-warning-text: #B45309;
--semantic-warning-border: #FDE68A;
--semantic-warning-icon: #D97706;
--semantic-danger-bg: #FEF2F2;
--semantic-danger-text: #B91C1C;
--semantic-danger-border: #FECACA;
--semantic-danger-icon: #DC2626;
--semantic-ai-bg: #F5F3FF;
--semantic-ai-text: #6D28D9;
--semantic-ai-border: #DDD6FE;
--semantic-ai-icon: #7C3AED;
}
.dark {
--semantic-neutral-bg: #1E293B;
--semantic-neutral-text: #E2E8F0;
--semantic-neutral-border: #475569;
--semantic-neutral-icon: #CBD5E1;
--semantic-info-bg: #172554;
--semantic-info-text: #BFDBFE;
--semantic-info-border: #1D4ED8;
--semantic-info-icon: #93C5FD;
--semantic-success-bg: #052E2B;
--semantic-success-text: #A7F3D0;
--semantic-success-border: #047857;
--semantic-success-icon: #6EE7B7;
--semantic-warning-bg: #451A03;
--semantic-warning-text: #FDE68A;
--semantic-warning-border: #B45309;
--semantic-warning-icon: #FCD34D;
--semantic-danger-bg: #450A0A;
--semantic-danger-text: #FECACA;
--semantic-danger-border: #B91C1C;
--semantic-danger-icon: #FCA5A5;
--semantic-ai-bg: #2E1065;
--semantic-ai-text: #DDD6FE;
--semantic-ai-border: #7C3AED;
--semantic-ai-icon: #C4B5FD;
}
@theme inline {
@ -16,6 +79,36 @@
--font-arabic: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--font-ryling: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--font-faminela: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
--color-semantic-neutral-bg: var(--semantic-neutral-bg);
--color-semantic-neutral-text: var(--semantic-neutral-text);
--color-semantic-neutral-border: var(--semantic-neutral-border);
--color-semantic-neutral-icon: var(--semantic-neutral-icon);
--color-semantic-info-bg: var(--semantic-info-bg);
--color-semantic-info-text: var(--semantic-info-text);
--color-semantic-info-border: var(--semantic-info-border);
--color-semantic-info-icon: var(--semantic-info-icon);
--color-semantic-success-bg: var(--semantic-success-bg);
--color-semantic-success-text: var(--semantic-success-text);
--color-semantic-success-border: var(--semantic-success-border);
--color-semantic-success-icon: var(--semantic-success-icon);
--color-semantic-warning-bg: var(--semantic-warning-bg);
--color-semantic-warning-text: var(--semantic-warning-text);
--color-semantic-warning-border: var(--semantic-warning-border);
--color-semantic-warning-icon: var(--semantic-warning-icon);
--color-semantic-danger-bg: var(--semantic-danger-bg);
--color-semantic-danger-text: var(--semantic-danger-text);
--color-semantic-danger-border: var(--semantic-danger-border);
--color-semantic-danger-icon: var(--semantic-danger-icon);
--color-semantic-ai-bg: var(--semantic-ai-bg);
--color-semantic-ai-text: var(--semantic-ai-text);
--color-semantic-ai-border: var(--semantic-ai-border);
--color-semantic-ai-icon: var(--semantic-ai-icon);
}
/* Typography Configuration - Centralized Design System Scale */

246
src/app/questions-list/[slug]/question-detail-client.tsx

@ -119,29 +119,31 @@ function getStoredAge() {
}
const storedAnswers = JSON.parse(rawValue) as StoredAnswers;
const ageField = storedAnswers.fields?.find(
(field) =>
field.type === "number" ||
field.label === "Age" ||
field.label === "سن" ||
(typeof (field as any).key === "string" &&
((field as any).key.endsWith("_age") ||
(field as any).key.endsWith("_sn"))),
const ageField = storedAnswers.fields?.find((field) => {
const f = field as { key?: string; type?: string; label?: string };
return (
f.type === "number" ||
f.label === "Age" ||
f.label === "سن" ||
(typeof f.key === "string" &&
(f.key.endsWith("_age") || f.key.endsWith("_sn")))
);
});
if (ageField) {
return parseStoredAge(ageField.value);
}
const dateOfBirthField = storedAnswers.fields?.find(
(field) =>
field.type === "date" ||
field.label === "Date of Birth" ||
field.label === "تاریخ تولد" ||
(typeof (field as any).key === "string" &&
((field as any).key.endsWith("_date_of_birth") ||
(field as any).key.endsWith("_tarykh_twld"))),
const dateOfBirthField = storedAnswers.fields?.find((field) => {
const f = field as { key?: string; type?: string; label?: string };
return (
f.type === "date" ||
f.label === "Date of Birth" ||
f.label === "تاریخ تولد" ||
(typeof f.key === "string" &&
(f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld")))
);
});
return parseStoredAge(dateOfBirthField?.value);
} catch {
@ -168,7 +170,188 @@ function QuestionFlowWrapper({
const { getAnswerValue } = useQuestionAnswers();
const dynamicQuestions = useMemo(() => {
return visibleQuestions.filter((question) => {
// Find employment status question by title or by choices length (10)
const employmentQuestionIndex = visibleQuestions.findIndex((q) => {
return (
q.title === "Employment Status" ||
q.title === "وضعیت اشتغال" ||
(q.type === "dropdown" && q.extras.options?.length === 10)
);
});
let employmentSelectedOptionIndex = -1;
if (employmentQuestionIndex !== -1) {
const employmentQuestion = visibleQuestions[employmentQuestionIndex];
const ans = getAnswerValue(employmentQuestion, employmentQuestionIndex);
if (ans) {
employmentSelectedOptionIndex =
employmentQuestion.extras.options?.indexOf(String(ans)) ?? -1;
}
}
// Find Parents' Marital Status question index and check selected index
const maritalStatusQuestionIndex = visibleQuestions.findIndex((q) => {
return (
q.title === "Parents' Marital Status" || q.title === "وضعیت تأهل والدین"
);
});
let isCircumstancesSelected = false;
if (maritalStatusQuestionIndex !== -1) {
const maritalQuestion = visibleQuestions[maritalStatusQuestionIndex];
const ans = getAnswerValue(maritalQuestion, maritalStatusQuestionIndex);
if (ans) {
const selectedIdx =
maritalQuestion.extras.options?.indexOf(String(ans)) ?? -1;
isCircumstancesSelected = selectedIdx === 2;
}
}
// Find Current Marital Status question index and check selected index
const currentMaritalQuestionIndex = visibleQuestions.findIndex((q) => {
return (
q.title === "Current Marital Status" || q.title === "وضعیت تأهل فعلی"
);
});
let maritalSelectedIndex = -1;
if (currentMaritalQuestionIndex !== -1) {
const maritalQuestion = visibleQuestions[currentMaritalQuestionIndex];
const ans = getAnswerValue(maritalQuestion, currentMaritalQuestionIndex);
if (ans) {
maritalSelectedIndex =
maritalQuestion.extras.options?.indexOf(String(ans)) ?? -1;
}
}
// Find Children and Guardianship Status question index
const custodyQuestionIndex = visibleQuestions.findIndex((q) => {
return (
q.title === "Children and Guardianship Status" ||
q.title === "وضعیت فرزند و تکفل"
);
});
let hasChildrenSelected = false;
let hasAnyGuardianshipSelected = false;
if (custodyQuestionIndex !== -1) {
const custodyQuestion = visibleQuestions[custodyQuestionIndex];
const ans = getAnswerValue(custodyQuestion, custodyQuestionIndex);
if (ans) {
const ansList = Array.isArray(ans) ? ans.map(String) : [String(ans)];
hasChildrenSelected = ansList.some(
(val) => val.includes("Have children") || val.includes("فرزند دارم"),
);
hasAnyGuardianshipSelected = ansList.some(
(val) =>
val.includes("Have children") ||
val.includes("فرزند دارم") ||
val.includes("under my guardianship") ||
val.includes("تحت تکفل"),
);
}
}
const filtered = visibleQuestions.filter((question, index) => {
// Check if this is one of the marital status/children questions
if (currentMaritalQuestionIndex !== -1) {
const isDuration =
index === currentMaritalQuestionIndex + 1 ||
question.title === "Previous Marriage Duration" ||
question.title === "مدت ازدواج یا عقد قبلی";
const isSeparation =
index === currentMaritalQuestionIndex + 2 ||
question.title === "Reason for Separation" ||
question.title === "علت جدایی، در صورت وجود";
const isCustody =
index === currentMaritalQuestionIndex + 3 ||
question.title === "Children and Guardianship Status" ||
question.title === "وضعیت فرزند و تکفل";
const isChildrenCount =
index === currentMaritalQuestionIndex + 4 ||
question.title === "Number of Children" ||
question.title === "تعداد فرزندان";
const isChildrenExplanation =
index === currentMaritalQuestionIndex + 5 ||
question.title === "Short Children/Guardianship Explanation" ||
question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل";
if (isDuration) {
return [1, 2, 3].includes(maritalSelectedIndex);
}
if (isSeparation) {
return [1, 2].includes(maritalSelectedIndex);
}
if (isCustody) {
return [2, 3].includes(maritalSelectedIndex);
}
if (isChildrenCount) {
return [2, 3].includes(maritalSelectedIndex) && hasChildrenSelected;
}
if (isChildrenExplanation) {
return (
[2, 3].includes(maritalSelectedIndex) && hasAnyGuardianshipSelected
);
}
}
// Check if this is one of the three job-related questions
if (employmentQuestionIndex !== -1) {
const isJobTitle =
index === employmentQuestionIndex + 1 ||
question.title === "Job Title" ||
question.title === "عنوان شغلی";
const isWorkLocation =
index === employmentQuestionIndex + 2 ||
question.title === "Work Location" ||
question.title === "محل فعالیت";
const isMonthlyIncome =
index === employmentQuestionIndex + 3 ||
question.title === "Monthly Income" ||
question.title === "میزان درآمد ماهانه";
if (isJobTitle || isWorkLocation || isMonthlyIncome) {
// If no employment status is selected yet, hide them by default
if (employmentSelectedOptionIndex === -1) {
return false;
}
// Options logic:
// Show all 3 for: index 0 (Full-time), 1 (Part-time), 2 (Self-employed), 3 (Entrepreneur), 5 (Working Student)
const showAll = [0, 1, 2, 3, 5].includes(
employmentSelectedOptionIndex,
);
// Hide all 3 for: index 4 (Student), 6 (Student & Job Seeking), 7 (Job Seeking / Unemployed), 8 (Homemaker)
const hideAll = [4, 6, 7, 8].includes(employmentSelectedOptionIndex);
// Special Retired logic: index 9 (Retired)
const isRetired = employmentSelectedOptionIndex === 9;
if (showAll) {
return true;
}
if (hideAll) {
return false;
}
if (isRetired) {
if (isJobTitle || isMonthlyIncome) {
return true;
}
if (isWorkLocation) {
return false;
}
}
return false;
}
}
// Default dependsOn logic
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const dependentQuestionIndex = visibleQuestions.findIndex(
@ -187,6 +370,33 @@ function QuestionFlowWrapper({
}
return true;
});
return filtered.map((question) => {
if (
question.title === "Short Family Description" ||
question.title === "توضیح کوتاه درباره خانواده"
) {
return {
...question,
required: isCircumstancesSelected,
};
}
if (
question.title === "Previous Marriage Duration" ||
question.title === "مدت ازدواج یا عقد قبلی" ||
question.title === "Number of Children" ||
question.title === "تعداد فرزندان" ||
question.title === "Short Children/Guardianship Explanation" ||
question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل"
) {
return {
...question,
required: true,
};
}
return question;
});
}, [visibleQuestions, getAnswerValue]);
const requiredCount = useMemo(
@ -314,7 +524,7 @@ export default function QuestionDetailClient({
options: mappedOptions,
};
});
}, [cattellQuery.data]);
}, [cattellQuery.data, locale]);
const glasserTestQuestions: TestQuestion[] = useMemo(() => {
const questionsList =

24
src/app/questions-list/page.tsx

@ -8,7 +8,6 @@ import {
getLocalSectionProgress,
getStoredAge,
} from "@/components/questions/progress-helper";
import { hasQuestionAnswerValue } from "@/components/questions/question-answer-storage";
import QuestionCard from "@/components/questions/question-card";
import RequiredStepsCard from "@/components/questions/required-steps-card";
import Button from "@/components/ui/button";
@ -18,8 +17,6 @@ import { PageBackground } from "@/components/utils/page-background";
import {
getQuestionListItems,
isQuestionListItemVisibleForProfile,
isQuestionRequiredForProfile,
isQuestionVisibleForProfile,
type QuestionListItem,
} from "@/data/question-data";
import { toFrontendSlug } from "@/data/section-slug-map";
@ -81,6 +78,27 @@ export default function QuestionsListPage() {
progressBySlug.set(section.slug, progress);
});
const fbSec = sections?.find((s) => s.slug === "family_background");
const mhSec = sections?.find((s) => s.slug === "marital_history");
if (fbSec || mhSec) {
const fbTotal = fbSec?.total_steps ?? 6;
const fbCurrent = fbSec
? Math.round((fbSec.completion_percent / 100) * fbTotal)
: 0;
const mhTotal = mhSec?.total_steps ?? 6;
const mhCurrent = mhSec
? Math.round((mhSec.completion_percent / 100) * mhTotal)
: 0;
const combinedProgress =
fbTotal + mhTotal > 0
? ((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100
: 0;
progressBySlug.set(
"family_marital_history",
Math.max(0, Math.min(100, Math.round(combinedProgress))),
);
}
questionListItems.forEach((item) => {
const localProgress = getLocalSectionProgress(item, profile, age);
if (localProgress !== null) {

208
src/components/questions/progress-helper.ts

@ -1,8 +1,11 @@
import {
isQuestionRequiredForProfile,
isQuestionVisibleForProfile,
type QuestionListItem,
} from "@/data/question-data";
import type {
MarriageFieldValue,
MarriageGender,
} from "@/hooks/marriage/types";
import { hasQuestionAnswerValue } from "./question-answer-storage";
export function getStoredAge(): number | null {
@ -14,7 +17,7 @@ export function getStoredAge(): number | null {
if (!rawValue) return null;
const storedAnswers = JSON.parse(rawValue);
const ageField = storedAnswers.fields?.find(
(f: any) =>
(f: Record<string, unknown>) =>
f.type === "number" ||
f.label === "Age" ||
f.label === "سن" ||
@ -26,15 +29,15 @@ export function getStoredAge(): number | null {
if (Number.isFinite(num)) return num;
}
const dobField = storedAnswers.fields?.find(
(f: any) =>
(f: Record<string, unknown>) =>
f.type === "date" ||
f.label === "Date of Birth" ||
f.label === "تاریخ تولد" ||
(typeof f.key === "string" &&
(f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))),
);
if (dobField && dobField.value) {
const dob = new Date(dobField.value);
if (dobField?.value) {
const dob = new Date(String(dobField.value));
if (!Number.isNaN(dob.getTime())) {
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
@ -45,12 +48,15 @@ export function getStoredAge(): number | null {
return age >= 0 ? age : null;
}
}
} catch (e) {}
} catch (_e) {}
return null;
}
function isFieldAnswered(q: any, field: any): boolean {
if (!field || !hasQuestionAnswerValue(field.value)) {
function isFieldAnswered(
q: Record<string, unknown>,
field: Record<string, unknown> | undefined,
): boolean {
if (!field || !hasQuestionAnswerValue(field.value as MarriageFieldValue)) {
return false;
}
if (q.type === "birthplace") {
@ -61,27 +67,80 @@ function isFieldAnswered(q: any, field: any): boolean {
return true;
}
function hashString(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash.toString(36);
}
function slugifyTitle(title: string) {
const slug = title
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return slug || `field_${hashString(title)}`;
}
export function getLocalSectionProgress(
item: QuestionListItem,
profile: any,
profile: { gender?: MarriageGender | null } | null | undefined,
age: number | null,
): number | null {
try {
if (typeof window === "undefined") return null;
const storageKey = `marriage:sections:${item.slug}:answers`;
const rawValue = window.localStorage.getItem(storageKey);
if (!rawValue) {
return null;
}
const storedValue = JSON.parse(rawValue);
if (storedValue && storedValue.completed) {
return 100;
}
const fields: Record<string, unknown>[] = [];
let hasFoundStorage = false;
const mainKey = `marriage:sections:${item.slug}:answers`;
const mainRaw = window.localStorage.getItem(mainKey);
if (mainRaw) {
try {
const parsed = JSON.parse(mainRaw);
if (
!storedValue ||
!Array.isArray(storedValue.fields) ||
storedValue.fields.length === 0
parsed &&
Array.isArray(parsed.fields) &&
parsed.fields.length > 0
) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
if (item.slug === "family_marital_history") {
const fbRaw = window.localStorage.getItem(
"marriage:sections:family_background:answers",
);
if (fbRaw) {
try {
const parsed = JSON.parse(fbRaw);
if (parsed && Array.isArray(parsed.fields)) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
const mhRaw = window.localStorage.getItem(
"marriage:sections:marital_history_children:answers",
);
if (mhRaw) {
try {
const parsed = JSON.parse(mhRaw);
if (parsed && Array.isArray(parsed.fields)) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
}
if (!hasFoundStorage || fields.length === 0) {
return null;
}
@ -90,59 +149,80 @@ export function getLocalSectionProgress(
gender: profile?.gender,
};
const visibleQuestions = item.questions
.filter((question) =>
isQuestionVisibleForProfile(question, profileContext),
)
.map((question) => ({
...question,
required: isQuestionRequiredForProfile(question, profileContext),
}));
const requiredQuestions = visibleQuestions.filter((q) => q.required);
if (requiredQuestions.length === 0) {
if (visibleQuestions.length === 0) {
return 100;
const hasDobQuestion = item.questions.some(
(q) => q.title === "Date of Birth" || q.title === "تاریخ تولد",
);
const profileVisible = item.questions.filter((question) => {
if (
hasDobQuestion &&
(question.title === "Age" || question.title === "سن")
) {
return false;
}
const answeredCount = visibleQuestions.filter((q, index) => {
const slugifyTitle = (title: string) =>
title
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
const fieldKey = `q${index + 1}_${slugifyTitle(q.title)}`;
const field = storedValue.fields.find(
(f: any) => f.key === fieldKey || f.label === q.title,
return isQuestionVisibleForProfile(question, profileContext);
});
const findAnswer = (
questionTitle: string,
questionIndex: number,
originalIndex?: number,
) => {
const indexToUse =
originalIndex !== undefined ? originalIndex : questionIndex;
const key = `q${indexToUse + 1}_${slugifyTitle(questionTitle)}`;
const field = fields.find(
(f) => f && (f.key === key || f.label === questionTitle),
);
return isFieldAnswered(q, field);
}).length;
return Math.round((answeredCount / visibleQuestions.length) * 100);
return field?.value;
};
const activeQuestions = profileVisible.filter((question, _idx) => {
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const depIndex = profileVisible.findIndex((q) => q.title === title);
const depQuestion = depIndex !== -1 ? profileVisible[depIndex] : null;
const depAnswer = depQuestion
? findAnswer(depQuestion.title, depIndex, depQuestion.originalIndex)
: undefined;
if (depAnswer === undefined || depAnswer === null) {
return false;
}
const answeredRequiredCount = requiredQuestions.filter((q) => {
const originalIndex = item.questions.findIndex(
(origQ) => origQ.title === q.title,
const ansList = Array.isArray(depAnswer)
? depAnswer.map(String)
: [String(depAnswer)];
const isMatch = values.some((val) =>
ansList.some((a) => a === val || a.includes(val) || val.includes(a)),
);
if (originalIndex === -1) return false;
const slugifyTitle = (title: string) =>
title
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
const fieldKey = `q${originalIndex + 1}_${slugifyTitle(q.title)}`;
const field = storedValue.fields.find(
(f: any) => f.key === fieldKey || f.label === q.title,
if (!isMatch) {
return false;
}
}
return true;
});
if (activeQuestions.length === 0) {
return 100;
}
const answeredCount = activeQuestions.filter((q, idx) => {
const indexToUse = q.originalIndex !== undefined ? q.originalIndex : idx;
const key = `q${indexToUse + 1}_${slugifyTitle(q.title)}`;
const field = fields.find(
(f) => f && (f.key === key || f.label === q.title),
);
return isFieldAnswered(q, field);
}).length;
return Math.round((answeredRequiredCount / requiredQuestions.length) * 100);
} catch (e) {
return Math.max(
0,
Math.min(100, Math.round((answeredCount / activeQuestions.length) * 100)),
);
} catch (_e) {
return null;
}
}

6
src/components/questions/question-answer-storage.tsx

@ -88,7 +88,11 @@ function slugifyQuestionTitle(title: string) {
}
function getQuestionFieldKey(question: QuestionField, questionIndex: number) {
return `q${questionIndex + 1}_${slugifyQuestionTitle(question.title)}`;
const index =
question.originalIndex !== undefined
? question.originalIndex
: questionIndex;
return `q${index + 1}_${slugifyQuestionTitle(question.title)}`;
}
export function getQuestionAnswersStorageKey(slug: string) {

41
src/components/questions/question-card.tsx

@ -28,6 +28,7 @@ const iconAssetMap: Partial<Record<QuestionCardIcon, string>> = {
details: "/assets/images/Grfdasfoup.svg",
checklist: "/assets/images/noun-test-4525471 1.svg",
contact: "/assets/images/solar_user-id-bold.svg",
family_marital: "/assets/images/Grfdasfoup.svg",
};
const iconMap: Record<QuestionCardIcon, IconType> = {
@ -36,6 +37,7 @@ const iconMap: Record<QuestionCardIcon, IconType> = {
details: IoDocumentText,
checklist: IoCheckbox,
contact: IoPeople,
family_marital: IoPeople,
};
export function QuestionCard({
@ -63,7 +65,7 @@ export function QuestionCard({
data-question-slug={item.slug}
className="rounded-[20px] border border-white/80 bg-white px-3 py-3 shadow-[0_12px_28px_rgba(15,23,42,0.05)] transition-transform duration-200 hover:-translate-y-0.5"
>
<div className="flex items-center gap-2.5">
<div className="flex items-start gap-2.5">
<div className="rounded-[13px] bg-linear-to-br from-[#E03950]/15 to-[#E03950]/0 p-px shadow-[0_8px_18px_rgba(240,67,99,0.18)]">
<div className="relative flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-[12px] bg-linear-to-br from-[#E03950]/15 to-[#FE6F82]/15 text-white">
{iconAsset ? (
@ -81,48 +83,37 @@ export function QuestionCard({
</div>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-baseline gap-1">
<h2 className="truncate group-14 leading-none font-bold text-[#1B1B1B]">
<h2 className="group-14 leading-tight font-bold text-[#1B1B1B] line-clamp-2">
{item.title}
</h2>
{item.required ? (
<span className="shrink-0 group-10 leading-none font-semibold text-[#FF5B73]">
({t.common.required})
</span>
) : null}
</div>
<p className="mt-1.5 group-10 leading-none font-medium text-[#7A7A7A]">
{t.common.estimateTime}: {item.estimate}
</p>
</div>
<div className="flex h-[44px] w-[44px] shrink-0 flex-col items-end justify-between">
{item.showInfoBadge ? (
<span
role="button"
tabIndex={0}
<div className="flex min-h-[44px] shrink-0 flex-col items-end justify-between self-stretch">
{item.required ? (
<span className="flex items-center justify-center rounded-full bg-[#F8D7DA] dark:bg-semantic-danger-bg px-3 py-1 text-center text-[10px] font-bold text-[#D9383A] dark:text-semantic-danger-text whitespace-nowrap">
{t.common.required}
</span>
) : item.showInfoBadge ? (
<button
type="button"
aria-label={`${item.title} details`}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onInfoClick?.(item);
}}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
onInfoClick?.(item);
}
}}
className="flex h-[17px] w-[17px] cursor-pointer items-center justify-center rounded-[6px] bg-[#747474] text-white"
className="flex h-[17px] w-[17px] cursor-pointer items-center justify-center rounded-[6px] bg-[#747474] text-white border-0 p-0 hover:bg-[#606060] transition-colors"
>
<IoInformation aria-hidden="true" className="text-[9px]" />
</span>
</button>
) : (
<span className="h-[14px] w-[14px]" aria-hidden="true" />
<span className="h-[17px] w-[17px]" aria-hidden="true" />
)}
<div className="flex items-center gap-1">
<div className="mt-auto flex items-center gap-1">
{progress === 100 ? (
<Image
src={"/assets/images/Groupfdas 2.svg"}

18
src/components/questions/question-checkbox.tsx

@ -37,14 +37,18 @@ export function QuestionCheckbox({
nextValue = [...value, option];
}
const doesnMatterOption = options.find(
(o) => o.includes("مهم نیست") || o.includes("Doesn't matter"),
const exclusiveOption = options.find(
(o) =>
o.includes("مهم نیست") ||
o.includes("Doesn't matter") ||
o.includes("No children") ||
o.includes("فرزندی ندارم."),
);
if (doesnMatterOption) {
if (option === doesnMatterOption) {
nextValue = [doesnMatterOption];
} else if (nextValue.includes(doesnMatterOption)) {
nextValue = nextValue.filter((v) => v !== doesnMatterOption);
if (exclusiveOption) {
if (option === exclusiveOption) {
nextValue = [exclusiveOption];
} else if (nextValue.includes(exclusiveOption)) {
nextValue = nextValue.filter((v) => v !== exclusiveOption);
}
}

12
src/components/questions/question-date.tsx

@ -151,7 +151,7 @@ export function QuestionDate({
onChange={(e) => handleDayChange(e.target.value)}
disabled={disabled}
aria-label="Day"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="h-[54px] w-full cursor-pointer rounded-[16px] border border-[#D0D5DD] bg-white px-3 text-[15px] font-medium text-[#181818] outline-none transition-all hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F]"
>
<option value="">Day</option>
{DAYS.map((d) => (
@ -167,7 +167,7 @@ export function QuestionDate({
onChange={(e) => handleMonthChange(e.target.value)}
disabled={disabled}
aria-label="Month"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="h-[54px] w-full cursor-pointer rounded-[16px] border border-[#D0D5DD] bg-white px-3 text-[15px] font-medium text-[#181818] outline-none transition-all hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F]"
>
<option value="">Month</option>
{MONTHS.map((m) => (
@ -183,7 +183,7 @@ export function QuestionDate({
onChange={(e) => handleYearChange(e.target.value)}
disabled={disabled}
aria-label="Year"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="h-[54px] w-full cursor-pointer rounded-[16px] border border-[#D0D5DD] bg-white px-3 text-[15px] font-medium text-[#181818] outline-none transition-all hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F]"
>
<option value="">Year</option>
{YEARS.map((y) => (
@ -205,10 +205,10 @@ export function QuestionDate({
readOnly
value={calculatedAge !== null ? calculatedAge : ""}
className={[
"h-[54px] w-full rounded-[15px] border px-4 group-12 font-medium outline-none cursor-not-allowed disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
"h-[54px] w-full rounded-[16px] border px-4.5 text-[15px] font-medium outline-none cursor-not-allowed disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
isUnder18
? "border-[#F2465F] text-[#F2465F]"
: "border-[#E7D8D5] text-[#7C7472]",
? "border-[#F2465F] text-[#F2465F] ring-1 ring-[#F2465F]"
: "border-[#D0D5DD] text-[#7C7472]",
].join(" ")}
/>
{isUnder18 ? (

580
src/components/questions/question-number.tsx

@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
@ -23,7 +23,7 @@ export default function QuestionNumber({
derivedFromQuestion,
derivedFromQuestionIndex,
}: QuestionNumberProps) {
const { dictionary: t } = useI18n();
const { dictionary: t, locale } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
@ -78,6 +78,301 @@ export default function QuestionNumber({
? rawInputValue
: "";
const isMonthlyIncome =
question.title === "Monthly Income" ||
question.title === "میزان درآمد ماهانه";
const countryName = useMemo(() => getCountryFromStorage(), []);
const [currencyCode, setCurrencyCode] = useState(() => {
if (typeof window !== "undefined") {
const stored = window.localStorage.getItem("marriage:income:currency");
if (stored) return stored;
}
return getCurrencyForCountry(countryName);
});
const [isCurrencyDropdownOpen, setIsCurrencyDropdownOpen] = useState(false);
const [currencySearchQuery, setCurrencySearchQuery] = useState("");
const currencyContainerRef = useRef<HTMLDivElement>(null);
const currencySearchInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (typeof window !== "undefined") {
const stored = window.localStorage.getItem("marriage:income:currency");
if (stored) {
setCurrencyCode(stored);
return;
}
}
const derived = getCurrencyForCountry(countryName);
setCurrencyCode(derived);
}, [countryName]);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
currencyContainerRef.current &&
!currencyContainerRef.current.contains(event.target as Node)
) {
setIsCurrencyDropdownOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
useEffect(() => {
if (isCurrencyDropdownOpen) {
setTimeout(() => {
currencySearchInputRef.current?.focus();
}, 50);
}
}, [isCurrencyDropdownOpen]);
const filteredCurrencies = useMemo(() => {
const q = currencySearchQuery.toLowerCase().trim();
if (!q) return CURRENCIES;
return CURRENCIES.filter(
(c) =>
c.code.toLowerCase().includes(q) ||
c.nameEn.toLowerCase().includes(q) ||
c.nameFa.toLowerCase().includes(q),
);
}, [currencySearchQuery]);
const placeholderCurrency = useMemo(() => {
if (currencyCode === "TOMAN") {
return locale === "fa" ? "تومان" : "TOMAN";
}
return currencyCode;
}, [currencyCode, locale]);
const dynamicPlaceholder = useMemo(() => {
if (!isMonthlyIncome) {
return question.extras.placeHolder;
}
return locale === "fa"
? `مثال: ۴۰۰۰ ${placeholderCurrency}`
: `e.g. 4000 ${placeholderCurrency}`;
}, [
isMonthlyIncome,
question.extras.placeHolder,
placeholderCurrency,
locale,
]);
const [localTextValue, setLocalTextValue] = useState(() =>
formatNumberWithCommas(inputValue),
);
useEffect(() => {
const formatted = formatNumberWithCommas(
value == null ? "" : String(value),
);
const cleanLocal = localTextValue.replace(/,/g, "");
const cleanFormatted = formatted.replace(/,/g, "");
if (cleanFormatted !== cleanLocal) {
setLocalTextValue(formatted);
}
}, [value, localTextValue]);
if (isMonthlyIncome) {
return (
<div
className={[
"flex w-full flex-col gap-2 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<div className="flex gap-3 w-full relative" ref={currencyContainerRef}>
<div className="flex-1 min-w-0">
<input
type="text"
inputMode="decimal"
required={question.required && !disabled}
disabled={disabled || Boolean(derivedFromQuestion)}
placeholder={dynamicPlaceholder}
value={localTextValue}
onChange={(event) => {
const nextValue = event.target.value;
const cleanValue = nextValue.replace(/,/g, "");
if (
cleanValue !== "" &&
!NUMBER_INPUT_PATTERN.test(cleanValue)
) {
return;
}
const formatted = formatNumberWithCommas(cleanValue);
const finalFormatted = nextValue.endsWith(".")
? `${formatted}.`
: formatted;
setLocalTextValue(finalFormatted);
if (cleanValue === "") {
setAnswerValue(question, questionIndex, null);
} else {
const parsed = parseFloat(cleanValue);
setAnswerValue(
question,
questionIndex,
Number.isNaN(parsed) ? cleanValue : parsed,
);
}
}}
className={[
"h-[54px] w-full rounded-[16px] border px-4.5 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
isOutOfRange
? "border-[#F2465F] ring-1 ring-[#F2465F]"
: "border-[#D0D5DD] hover:border-[#98A2B3] bg-white",
].join(" ")}
/>
</div>
<div className="w-[110px] shrink-0 relative">
<button
type="button"
disabled={disabled}
onClick={() => setIsCurrencyDropdownOpen(!isCurrencyDropdownOpen)}
className={[
"flex h-[54px] w-full items-center justify-between rounded-[16px] border bg-white px-3.5 text-start transition-all cursor-pointer outline-none",
isCurrencyDropdownOpen
? "border-[#6F6F6F] ring-1 ring-[#6F6F6F]"
: "border-[#D0D5DD] hover:border-[#98A2B3]",
].join(" ")}
>
<span className="text-[15px] font-medium text-[#181818] truncate">
{placeholderCurrency}
</span>
<svg
width="12"
height="8"
viewBox="0 0 16 10"
fill="none"
role="img"
aria-label="Dropdown chevron"
className={[
"shrink-0 transition-transform duration-200 text-[#344054] ml-1",
isCurrencyDropdownOpen ? "rotate-180" : "",
].join(" ")}
>
<path
d="M14.75 1.25L7.75 8.25L0.75 1.25"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{isCurrencyDropdownOpen && (
<div className="absolute top-[calc(100%+8px)] right-0 z-50 flex w-[240px] flex-col gap-3 rounded-[20px] bg-white p-4 shadow-[0_12px_36px_rgba(0,0,0,0.09)] border border-[#EAECF0] animate-in fade-in zoom-in-95 duration-150">
{/* Search bar */}
<div className="flex h-[40px] w-full items-center gap-2 rounded-[10px] bg-[#EFEFEF] px-3 transition-colors focus-within:bg-[#E8E8E8]">
<svg
width="16"
height="16"
viewBox="0 0 18 18"
fill="none"
role="img"
aria-label="Search"
className="shrink-0 text-[#667085]"
>
<path
d="M8.25 14.25C11.5637 14.25 14.25 11.5637 14.25 8.25C14.25 4.93629 11.5637 2.25 8.25 2.25C4.93629 2.25 2.25 4.93629 2.25 8.25C2.25 11.5637 4.93629 14.25 8.25 14.25Z"
stroke="#667085"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.75 15.75L12.5 12.5"
stroke="#667085"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<input
ref={currencySearchInputRef}
type="text"
value={currencySearchQuery}
onChange={(e) => setCurrencySearchQuery(e.target.value)}
placeholder={locale === "fa" ? "جستجو..." : "Search..."}
className="flex-1 min-w-0 bg-transparent text-[13px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
/>
{currencySearchQuery ? (
<button
type="button"
onClick={() => setCurrencySearchQuery("")}
className="text-[#667085] hover:text-[#181818] text-xs font-semibold cursor-pointer shrink-0"
>
</button>
) : null}
</div>
{/* Options list */}
<div className="flex max-h-[180px] flex-col gap-2.5 overflow-y-auto overscroll-contain pr-1">
{filteredCurrencies.length > 0 ? (
filteredCurrencies.map((c) => {
const isSelected = c.code === currencyCode;
return (
<button
key={c.code}
type="button"
onClick={() => {
setCurrencyCode(c.code);
if (typeof window !== "undefined") {
window.localStorage.setItem(
"marriage:income:currency",
c.code,
);
}
setIsCurrencyDropdownOpen(false);
setCurrencySearchQuery("");
}}
className="flex w-full items-center justify-between text-start cursor-pointer group/opt py-1 px-1.5 rounded-lg hover:bg-gray-50 transition-colors"
>
<span className="text-[14px] font-medium text-[#181818]">
{c.code}{" "}
{locale === "fa"
? `(${c.nameFa})`
: `(${c.nameEn})`}
</span>
{isSelected ? (
<div className="size-[6px] rounded-full bg-[#F2465F]" />
) : null}
</button>
);
})
) : (
<span className="py-2 text-[12px] text-[#667085] text-center">
{locale === "fa"
? "ارزی یافت نشد"
: "No currencies found"}
</span>
)}
</div>
</div>
)}
</div>
</div>
{isOutOfRange ? (
<span className="block text-[10px] font-semibold text-[#F2465F]">
{t.common.rangeError ||
`Please enter a value between ${min} and ${max}`}
</span>
) : null}
</div>
);
}
return (
<div
className={[
@ -113,8 +408,10 @@ export default function QuestionNumber({
}
}}
className={[
"h-[54px] w-full rounded-[15px] border px-4 text-[15px] text-[#181818] outline-none placeholder:text-[#9D8F8C] focus:border-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
isOutOfRange ? "border-[#F2465F]" : "border-[#E7D8D5] bg-white",
"h-[54px] w-full rounded-[16px] border px-4.5 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
isOutOfRange
? "border-[#F2465F] ring-1 ring-[#F2465F]"
: "border-[#D0D5DD] hover:border-[#98A2B3] bg-white",
].join(" ")}
/>
{isOutOfRange ? (
@ -147,3 +444,278 @@ function calculateAge(dateOfBirth: string) {
return String(Math.max(age, 0));
}
function getCountryFromStorage(): string {
if (typeof window === "undefined") return "";
try {
const rawValue = window.localStorage.getItem(
"marriage:sections:contact_residence_family_communication:answers",
);
if (!rawValue) return "";
const storedValue = JSON.parse(rawValue);
const field = storedValue.fields?.find(
(f: { type?: string; key?: string; value?: unknown }) =>
f.type === "birthplace" ||
f.key?.includes("current_residence") ||
f.key?.includes("mhl_skwnt_fly"),
);
const value = field?.value;
if (typeof value === "string") {
const parts = value.split(",");
return parts[0]?.trim() || "";
}
} catch {
// Ignore
}
return "";
}
function getCurrencyForCountry(countryName: string): string {
const cleanCountry = countryName?.trim();
if (!cleanCountry) return "USD";
const countryMap: Record<string, string> = {
// English
Iran: "TOMAN",
"United States": "USD",
"United Kingdom": "GBP",
Canada: "CAD",
Germany: "EUR",
France: "EUR",
"United Arab Emirates": "AED",
Turkey: "TRY",
Iraq: "IQD",
Afghanistan: "AFN",
Pakistan: "PKR",
"Saudi Arabia": "SAR",
Qatar: "QAR",
Sweden: "SEK",
Netherlands: "EUR",
Norway: "NOK",
Australia: "AUD",
Bulgaria: "BGN",
// Persian
ایران: "TOMAN",
"ایالات متحده": "USD",
بریتانیا: "GBP",
کانادا: "CAD",
آلمان: "EUR",
فرانسه: "EUR",
"امارات متحده عربی": "AED",
ترکیه: "TRY",
عراق: "IQD",
افغانستان: "AFN",
پاکستان: "PKR",
"عربستان سعودی": "SAR",
قطر: "QAR",
سوئد: "SEK",
هلند: "EUR",
نروژ: "NOK",
استرالیا: "AUD",
بلغارستان: "BGN",
};
return countryMap[cleanCountry] || "USD";
}
const CURRENCIES = [
{ code: "AED", nameEn: "UAE Dirham", nameFa: "درهم امارات" },
{ code: "AFN", nameEn: "Afghan Afghani", nameFa: "افغانی افغانستان" },
{ code: "ALL", nameEn: "Albanian Lek", nameFa: "لک آلبانی" },
{ code: "AMD", nameEn: "Armenian Dram", nameFa: "درام ارمنستان" },
{
code: "ANG",
nameEn: "Netherlands Antillean Guilder",
nameFa: "گیلدر آنتیل هلند",
},
{ code: "AOA", nameEn: "Angolan Kwanza", nameFa: "کوانزای آنگولا" },
{ code: "ARS", nameEn: "Argentine Peso", nameFa: "پزو آرژانتین" },
{ code: "AUD", nameEn: "Australian Dollar", nameFa: "دلار استرالیا" },
{ code: "AZN", nameEn: "Azerbaijani Manat", nameFa: "منات آذربایجان" },
{
code: "BAM",
nameEn: "Bosnia-Herzegovina Mark",
nameFa: "مارک بوسنی و هرزگوین",
},
{ code: "BBD", nameEn: "Barbadian Dollar", nameFa: "دلار باربادوس" },
{ code: "BDT", nameEn: "Bangladeshi Taka", nameFa: "تاکای بنگلادش" },
{ code: "BGN", nameEn: "Bulgarian Lev", nameFa: "لو بلغارستان" },
{ code: "BHD", nameEn: "Bahraini Dinar", nameFa: "دینار بحرین" },
{ code: "BIF", nameEn: "Burundian Franc", nameFa: "فرانک بروندی" },
{ code: "BMD", nameEn: "Bermudian Dollar", nameFa: "دلار برمودا" },
{ code: "BND", nameEn: "Brunei Dollar", nameFa: "دلار برونئی" },
{ code: "BOB", nameEn: "Bolivian Boliviano", nameFa: "بولیویانو بولیوی" },
{ code: "BRL", nameEn: "Brazilian Real", nameFa: "رئال برزیل" },
{ code: "BSD", nameEn: "Bahamian Dollar", nameFa: "دلار باهاما" },
{ code: "BTN", nameEn: "Bhutanese Ngultrum", nameFa: "نگولتروم بوتان" },
{ code: "BWP", nameEn: "Botswanan Pula", nameFa: "پولای بوتسوانا" },
{ code: "BYN", nameEn: "Belarusian Ruble", nameFa: "روبل بلاروس" },
{ code: "BZD", nameEn: "Belize Dollar", nameFa: "دلار بلیز" },
{ code: "CAD", nameEn: "Canadian Dollar", nameFa: "دلار کانادا" },
{ code: "CDF", nameEn: "Congolese Franc", nameFa: "فرانک کنگو" },
{ code: "CHF", nameEn: "Swiss Franc", nameFa: "فرانک سوئیس" },
{ code: "CLP", nameEn: "Chilean Peso", nameFa: "پزو شیلی" },
{ code: "CNY", nameEn: "Chinese Yuan", nameFa: "یوان چین" },
{ code: "COP", nameEn: "Colombian Peso", nameFa: "پزو کلمبیا" },
{ code: "CRC", nameEn: "Costa Rican Colón", nameFa: "کولون کاستاریکا" },
{ code: "CUP", nameEn: "Cuban Peso", nameFa: "پزو کوبا" },
{ code: "CVE", nameEn: "Cape Verdean Escudo", nameFa: "اسکودو کیپ ورد" },
{ code: "CZK", nameEn: "Czech Koruna", nameFa: "کرون چک" },
{ code: "DJF", nameEn: "Djiboutian Franc", nameFa: "فرانک جیبوتی" },
{ code: "DKK", nameEn: "Danish Krone", nameFa: "کرون دانمارک" },
{ code: "DOP", nameEn: "Dominican Peso", nameFa: "پزو دومینیکن" },
{ code: "DZD", nameEn: "Algerian Dinar", nameFa: "دینار الجزایر" },
{ code: "EGP", nameEn: "Egyptian Pound", nameFa: "پوند مصر" },
{ code: "ERN", nameEn: "Eritrean Nakfa", nameFa: "ناکفای اریتره" },
{ code: "ETB", nameEn: "Ethiopian Birr", nameFa: "بیر اتیوپی" },
{ code: "EUR", nameEn: "Euro", nameFa: "یورو" },
{ code: "FJD", nameEn: "Fijian Dollar", nameFa: "دلار فیجی" },
{
code: "FKP",
nameEn: "Falkland Islands Pound",
nameFa: "پوند جزایر فالکلند",
},
{ code: "GBP", nameEn: "British Pound", nameFa: "پوند بریتانیا" },
{ code: "GEL", nameEn: "Georgian Lari", nameFa: "لاری گرجستان" },
{ code: "GHS", nameEn: "Ghanaian Cedi", nameFa: "سدی غنا" },
{ code: "GIP", nameEn: "Gibraltar Pound", nameFa: "پوند جبل الطارق" },
{ code: "GMD", nameEn: "Gambian Dalasi", nameFa: "دالاسی گامبیا" },
{ code: "GNF", nameEn: "Guinean Franc", nameFa: "فرانک گینه" },
{ code: "GTQ", nameEn: "Guatemalan Quetzal", nameFa: "کوتزال گواتمالا" },
{ code: "GYD", nameEn: "Guyanese Dollar", nameFa: "دلار گویان" },
{ code: "HKD", nameEn: "Hong Kong Dollar", nameFa: "دلار هنگ کنگ" },
{ code: "HNL", nameEn: "Honduran Lempira", nameFa: "لمپیرای هندوراس" },
{ code: "HRK", nameEn: "Croatian Kuna", nameFa: "کونای کرواسی" },
{ code: "HTG", nameEn: "Haitian Gourde", nameFa: "گورد هائیتی" },
{ code: "HUF", nameEn: "Hungarian Forint", nameFa: "فورینت مجارستان" },
{ code: "IDR", nameEn: "Indonesian Rupiah", nameFa: "روپیه اندونزی" },
{ code: "ILS", nameEn: "Israeli Shekel", nameFa: "شکل اسرائیل" },
{ code: "INR", nameEn: "Indian Rupee", nameFa: "روپیه هند" },
{ code: "IQD", nameEn: "Iraqi Dinar", nameFa: "دینار عراق" },
{ code: "IRR", nameEn: "Iranian Rial", nameFa: "ریال ایران" },
{ code: "ISK", nameEn: "Icelandic Króna", nameFa: "کرون ایسلند" },
{ code: "JMD", nameEn: "Jamaican Dollar", nameFa: "دلار جامائیکا" },
{ code: "JOD", nameEn: "Jordanian Dinar", nameFa: "دینار اردن" },
{ code: "JPY", nameEn: "Japanese Yen", nameFa: "ین ژاپن" },
{ code: "KES", nameEn: "Kenyan Shilling", nameFa: "شیلینگ کنیا" },
{ code: "KGS", nameEn: "Kyrgystani Som", nameFa: "سوم قرقیزستان" },
{ code: "KHR", nameEn: "Cambodian Riel", nameFa: "ریال کامبوج" },
{ code: "KMF", nameEn: "Comorian Franc", nameFa: "فرانک کومور" },
{ code: "KPW", nameEn: "North Korean Won", nameFa: "وون کره شمالی" },
{ code: "KRW", nameEn: "South Korean Won", nameFa: "وون کره جنوبی" },
{ code: "KWD", nameEn: "Kuwaiti Dinar", nameFa: "دینار کویت" },
{ code: "KYD", nameEn: "Cayman Islands Dollar", nameFa: "دلار جزایر کیمن" },
{ code: "KZT", nameEn: "Kazakhstani Tenge", nameFa: "تنگه قزاقستان" },
{ code: "LAK", nameEn: "Laotian Kip", nameFa: "کیپ لائوس" },
{ code: "LBP", nameEn: "Lebanese Pound", nameFa: "پوند لبنان" },
{ code: "LKR", nameEn: "Sri Lankan Rupee", nameFa: "روپیه سریلانکا" },
{ code: "LRD", nameEn: "Liberian Dollar", nameFa: "دلار لیبریا" },
{ code: "LSL", nameEn: "Lesotho Loti", nameFa: "لوتی لسوتو" },
{ code: "LYD", nameEn: "Libyan Dinar", nameFa: "دینار لیبی" },
{ code: "MAD", nameEn: "Moroccan Dirham", nameFa: "درهم مراکش" },
{ code: "MDL", nameEn: "Moldovan Leu", nameFa: "لوی مولداوی" },
{ code: "MGA", nameEn: "Malagasy Ariary", nameFa: "آریاری ماداگاسکار" },
{ code: "MKD", nameEn: "Macedonian Denar", nameFa: "دینار مقدونیه" },
{ code: "MMK", nameEn: "Myanmar Kyat", nameFa: "کیات میانمار" },
{ code: "MNT", nameEn: "Mongolian Tugrik", nameFa: "توگریک مغولستان" },
{ code: "MOP", nameEn: "Macanese Pataca", nameFa: "پاتاکای ماکائو" },
{ code: "MRU", nameEn: "Mauritanian Ouguiya", nameFa: "اوگیای موریتانی" },
{ code: "MUR", nameEn: "Mauritian Rupee", nameFa: "روپیه موریس" },
{ code: "MVR", nameEn: "Maldivian Rufiyaa", nameFa: "روفیای مالدیو" },
{ code: "MWK", nameEn: "Malawian Kwacha", nameFa: "کواچای مالاوی" },
{ code: "MXN", nameEn: "Mexican Peso", nameFa: "پزو مکزیک" },
{ code: "MYR", nameEn: "Malaysian Ringgit", nameFa: "رینگیت مالزی" },
{ code: "MZN", nameEn: "Mozambican Metical", nameFa: "متیکال موزامبیک" },
{ code: "NAD", nameEn: "Namibian Dollar", nameFa: "دلار نامیبیا" },
{ code: "NGN", nameEn: "Nigerian Naira", nameFa: "نایرای نیجریه" },
{ code: "NIO", nameEn: "Nicaraguan Córdoba", nameFa: "کوردوبای نیکاراگوئه" },
{ code: "NOK", nameEn: "Norwegian Krone", nameFa: "کرون نروژ" },
{ code: "NPR", nameEn: "Nepalese Rupee", nameFa: "روپیه نپال" },
{ code: "NZD", nameEn: "New Zealand Dollar", nameFa: "دلار نیوزیلند" },
{ code: "OMR", nameEn: "Omani Rial", nameFa: "ریال عمان" },
{ code: "PAB", nameEn: "Panamanian Balboa", nameFa: "بالبوای پاناما" },
{ code: "PEN", nameEn: "Peruvian Sol", nameFa: "سول پرو" },
{
code: "PGK",
nameEn: "Papua New Guinean Kina",
nameFa: "کینای پاپوآ گینه نو",
},
{ code: "PHP", nameEn: "Philippine Peso", nameFa: "پزو فیلیپین" },
{ code: "PKR", nameEn: "Pakistani Rupee", nameFa: "روپیه پاکستان" },
{ code: "PLN", nameEn: "Polish Zloty", nameFa: "زلوتی لهستان" },
{ code: "PYG", nameEn: "Paraguayan Guarani", nameFa: "گوارانی پاراگوئه" },
{ code: "QAR", nameEn: "Qatari Rial", nameFa: "ریال قطر" },
{ code: "RON", nameEn: "Romanian Leu", nameFa: "لوی رومانی" },
{ code: "RSD", nameEn: "Serbian Dinar", nameFa: "دینار صربستان" },
{ code: "RUB", nameEn: "Russian Ruble", nameFa: "روبل روسیه" },
{ code: "RWF", nameEn: "Rwandan Franc", nameFa: "فرانک رواندا" },
{ code: "SAR", nameEn: "Saudi Riyal", nameFa: "ریال عربستان" },
{
code: "SBD",
nameEn: "Solomon Islands Dollar",
nameFa: "دلار جزایر سلیمان",
},
{ code: "SCR", nameEn: "Seychellois Rupee", nameFa: "روپیه سیشل" },
{ code: "SDG", nameEn: "Sudanese Pound", nameFa: "پوند سودان" },
{ code: "SEK", nameEn: "Swedish Krona", nameFa: "کرون سوئد" },
{ code: "SGD", nameEn: "Singapore Dollar", nameFa: "دلار سنگاپور" },
{ code: "SHP", nameEn: "St. Helena Pound", nameFa: "پوند سنت هلن" },
{ code: "SLL", nameEn: "Sierra Leonean Leone", nameFa: "لئون سیرالئون" },
{ code: "SOS", nameEn: "Somali Shilling", nameFa: "شیلینگ سومالی" },
{ code: "SRD", nameEn: "Surinamese Dollar", nameFa: "دلار سورینام" },
{ code: "SSP", nameEn: "South Sudanese Pound", nameFa: "پوند سودان جنوبی" },
{ code: "STN", nameEn: "São Tomé Dobra", nameFa: "دوبرا سائوتومه" },
{ code: "SVC", nameEn: "Salvadoran Colón", nameFa: "کولون السالوادور" },
{ code: "SYP", nameEn: "Syrian Pound", nameFa: "پوند سوریه" },
{ code: "SZL", nameEn: "Swazi Lilangeni", nameFa: "لیلانگنی سوازیلند" },
{ code: "THB", nameEn: "Thai Baht", nameFa: "بات تایلند" },
{ code: "TJS", nameEn: "Tajikistani Somoni", nameFa: "سامانی تاجیکستان" },
{ code: "TMT", nameEn: "Turkmenistani Manat", nameFa: "منات ترکمنستان" },
{ code: "TND", nameEn: "Tunisian Dinar", nameFa: "دینار تونس" },
{ code: "TOMAN", nameEn: "Iranian Toman", nameFa: "تومان ایران" },
{ code: "TOP", nameEn: "Tongan Paʻanga", nameFa: "پاآنگای تونگا" },
{ code: "TRY", nameEn: "Turkish Lira", nameFa: "لیر ترکیه" },
{
code: "TTD",
nameEn: "Trinidad & Tobago Dollar",
nameFa: "دلار ترینیداد و توباگر",
},
{ code: "TWD", nameEn: "New Taiwan Dollar", nameFa: "دلار جدید تایوان" },
{ code: "TZS", nameEn: "Tanzanian Shilling", nameFa: "شیلینگ تانزانیا" },
{ code: "UAH", nameEn: "Ukrainian Hryvnia", nameFa: "گریونا اوکراین" },
{ code: "UGX", nameEn: "Ugandan Shilling", nameFa: "شیلینگ اوگاندا" },
{ code: "USD", nameEn: "US Dollar", nameFa: "دلار آمریکا" },
{ code: "UYU", nameEn: "Uruguayan Peso", nameFa: "پزو اروگوئه" },
{ code: "UZS", nameEn: "Uzbekistani Som", nameFa: "سوم ازبکستان" },
{ code: "VES", nameEn: "Venezuelan Bolívar", nameFa: "بولیوار ونزوئلا" },
{ code: "VND", nameEn: "Vietnamese Dong", nameFa: "دانگ ویتنام" },
{ code: "VUV", nameEn: "Vanuatu Vatu", nameFa: "واتو وانواتو" },
{ code: "WST", nameEn: "Samoan Tālā", nameFa: "تالای ساموآ" },
{
code: "XAF",
nameEn: "Central African CFA Franc",
nameFa: "فرانک سی‌اف‌آی آفریقای مرکزی",
},
{ code: "XCD", nameEn: "East Caribbean Dollar", nameFa: "دلار کارائیب شرقی" },
{
code: "XOF",
nameEn: "West African CFA Franc",
nameFa: "فرانک سی‌اف‌آی آفریقای غربی",
},
{ code: "XPF", nameEn: "CFP Franc", nameFa: "فرانک اقیانوس آرام" },
{ code: "YER", nameEn: "Yemeni Rial", nameFa: "ریال یمن" },
{ code: "ZAR", nameEn: "South African Rand", nameFa: "راند آفریقای جنوبی" },
{ code: "ZMW", nameEn: "Zambian Kwacha", nameFa: "کواچای زامبیا" },
{ code: "ZWL", nameEn: "Zimbabwean Dollar", nameFa: "دلار زیمبابوه" },
];
function formatNumberWithCommas(
val: string | number | null | undefined,
): string {
if (val === null || val === undefined || val === "") return "";
const cleanStr = String(val).replace(/,/g, "");
const parts = cleanStr.split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}

10
src/components/questions/question-phone.tsx

@ -456,8 +456,10 @@ export function QuestionPhone({
<div
dir="ltr"
className={[
"flex h-[54px] w-full items-center rounded-[15px] border bg-white text-[#181818] focus-within:border-[#6F6F6F]",
showInvalidState ? "border-[#F2465F]" : "border-[#E7D8D5]",
"flex h-[54px] w-full items-center rounded-[16px] border bg-white text-[#181818] focus-within:border-[#6F6F6F] focus-within:ring-1 focus-within:ring-[#6F6F6F] transition-all",
showInvalidState
? "border-[#F2465F] ring-1 ring-[#F2465F]"
: "border-[#D0D5DD] hover:border-[#98A2B3]",
].join(" ")}
>
<div className="flex shrink-0 items-center pl-2.5 pr-2">
@ -465,7 +467,7 @@ export function QuestionPhone({
type="button"
disabled={disabled}
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1.5 h-full px-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer outline-none border-0 group-12 leading-none text-[#181818] tabular-nums"
className="flex items-center gap-1.5 h-full px-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer outline-none border-0 text-[15px] font-medium leading-none text-[#181818] tabular-nums"
>
<span>{activeFlag}</span>
<span>{codeValue || defaultCodeValue}</span>
@ -510,7 +512,7 @@ export function QuestionPhone({
updateStoredValue(codeValue, truncatedPhone);
}}
dir="ltr"
className="h-full w-full border-0 bg-transparent p-0 text-left group-12 leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
className="h-full w-full border-0 bg-transparent p-0 text-left text-[15px] font-medium leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#98A2B3]"
/>
</span>
</div>

6
src/components/questions/question-snap-list.tsx

@ -102,6 +102,12 @@ export function QuestionSnapList({
const hasInitializedActiveIndexRef = useRef(false);
useEffect(() => {
if (questions.length > 0 && activeIndex >= questions.length) {
setActiveIndex(questions.length - 1);
}
}, [questions.length, activeIndex]);
useEffect(() => {
if (hasInitializedActiveIndexRef.current || questions.length === 0) {
return;

10
src/components/questions/question-text.tsx

@ -50,8 +50,10 @@ export default function QuestionText({
<div
dir="ltr"
className={[
"flex h-[54px] w-full items-center rounded-[15px] border bg-white text-[#181818] focus-within:border-[#6F6F6F]",
showInvalidState ? "border-[#F2465F]" : "border-[#E7D8D5]",
"flex h-[54px] w-full items-center rounded-[16px] border bg-white text-[#181818] focus-within:border-[#6F6F6F] focus-within:ring-1 focus-within:ring-[#6F6F6F] transition-all",
showInvalidState
? "border-[#F2465F] ring-1 ring-[#F2465F]"
: "border-[#D0D5DD] hover:border-[#98A2B3]",
].join(" ")}
>
<input
@ -62,7 +64,7 @@ export default function QuestionText({
}
placeholder={question.extras.placeHolder}
disabled={disabled}
className="h-full w-full border-0 bg-transparent px-[18px] group-12 leading-none text-[#181818] outline-none placeholder:text-[#9D8F8C]"
className="h-full w-full border-0 bg-transparent px-4.5 text-[15px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
/>
</div>
{showInvalidState ? (
@ -99,7 +101,7 @@ export default function QuestionText({
}
placeholder={question.extras.placeHolder}
disabled={disabled}
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 group-12 font-medium text-[#181818] outline-none transition-all placeholder:text-[#9D8F8C] focus:border-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
className="h-[54px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
/>
{description ? (
<span className="block group-10 font-semibold text-[#747474]">

25
src/components/questions/question-title.tsx

@ -12,9 +12,13 @@ type QuestionTitleProps = {
};
export function QuestionTitle({ question, className }: QuestionTitleProps) {
const { dictionary: t } = useI18n();
const { dictionary: t, locale } = useI18n();
const [isHelpOpen, setIsHelpOpen] = useState(false);
const isMonthlyIncome =
question.title === "Monthly Income" ||
question.title === "میزان درآمد ماهانه";
return (
<span className="block w-full">
{question.private ? (
@ -39,15 +43,14 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
>
<span className="inline">
{question.title}
{isMonthlyIncome
? locale === "fa"
? " (تقریبی)"
: " (approximately)"
: ""}
{question.required ? (
<span className="text-[#F2465F] font-bold ml-1 inline">*</span>
) : null}
</span>
{question.description ? (
<span className="text-[13.5px] font-normal text-[#667085]">
({question.description})
</span>
) : null}
{question.tooltip ? (
<>
<button
@ -57,7 +60,7 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
e.stopPropagation();
setIsHelpOpen(true);
}}
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#F2465F] text-[10px] font-serif italic text-white shadow-xs cursor-pointer hover:scale-110 active:scale-95 transition-transform"
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#F2465F] text-[10px] font-serif italic text-white shadow-xs cursor-pointer hover:scale-110 active:scale-95 transition-transform ml-1.5 align-middle"
aria-label="Help"
>
i
@ -70,6 +73,12 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
</>
) : null}
</span>
{question.description ? (
<span className="text-[13.5px] font-normal text-[#667085]">
({question.description})
</span>
) : null}
</span>
</span>
);
}

27
src/components/questions/required-steps-card.tsx

@ -76,7 +76,32 @@ export default function RequiredStepsCard({
progressBySlug &&
typeof progressBySlug.get(item.slug) === "number"
) {
progress = progressBySlug.get(item.slug)!;
progress = progressBySlug.get(item.slug) ?? 0;
} else if (item.slug === "family_marital_history" && sections) {
const fbSec = sections.find((s) => s.slug === "family_background");
const mhSec = sections.find((s) => s.slug === "marital_history");
if (fbSec || mhSec) {
const fbTotal = fbSec?.total_steps ?? 6;
const fbCurrent = fbSec
? Math.round((fbSec.completion_percent / 100) * fbTotal)
: 0;
const mhTotal = mhSec?.total_steps ?? 6;
const mhCurrent = mhSec
? Math.round((mhSec.completion_percent / 100) * mhTotal)
: 0;
progress =
fbTotal + mhTotal > 0
? Math.max(
0,
Math.min(
100,
Math.round(
((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100,
),
),
)
: 0;
}
} else if (section) {
progress = Math.max(
0,

61
src/components/ui/help-modal.tsx

@ -19,7 +19,7 @@ export type HelpModalProps = {
export function HelpModal({
isOpen,
onClose,
title,
title: _title,
description,
buttonText,
}: HelpModalProps) {
@ -36,9 +36,12 @@ export function HelpModal({
const resolvedTitle = "Tips";
const resolvedDescription =
description ??
(t.common as any)?.helpDescription ??
(t.common as Record<string, string | undefined>)?.helpDescription ??
"Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance.";
const resolvedButtonText = buttonText ?? (t.common as any)?.gotIt ?? "Got it";
const resolvedButtonText =
buttonText ??
(t.common as Record<string, string | undefined>)?.gotIt ??
"Got it";
const closeSheet = useCallback(() => {
if (isClosing) return;
@ -125,6 +128,21 @@ export function HelpModal({
</h2>
{(() => {
const parseBoldText = (text: string) => {
const parts = text.split(/(\*\*[^*]+\*\*)/g);
return parts.map((part, index) => {
if (part.startsWith("**") && part.endsWith("**")) {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: parts array is static
<strong key={index} className="font-bold text-[#171717]">
{part.slice(2, -2)}
</strong>
);
}
return part;
});
};
const items = Array.isArray(resolvedDescription)
? resolvedDescription
: typeof resolvedDescription === "string" &&
@ -138,19 +156,48 @@ export function HelpModal({
if (items && Array.isArray(items)) {
return (
<ul className="mt-3.5 w-full text-[12px] leading-[1.6] text-[#4D4D4D] text-start space-y-2">
{items.map((item, i) => (
{items.map((item, i) => {
let cleanItem = item.trim();
let isHeader = false;
if (cleanItem.startsWith("###")) {
cleanItem = cleanItem.replace(/^###\s*/, "");
isHeader = true;
} else {
cleanItem = cleanItem.replace(/^[*+-]\s*/, "");
}
if (isHeader) {
return (
<li
// biome-ignore lint/suspicious/noArrayIndexKey: list is static
key={i}
className="block w-full font-bold text-[#171717] mt-3 first:mt-0 text-[13px]"
>
{parseBoldText(cleanItem)}
</li>
);
}
return (
// biome-ignore lint/suspicious/noArrayIndexKey: list is static
<li key={i} className="flex items-start gap-2.5 w-full">
<span className="shrink-0 mt-1.5 size-1.5 rounded-full bg-[#4D4D4D]" />
<span className="flex-1 min-w-0">{item}</span>
<span className="flex-1 min-w-0">
{parseBoldText(cleanItem)}
</span>
</li>
))}
);
})}
</ul>
);
}
return (
<div className="mt-3.5 w-full text-[12px] leading-[1.6] text-[#4D4D4D] text-start">
{resolvedDescription}
{typeof resolvedDescription === "string"
? parseBoldText(resolvedDescription)
: resolvedDescription}
</div>
);
})()}

59
src/data/question-data.ts

@ -2,6 +2,7 @@ import enQuestions from "@/data/questions/en.json";
import faQuestions from "@/data/questions/fa.json";
import type { MarriageGender } from "@/hooks/marriage/types";
import { defaultLocale, type Locale } from "@/translations/config";
import { getDictionary } from "@/translations/dictionaries";
export const bookingTerms = [
"You will be contacted by your consultant.",
@ -14,7 +15,8 @@ export type QuestionCardIcon =
| "education"
| "details"
| "checklist"
| "contact";
| "contact"
| "family_marital";
type QuestionExtras = {
placeHolder: string;
@ -48,6 +50,8 @@ export type QuestionField = {
requiredWhen?: QuestionAudienceRule;
logic?: QuestionLogic;
showGuardianNotice?: boolean;
originalSlug?: string;
originalIndex?: number;
};
export type QuestionListItem = {
@ -109,12 +113,61 @@ function mapQuestionListItem(item: RawQuestionListItem): QuestionListItem {
}
export function getQuestionListItems(locale: Locale = defaultLocale) {
const items =
const rawItems =
questionsByLocale[locale] ??
questionsByLocale[defaultLocale] ??
questionsByLocale.en ??
[];
return items.map(mapQuestionListItem);
const items = rawItems.map(mapQuestionListItem);
const fbIndex = items.findIndex((item) => item.slug === "family_background");
const mhIndex = items.findIndex(
(item) => item.slug === "marital_history_children",
);
if (fbIndex !== -1 && mhIndex !== -1) {
const fbItem = items[fbIndex];
const mhItem = items[mhIndex];
const dict = getDictionary(locale);
const mergedTitle =
dict.questions.familyMaritalTitle ||
"Family Background, Marital Status, and Children";
const mergedEstimate = dict.questions.familyMaritalEstimate || "20 minutes";
const fbQuestions = fbItem.questions.map((q, idx) => ({
...q,
originalSlug: "family_background",
originalIndex: idx,
}));
const mhQuestions = mhItem.questions.map((q, idx) => ({
...q,
originalSlug: "marital_history_children",
originalIndex: idx,
}));
const mergedItem: QuestionListItem = {
slug: "family_marital_history",
title: mergedTitle,
estimate: mergedEstimate,
progress: 0,
icon: "family_marital",
required: fbItem.required || mhItem.required,
showInfoBadge: fbItem.showInfoBadge || mhItem.showInfoBadge,
summary: `${fbItem.summary}\n\n${mhItem.summary}`,
checkpoints: [...fbItem.checkpoints, ...mhItem.checkpoints],
tooltip: fbItem.tooltip || mhItem.tooltip,
questions: [...fbQuestions, ...mhQuestions],
};
const newItems = [...items];
newItems[fbIndex] = mergedItem;
newItems.splice(mhIndex, 1);
return newItems;
}
return items;
}
export function getQuestionListItemBySlug(

74
src/data/questions/en.json

@ -613,7 +613,8 @@
"range": [0, 0],
"options": []
},
"private": true
"private": true,
"tooltip": "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly."
}
]
},
@ -661,25 +662,35 @@
"options": [
"Living together",
"Separated / Divorced",
"One parent remarried",
"Special family circumstances (explained in comments)"
"I have special family circumstances and will provide the details in the description."
]
},
"private": true
},
{
"title": "Short Family Description",
"type": "text",
"required": false,
"extras": {
"placeHolder": "Enter details here...",
"range": [0, 0],
"options": []
}
},
{
"title": "Family's Religious and Ideological Atmosphere",
"type": "radio",
"required": false,
"description": "Please select the option that best describes the general atmosphere and lifestyle of your family.",
"tooltip": "### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.",
"extras": {
"placeHolder": "Select one option",
"range": [0, 0],
"options": [
"Religious and strictly observant",
"Religious (committed to duties)",
"Religious (observant of obligations)",
"Traditional (respectful of religious values)",
"Non-religious / Customary"
"Non-religious / Secular"
]
}
},
@ -692,16 +703,6 @@
"range": [0, 0],
"options": ["Weak", "Average", "Good", "Prosperous"]
}
},
{
"title": "Short Family Description",
"type": "text",
"required": false,
"extras": {
"placeHolder": "Enter details here...",
"range": [0, 0],
"options": []
}
}
]
},
@ -773,7 +774,7 @@
},
{
"title": "Children and Guardianship Status",
"type": "radio",
"type": "checkbox",
"required": true,
"extras": {
"placeHolder": "Select one option",
@ -784,7 +785,8 @@
"Have children not living with me",
"Someone else is under my guardianship"
]
}
},
"private": true
},
{
"title": "Number of Children",
@ -800,7 +802,8 @@
"placeHolder": "2",
"range": [0, 10],
"options": []
}
},
"private": true
},
{
"title": "Short Children/Guardianship Explanation",
@ -810,7 +813,8 @@
"placeHolder": "Enter details here...",
"range": [0, 0],
"options": []
}
},
"private": true
}
]
},
@ -831,7 +835,8 @@
"placeHolder": "Ayatollah Sistani",
"range": [0, 0],
"options": []
}
},
"private": true
},
{
"title": "Commitment to Obligatory Prayers",
@ -874,13 +879,14 @@
"range": [0, 0],
"options": [
"Full Islamic covering (Maximum Hijab) - Abaya, Jilbab, Chador, or Niqab with full observance.",
"Full Hijab with loose modest clothing - Wide and long outfits with full hair covering.",
"Customary/Everyday clothing with Hijab - Modern styles with hair covered by shawl or turban.",
"Modest and dignified (No hair covering) - Formal and modest outfits without a headscarf.",
"Modern and casual (No Hijab) - Following international styles without Islamic Hijab rules."
"Full Hijab with modest clothing - Modest styling with hair completely covered.",
"Customary covering - Modest everyday clothing with general hair covering.",
"No Hijab (Modest styling) - Dignified modest attire without headscarf.",
"No Hijab (Casual/Modern) - Modern styling and casual outfits."
],
"noSearch": true
}
},
"private": true
},
{
"title": "Makeup in Public",
@ -897,7 +903,8 @@
"Only very light makeup",
"Full makeup"
]
}
},
"private": true
},
{
"title": "Attitude towards Religion and Politics",
@ -948,7 +955,8 @@
"Social and comfortable (Within religious limits) - Active in social circles within moral limits.",
"No specific boundaries - Fully comfortable with modern social interactions."
]
}
},
"private": true
},
{
"title": "Smoking",
@ -1043,7 +1051,8 @@
"Listen to Halal and permissible music",
"No specific sensitivity towards music types"
]
}
},
"private": true
},
{
"title": "Attitude towards Wedding Ceremony",
@ -1089,7 +1098,8 @@
"Dedicated to Personal Growth"
],
"noSearch": true
}
},
"private": true
},
{
"title": "Your Hobbies and Main Interests",
@ -1118,7 +1128,8 @@
"Technology and Computers"
],
"noSearch": true
}
},
"private": true
},
{
"title": "Short explanation about your lifestyle",
@ -1128,7 +1139,8 @@
"placeHolder": "Enter details here...",
"range": [0, 0],
"options": []
}
},
"private": true
}
]
},

60
src/data/questions/fa.json

@ -613,7 +613,8 @@
"range": [0, 0],
"options": []
},
"private": true
"private": true,
"tooltip": "اگر شرایط خاصی درباره کار، درآمد، اجاره، خرید خانه، مهاجرت یا محل زندگی آینده دارید، کوتاه توضیح دهید."
}
]
},
@ -661,17 +662,27 @@
"options": [
"با هم زندگی می‌کنند.",
"از هم جدا شده‌اند / طلاق گرفته‌اند.",
"یکی از والدین ازدواج مجدد داشته است.",
"شرایط خانوادگی خاص دارم و در توضیحات می‌نویسم."
]
},
"private": true
},
{
"title": "توضیح کوتاه درباره خانواده",
"type": "text",
"required": false,
"extras": {
"placeHolder": "توضیحات خود را اینجا وارد کنید...",
"range": [0, 0],
"options": []
}
},
{
"title": "فضای مذهبی و اعتقادی خانواده",
"type": "radio",
"required": false,
"description": "Please select the option that best describes the general atmosphere and lifestyle of your family.",
"tooltip": "### راهنمای گزینه‌های فضای مذهبی خانواده\n\n* **مذهبی و کاملاً مقید:** خانواده‌ای که تقید بسیار بالایی به انجام تمام واجبات دارد، حدود شرعی (مانند محرم و نامحرم) را به شدت رعایت می‌کند و آداب و مناسک مذهبی در تمام شئون زندگی آن‌ها جریان دارد.\n* **مذهبی (مقید به واجبات):** خانواده‌ای که متعهد به واجبات اصلی مذهبی (مانند نماز و روزه) و اخلاق اسلامی است و در چارچوب‌های متعارف یک جامعه متدین زندگی می‌کند.\n* **سنتی (محترم به ارزش‌های دینی):** خانواده‌ای که به ارزش‌های اخلاقی پایبند است و به دین احترام می‌گذارد، اما ممکن است تمام احکام و واجبات مذهبی را به طور دقیق و کامل اجرا نکند.\n* **غیرمذهبی / عرفی:** خانواده‌ای که مناسک و چارچوب‌های مذهبی تاثیر تعیین‌کننده‌ای بر سبک زندگی، ارتباطات یا تصمیم‌گیری‌های روزمره‌شان ندارد، هرچند ممکن است احترامی کلی برای مذهب قائل باشند.",
"extras": {
"placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0],
@ -692,16 +703,6 @@
"range": [0, 0],
"options": ["ضعیف", "متوسط", "خوب", "مرفه"]
}
},
{
"title": "توضیح کوتاه درباره خانواده",
"type": "text",
"required": false,
"extras": {
"placeHolder": "توضیحات خود را اینجا وارد کنید...",
"range": [0, 0],
"options": []
}
}
]
},
@ -773,7 +774,7 @@
},
{
"title": "وضعیت فرزند و تکفل",
"type": "radio",
"type": "checkbox",
"required": true,
"extras": {
"placeHolder": "یک گزینه را انتخاب کنید",
@ -784,7 +785,8 @@
"فرزند دارم اما با من زندگی نمی‌کند.",
"شخص دیگری غیر از فرزند تحت تکفل من است."
]
}
},
"private": true
},
{
"title": "تعداد فرزندان",
@ -800,7 +802,8 @@
"placeHolder": "۲",
"range": [0, 10],
"options": []
}
},
"private": true
},
{
"title": "توضیح کوتاه درباره شرایط فرزند یا تکفل",
@ -810,7 +813,8 @@
"placeHolder": "توضیحات را اینجا وارد کنید...",
"range": [0, 0],
"options": []
}
},
"private": true
}
]
},
@ -831,7 +835,8 @@
"placeHolder": "آیت‌الله سیستانی",
"range": [0, 0],
"options": []
}
},
"private": true
},
{
"title": "میزان تقید به نمازهای واجب",
@ -880,7 +885,8 @@
"پوشش مدرن و آزاد (بدون رعایت حجاب) - دنبال کردن استایل‌های روز بدون پایبندی به قواعد حجاب اسلامی."
],
"noSearch": true
}
},
"private": true
},
{
"title": "استفاده از آرایش در اجتماع",
@ -897,7 +903,8 @@
"فقط آرایش بسیار ملایم",
"آرایش کامل"
]
}
},
"private": true
},
{
"title": "نگرش به رابطه دین و سیاست",
@ -948,7 +955,8 @@
"اجتماعی و راحت (در چارچوب شرعی) - حضور فعال در جمع‌های اجتماعی با رعایت حدود اخلاقی.",
"مرز خاصی ندارم - با تعاملات اجتماعی مدرن کاملاً راحت هستم."
]
}
},
"private": true
},
{
"title": "سیگار",
@ -1043,7 +1051,8 @@
"به موسیقی‌های مجاز و حلال گوش می‌دهم",
"حساسیت خاصی روی نوع موسیقی ندارم"
]
}
},
"private": true
},
{
"title": "نگرش به مراسم عروسی",
@ -1089,7 +1098,8 @@
"اهل رشد فردی"
],
"noSearch": true
}
},
"private": true
},
{
"title": "سرگرمی‌ها و علایق اصلی",
@ -1118,7 +1128,8 @@
"تکنولوژی و کامپیوتر"
],
"noSearch": true
}
},
"private": true
},
{
"title": "توضیح کوتاه درباره سبک زندگی",
@ -1128,7 +1139,8 @@
"placeHolder": "توضیحات را اینجا وارد کنید...",
"range": [0, 0],
"options": []
}
},
"private": true
}
]
},

4
src/data/section-slug-map.ts

@ -8,8 +8,8 @@ export const BACKEND_TO_FRONTEND_SLUG_MAP: Record<string, string> = {
contact_residence: "contact_residence_family_communication",
appearance_health: "appearance_health_activity",
education_career: "education_career_economic_status",
family_background: "family_background",
marital_history: "marital_history_children",
family_background: "family_marital_history",
marital_history: "family_marital_history",
beliefs_lifestyle: "beliefs_lifestyle_boundaries",
spouse_criteria: "future_spouse_criteria",
documents_verification: "identity_verification",

147
src/hooks/marriage/use-section-data.ts

@ -1,6 +1,7 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getQuestionListItemBySlug } from "@/data/question-data";
import { toBackendSlug } from "@/data/section-slug-map";
import { http } from "@/lib/http";
import type { MutationOptions, QueryOptions } from "./options";
@ -11,8 +12,62 @@ import type {
UpdateMarriageSectionDataPayload,
} from "./types";
export async function getMarriageSectionData(slug: string) {
function hashString(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash.toString(36);
}
function slugifyQuestionTitle(title: string) {
const slug = title
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return slug || `field_${hashString(title)}`;
}
function getQuestionFieldKey(title: string, index: number) {
return `q${index + 1}_${slugifyQuestionTitle(title)}`;
}
function hasQuestionAnswerValue(value: unknown) {
if (value === null || value === undefined) {
return false;
}
if (typeof value === "string") {
return value.trim().length > 0;
}
return true;
}
export async function getMarriageSectionData(
slug: string,
): Promise<MarriageSectionData> {
const backendSlug = toBackendSlug(slug);
if (backendSlug === "family_marital_history") {
const [fbData, mhData] = await Promise.all([
getMarriageSectionData("family_background"),
getMarriageSectionData("marital_history_children"),
]);
return {
slug: "family_marital_history",
data: [...(fbData.data || []), ...(mhData.data || [])],
current_step: fbData.current_step + mhData.current_step,
total_steps: fbData.total_steps + mhData.total_steps,
completion_percent:
fbData.total_steps + mhData.total_steps > 0
? ((fbData.current_step + mhData.current_step) /
(fbData.total_steps + mhData.total_steps)) *
100
: 0,
updated_at: fbData.updated_at || mhData.updated_at,
};
}
const { data } = await http.get<MarriageSectionData>(
`/api/marriage/sections/${pathParam(backendSlug)}/data/`,
);
@ -25,6 +80,96 @@ export async function updateMarriageSectionData(
payload: UpdateMarriageSectionDataPayload,
) {
const backendSlug = toBackendSlug(slug);
if (backendSlug === "family_marital_history") {
const fbQuestionsEn =
getQuestionListItemBySlug("family_background", "en")?.questions || [];
const mhQuestionsEn =
getQuestionListItemBySlug("marital_history_children", "en")?.questions ||
[];
const fbQuestionsFa =
getQuestionListItemBySlug("family_background", "fa")?.questions || [];
const mhQuestionsFa =
getQuestionListItemBySlug("marital_history_children", "fa")?.questions ||
[];
const fbKeys = new Set([
...fbQuestionsEn.map((q, idx) => getQuestionFieldKey(q.title, idx)),
...fbQuestionsFa.map((q, idx) => getQuestionFieldKey(q.title, idx)),
]);
const mhKeys = new Set([
...mhQuestionsEn.map((q, idx) => getQuestionFieldKey(q.title, idx)),
...mhQuestionsFa.map((q, idx) => getQuestionFieldKey(q.title, idx)),
]);
const fbFields = payload.fields.filter((f) => fbKeys.has(f.key));
const mhFields = payload.fields.filter((f) => mhKeys.has(f.key));
const fbRequiredQuestions = fbQuestionsEn.filter(
(q) => q.required && !q.logic?.dependsOn,
);
const mhRequiredQuestions = mhQuestionsEn.filter(
(q) => q.required && !q.logic?.dependsOn,
);
const fbRequiredCount = fbRequiredQuestions.length;
const mhRequiredCount = mhRequiredQuestions.length;
const fbCurrentStep = fbQuestionsEn.filter((q, idx) => {
if (!q.required || q.logic?.dependsOn) return false;
const keyEn = getQuestionFieldKey(q.title, idx);
const keyFa = getQuestionFieldKey(
fbQuestionsFa[idx]?.title || q.title,
idx,
);
const field = fbFields.find((f) => f.key === keyEn || f.key === keyFa);
return field && hasQuestionAnswerValue(field.value);
}).length;
const mhCurrentStep = mhQuestionsEn.filter((q, idx) => {
if (!q.required || q.logic?.dependsOn) return false;
const keyEn = getQuestionFieldKey(q.title, idx);
const keyFa = getQuestionFieldKey(
mhQuestionsFa[idx]?.title || q.title,
idx,
);
const field = mhFields.find((f) => f.key === keyEn || f.key === keyFa);
return field && hasQuestionAnswerValue(field.value);
}).length;
const [fbResult, mhResult] = await Promise.all([
http.patch<MarriageSectionData>(
`/api/marriage/sections/family_background/data/`,
{
current_step: fbCurrentStep,
total_steps: fbRequiredCount,
fields: fbFields,
},
),
http.patch<MarriageSectionData>(
`/api/marriage/sections/marital_history/data/`,
{
current_step: mhCurrentStep,
total_steps: mhRequiredCount,
fields: mhFields,
},
),
]);
return {
slug: "family_marital_history",
data: [...(fbResult.data.data || []), ...(mhResult.data.data || [])],
current_step: fbResult.data.current_step + mhResult.data.current_step,
total_steps: fbResult.data.total_steps + mhResult.data.total_steps,
completion_percent:
fbResult.data.total_steps + mhResult.data.total_steps > 0
? ((fbResult.data.current_step + mhResult.data.current_step) /
(fbResult.data.total_steps + mhResult.data.total_steps)) *
100
: 0,
updated_at: fbResult.data.updated_at || mhResult.data.updated_at,
};
}
const { data } = await http.patch<MarriageSectionData>(
`/api/marriage/sections/${pathParam(backendSlug)}/data/`,
payload,

4
src/translations/locales/ar.json

@ -67,7 +67,9 @@
"startMatchFailed": "فشل إرسال طلب التوافق. يرجى التحقق من الاتصال والمحاولة مرة أخرى.",
"moveToEnd": "الانتقال إلى النهاية",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "الخلفية العائلية، الحالة الاجتماعية والأطفال",
"familyMaritalEstimate": "20 دقيقة"
},
"match": {
"title": "New Match",

4
src/translations/locales/az.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Ailə Keçmişi, Ailə Vəziyyəti və Uşaqlar",
"familyMaritalEstimate": "20 dəqiqə"
},
"match": {
"title": "New Match",

4
src/translations/locales/bn.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "পারিবারিক পটভূমি, বৈবাহিক অবস্থা এবং সন্তানাদি",
"familyMaritalEstimate": "20 মিনিট"
},
"match": {
"title": "New Match",

4
src/translations/locales/da.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Familiebaggrund, civilstand og børn",
"familyMaritalEstimate": "20 minutter"
},
"match": {
"title": "New Match",

4
src/translations/locales/de.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Familiärer Hintergrund, Familienstand und Kinder",
"familyMaritalEstimate": "20 Minuten"
},
"match": {
"title": "New Match",

4
src/translations/locales/en.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Family Background, Marital Status, and Children",
"familyMaritalEstimate": "20 minutes"
},
"match": {
"title": "New Match",

4
src/translations/locales/es.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Antecedentes familiares, estado civil e hijos",
"familyMaritalEstimate": "20 minutos"
},
"match": {
"title": "New Match",

4
src/translations/locales/fa.json

@ -67,7 +67,9 @@
"startMatchFailed": "ارسال درخواست مچ انجام نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "برای حفظ آرامش، امنیت و شأن شما، روند آشنایی در پلتفرم ما با الگوگیری از رسوم اصیل و محترمانه خانوادگی پیش می‌رود. حضور یک فرد معتمد (ترجیحاً پدر یا مادر) به عنوان رابط، علاوه بر اینکه نشان‌دهنده اصالت شماست، باعث می‌شود طرف مقابل نیز با جدیت، احترام و اطمینان کامل قدم پیش بگذارد.",
"guardianNoticeOver27": "هدف ما شکل‌گیری پیوندهای پایدار بر بستر اعتماد متقابل است. با اینکه ثبت اطلاعات رابط برای شما الزامی نیست، اما معرفی یک فرد معتمد (مانند پدر، مادر یا بزرگتر خانواده) نشان‌دهنده شفافیت و نیت جدی شما برای ازدواج است. پروفایل‌هایی که دارای رابط معتمد هستند، اعتبار بسیار بالاتری دارند و باعث ایجاد اطمینان خاطر بیشتری در خانواده طرف مقابل می‌شوند."
"guardianNoticeOver27": "هدف ما شکل‌گیری پیوندهای پایدار بر بستر اعتماد متقابل است. با اینکه ثبت اطلاعات رابط برای شما الزامی نیست، اما معرفی یک فرد معتمد (مانند پدر، مادر یا بزرگتر خانواده) نشان‌دهنده شفافیت و نیت جدی شما برای ازدواج است. پروفایل‌هایی که دارای رابط معتمد هستند، اعتبار بسیار بالاتری دارند و باعث ایجاد اطمینان خاطر بیشتری در خانواده طرف مقابل می‌شوند.",
"familyMaritalTitle": "پیشینه خانوادگی، وضعیت تأهل و فرزندان",
"familyMaritalEstimate": "20 دقیقه"
},
"match": {
"title": "گزینه جدید",

4
src/translations/locales/fr.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Antécédents familiaux, état civil et enfants",
"familyMaritalEstimate": "20 minutes"
},
"match": {
"title": "New Match",

4
src/translations/locales/gu.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "પારિવારિક પૃષ્ઠભૂમિ, વૈવાહિક સ્થિતિ અને બાળકો",
"familyMaritalEstimate": "20 મિનિટ"
},
"match": {
"title": "New Match",

4
src/translations/locales/ha.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Tarihin Iyali, Yanayin Aure da Yara",
"familyMaritalEstimate": "Minti 20"
},
"match": {
"title": "New Match",

4
src/translations/locales/he.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "רקע משפחתי, מצב משפחתי וילדים",
"familyMaritalEstimate": "20 דקות"
},
"match": {
"title": "New Match",

4
src/translations/locales/hi.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "पारिवारिक पृष्ठभूमि, वैवाहिक स्थिति और बच्चे",
"familyMaritalEstimate": "20 मिनट"
},
"match": {
"title": "New Match",

4
src/translations/locales/id.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Latar Belakang Keluarga, Status Pernikahan, dan Anak-anak",
"familyMaritalEstimate": "20 menit"
},
"match": {
"title": "New Match",

4
src/translations/locales/ks.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "خاندانی پس منظر، ازدواجی حیثیت تہٰ شرے",
"familyMaritalEstimate": "20 منٹ"
},
"match": {
"title": "New Match",

4
src/translations/locales/pt.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Histórico familiar, estado civil e filhos",
"familyMaritalEstimate": "20 minutos"
},
"match": {
"title": "New Match",

4
src/translations/locales/ru.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Семейное положение, история брака и дети",
"familyMaritalEstimate": "20 минут"
},
"match": {
"title": "New Match",

4
src/translations/locales/sw.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Historia ya Familia, Hali ya Ndoa na Watoto",
"familyMaritalEstimate": "Dakika 20"
},
"match": {
"title": "New Match",

4
src/translations/locales/tg.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Маълумоти оилавӣ, вазъи оилавӣ ва кӯдакон",
"familyMaritalEstimate": "20 дақиқа"
},
"match": {
"title": "New Match",

4
src/translations/locales/tr.json

@ -67,7 +67,9 @@
"startMatchFailed": "Eşleşme isteği gönderilemedi. Lütfen bağlantınızı kontrol edip tekrar deneyin.",
"moveToEnd": "Sona Taşı",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Aile Geçmişi, Medeni Durum ve Çocuklar",
"familyMaritalEstimate": "20 dakika"
},
"match": {
"title": "New Match",

4
src/translations/locales/ul.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Khandani Pas-manzar, Azdawaji Haisiyat aur Bacche",
"familyMaritalEstimate": "20 minutes"
},
"match": {
"title": "New Match",

4
src/translations/locales/ur.json

@ -67,7 +67,9 @@
"startMatchFailed": "میچ کی درخواست بھیجنے میں ناکامی۔ برائے مہربانی اپنا کنکشن چیک کریں اور دوبارہ کوشش کریں۔",
"moveToEnd": "آخر میں منتقل کریں",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "خاندانی پس منظر، ازدواجی حیثیت اور بچے",
"familyMaritalEstimate": "20 منٹ"
},
"match": {
"title": "New Match",

4
src/translations/locales/uz.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Oila tarixi, oilaviy ahvol va bolalar",
"familyMaritalEstimate": "20 daqiqa"
},
"match": {
"title": "New Match",

4
src/translations/locales/zh.json

@ -67,7 +67,9 @@
"startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"moveToEnd": "Move to the End",
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "家庭背景、婚姻状况和子女",
"familyMaritalEstimate": "20 分钟"
},
"match": {
"title": "New Match",

Loading…
Cancel
Save