diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx
index 5cc6df5..c610f37 100644
--- a/src/app/questions-list/[slug]/question-detail-client.tsx
+++ b/src/app/questions-list/[slug]/question-detail-client.tsx
@@ -520,7 +520,7 @@ export default function QuestionDetailClient({
@@ -565,7 +565,7 @@ export default function QuestionDetailClient({
@@ -601,7 +601,7 @@ export default function QuestionDetailClient({
@@ -780,7 +780,7 @@ export default function QuestionDetailClient({
@@ -961,7 +961,7 @@ export default function QuestionDetailClient({
diff --git a/src/components/Componentes/question-sheet.test.tsx b/src/components/Componentes/question-sheet.test.tsx
index 0865cc0..175b557 100644
--- a/src/components/Componentes/question-sheet.test.tsx
+++ b/src/components/Componentes/question-sheet.test.tsx
@@ -744,6 +744,105 @@ describe("QuestionSheet component", () => {
expect(screen.getByRole("button", { name: "Арабский" })).toBeDefined();
expect(screen.getByRole("button", { name: "Русский" })).toBeDefined();
});
+
+ it("renders search bar and full list of languages for mother_tongue even with initial partial options", async () => {
+ const { useI18n } = await import("@/translations/provider");
+ (useI18n as any).mockReturnValue({
+ locale: "fa",
+ dictionary: { "Mother Tongue": "زبان مادری", Confirm: "تایید" },
+ });
+
+ const languageQuestion = {
+ id: "personal_identity.mother_tongue",
+ title: "Native Language / Mother Tongue",
+ type: "dropdown",
+ order: 1,
+ required: true,
+ isVisible: true,
+ description: "",
+ tooltip: "",
+ extras: { placeHolder: "انتخاب کنید", noSearch: true },
+ ui_config: { noSearch: true },
+ options: [
+ { id: "opt_fa", value: "persian", label: "Persian (Farsi)", order: 1 },
+ { id: "opt_en", value: "english", label: "English", order: 2 },
+ { id: "opt_other", value: "other", label: "Other", order: 3 },
+ ],
+ } as QuestionField;
+
+ render(
+
+
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button"));
+
+ // Verify search input is present
+ const searchInput = screen.getByPlaceholderText("جستجو...");
+ expect(searchInput).toBeDefined();
+
+ // Verify options have full languages list (e.g. آلمانی, فرانسوی, etc.)
+ expect(screen.getByRole("button", { name: "آلمانی" })).toBeDefined();
+ expect(screen.getByRole("button", { name: "فرانسوی" })).toBeDefined();
+ expect(screen.getByRole("button", { name: "فارسی" })).toBeDefined();
+
+ // Test search filtering
+ fireEvent.change(searchInput, { target: { value: "آلمان" } });
+ expect(screen.getByRole("button", { name: "آلمانی" })).toBeDefined();
+ expect(screen.queryByRole("button", { name: "فرانسوی" })).toBeNull();
+ });
+
+ it("renders search bar and filters country options for current_nationality_citizenship", async () => {
+ const { useI18n } = await import("@/translations/provider");
+ (useI18n as any).mockReturnValue({
+ locale: "en",
+ dictionary: { Confirm: "Confirm" },
+ });
+
+ const nationalityQuestion = {
+ id: "personal_identity.current_nationality_citizenship",
+ title: "Current Nationality / Citizenship",
+ type: "dropdown",
+ order: 2,
+ required: true,
+ isVisible: true,
+ description: "",
+ tooltip: "",
+ extras: { placeHolder: "Select one option", noSearch: true },
+ ui_config: { noSearch: true },
+ options: [
+ { id: "opt_af", value: "afghanistan", label: "Afghanistan", order: 1 },
+ { id: "opt_al", value: "albania", label: "Albania", order: 2 },
+ { id: "opt_dz", value: "algeria", label: "Algeria", order: 3 },
+ ],
+ } as QuestionField;
+
+ render(
+
+
+
+
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button"));
+
+ // Verify search input is present
+ const searchInput = screen.getByPlaceholderText("Search...");
+ expect(searchInput).toBeDefined();
+
+ // Verify country options are rendered
+ expect(screen.getByRole("button", { name: "Afghanistan" })).toBeDefined();
+ expect(screen.getByRole("button", { name: "Albania" })).toBeDefined();
+
+ // Test search filtering by country name
+ fireEvent.change(searchInput, { target: { value: "Albania" } });
+ expect(screen.getByRole("button", { name: "Albania" })).toBeDefined();
+ expect(screen.queryByRole("button", { name: "Afghanistan" })).toBeNull();
+ });
});
diff --git a/src/components/Componentes/question-sheet.tsx b/src/components/Componentes/question-sheet.tsx
index 79d398c..0aa7739 100644
--- a/src/components/Componentes/question-sheet.tsx
+++ b/src/components/Componentes/question-sheet.tsx
@@ -9,7 +9,12 @@ import {
normalizeSearchString,
getCountrySearchKeywords,
} from "@/data/countries";
-import { LANGUAGES_EN, LANGUAGES_FA, resolveLanguageName } from "@/data/languages";
+import {
+ LANGUAGES_EN,
+ LANGUAGES_FA,
+ resolveLanguageName,
+ getLanguageSearchKeywords,
+} from "@/data/languages";
import type { QuestionField } from "@/lib/schema-adapter";
import { useI18n } from "@/translations/provider";
import { Button } from "./button";
@@ -21,6 +26,7 @@ import { useSheetScrollLock } from "./use-sheet-scroll-lock";
const EXIT_ANIMATION_MS = 200;
const EMPTY_ARRAY: string[] = [];
+const COMPACT_OPTIONS_MAX = 6;
export type QuestionSheetProps = {
question: QuestionField;
@@ -122,32 +128,47 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
question.id?.toLowerCase().includes("residence_status") ||
question.id?.toLowerCase().includes("status") ||
question.id?.toLowerCase().includes("responsibility") ||
- question.extras?.noSearch === true ||
- question.ui_config?.noSearch === true;
+ question.id?.toLowerCase().includes("difference") ||
+ question.id?.toLowerCase().includes("impact");
+
+ const isLanguageQuestion = Boolean(
+ question.ui_config?.dataset === "languages" ||
+ question.id?.endsWith(".mother_tongue") ||
+ question.id?.endsWith(".native_language") ||
+ question.id?.endsWith(".other_languages") ||
+ question.id?.endsWith(".languages") ||
+ question.id?.endsWith(".language") ||
+ (!question.id?.includes("impact") &&
+ !question.id?.includes("difference") &&
+ Boolean(
+ question.title?.toLowerCase().includes("mother tongue") ||
+ question.title?.toLowerCase().includes("native language") ||
+ question.title?.toLowerCase() === "language" ||
+ question.title?.toLowerCase() === "languages" ||
+ question.title?.includes("زبان مادری") ||
+ question.title?.includes("زبانهای مسلط") ||
+ question.title?.includes("زبان های مسلط")
+ ))
+ );
- const isLanguageQuestion =
- !isExcludedFromAutoDatasets &&
- (question.ui_config?.dataset === "languages" ||
- question.id?.endsWith(".mother_tongue") ||
- question.id?.endsWith(".native_language") ||
- question.id?.endsWith(".other_languages") ||
- (Boolean(question.title?.toLowerCase().includes("language")) &&
- !question.options?.length));
-
- const isCountryQuestion =
+ const isCountryQuestion = Boolean(
!isExcludedFromAutoDatasets &&
(question.ui_config?.dataset === "countries" ||
question.ui_config?.dataset === "nationalities" ||
- question.id?.endsWith(".nationality") ||
- question.id?.endsWith(".citizenship") ||
- question.id?.endsWith(".second_nationality") ||
- (Boolean(
- question.id?.endsWith(".birthplace") ||
- question.id?.endsWith(".current_residence") ||
- question.title?.toLowerCase().includes("nationality") ||
- question.title?.toLowerCase().includes("citizenship"),
- ) &&
- !question.options?.length));
+ question.id?.includes("nationality") ||
+ question.id?.includes("citizenship") ||
+ question.id?.endsWith(".birthplace") ||
+ question.id?.endsWith(".current_residence") ||
+ question.id?.endsWith(".country") ||
+ Boolean(
+ question.title?.toLowerCase().includes("nationality") ||
+ question.title?.toLowerCase().includes("citizenship") ||
+ question.title?.toLowerCase().includes("country") ||
+ question.title?.includes("تابعیت") ||
+ question.title?.includes("ملیت") ||
+ question.title?.includes("کشور")
+ ))
+ );
const options = useMemo(() => {
const rawOptions = question.options || [];
@@ -165,6 +186,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
const mergedOptions: typeof rawOptions = [];
const seenIds = new Set();
+ const seenLabels = new Set();
LANGUAGES_EN.forEach((enLang, idx) => {
const displayLabel = resolveLanguageName(enLang, locale);
@@ -181,8 +203,10 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
existingById.get(generatedId.toLowerCase());
const finalId = existing?.id || generatedId;
- if (seenIds.has(finalId)) return;
+ const normLabel = displayLabel.toLowerCase().trim();
+ if (seenIds.has(finalId) || seenLabels.has(normLabel)) return;
seenIds.add(finalId);
+ seenLabels.add(normLabel);
mergedOptions.push({
id: finalId,
@@ -193,12 +217,27 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
});
rawOptions.forEach((opt) => {
- if (!seenIds.has(opt.id)) {
+ const normLabel = opt.label.toLowerCase().trim();
+ if (!seenIds.has(opt.id) && !seenLabels.has(normLabel)) {
seenIds.add(opt.id);
+ seenLabels.add(normLabel);
mergedOptions.push(opt);
}
});
+ // Keep "Other" / "سایر" at the very end
+ const otherIndex = mergedOptions.findIndex(
+ (opt) =>
+ opt.value === "other" ||
+ opt.id.endsWith(".other") ||
+ opt.label.toLowerCase() === "other" ||
+ opt.label === "سایر",
+ );
+ if (otherIndex !== -1 && otherIndex !== mergedOptions.length - 1) {
+ const [otherOpt] = mergedOptions.splice(otherIndex, 1);
+ mergedOptions.push(otherOpt);
+ }
+
return mergedOptions;
}
@@ -259,13 +298,16 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
}, [question, isLanguageQuestion, isCountryQuestion, locale, t]);
const noSearch = Boolean(
- question.extras?.noSearch === true ||
- question.ui_config?.noSearch === true ||
- question.id?.toLowerCase().includes("responsibility"),
+ !isLanguageQuestion &&
+ !isCountryQuestion &&
+ options.length <= COMPACT_OPTIONS_MAX &&
+ (
+ question.extras?.noSearch === true ||
+ question.ui_config?.noSearch === true ||
+ question.id?.toLowerCase().includes("responsibility")
+ )
);
- const COMPACT_OPTIONS_MAX = 6;
-
const isCompact = options.length <= COMPACT_OPTIONS_MAX || noSearch;
const showSearch = !noSearch && options.length > COMPACT_OPTIONS_MAX;
@@ -299,6 +341,13 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
) {
return true;
}
+ if (isLanguageQuestion) {
+ const labelKeywords = getLanguageSearchKeywords(option.label, locale);
+ if (labelKeywords.some((k) => k.includes(normQ))) return true;
+
+ const valKeywords = getLanguageSearchKeywords(String(option.value), locale);
+ if (valKeywords.some((k) => k.includes(normQ))) return true;
+ }
if (isCountryQuestion) {
const labelKeywords = getCountrySearchKeywords(option.label, locale);
if (labelKeywords.some((k) => k.includes(normQ))) return true;
@@ -308,7 +357,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
}
return false;
});
- }, [options, searchQuery, isCountryQuestion, locale]);
+ }, [options, searchQuery, isLanguageQuestion, isCountryQuestion, locale]);
const getCleanLabel = (optId: string) => {
const normalized = String(optId || "").toLowerCase().trim();
@@ -317,9 +366,16 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
o.id === optId ||
o.id.toLowerCase() === normalized ||
String(o.value).toLowerCase() === normalized ||
+ o.label.toLowerCase().trim() === normalized ||
o.id.endsWith(`.${normalized}`),
);
- if (!opt) return optId;
+ if (!opt) {
+ if (isLanguageQuestion) {
+ const resolved = resolveLanguageName(optId, locale);
+ if (resolved && resolved !== optId) return resolved;
+ }
+ return optId;
+ }
return opt.label.split(" - ")[0];
};
@@ -593,8 +649,13 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => {
const isSelected = isMulti
- ? localSelectedList.includes(option.id)
- : singleValue === option.id;
+ ? localSelectedList.includes(option.id) ||
+ localSelectedList.includes(option.value) ||
+ localSelectedList.includes(option.id.split(".").pop() || "")
+ : singleValue === option.id ||
+ singleValue === option.value ||
+ singleValue === option.id.split(".").pop() ||
+ option.id.endsWith(`.${singleValue}`);
const isOptionDisabled =
isMulti &&
diff --git a/src/data/languages.test.ts b/src/data/languages.test.ts
index 6cce5f6..d5e3b2c 100644
--- a/src/data/languages.test.ts
+++ b/src/data/languages.test.ts
@@ -5,6 +5,8 @@ import {
getLanguageCode,
getLanguageList,
resolveLanguageName,
+ getLanguageSearchKeywords,
+ filterLanguageOptions,
} from "./languages";
describe("languages data and localization", () => {
@@ -56,4 +58,26 @@ describe("languages data and localization", () => {
const listEn = getLanguageList("en");
expect(listEn[0]).toBe("Afrikaans");
});
+
+ it("extracts language search keywords and filters options correctly", () => {
+ const faKeywords = getLanguageSearchKeywords("Persian (Farsi)", "fa");
+ expect(faKeywords).toContain("farsi");
+ expect(faKeywords).toContain("فارسی");
+
+ const options = ["Persian (Farsi)", "English", "Arabic", "German", "Other"];
+
+ // Search in Persian
+ expect(filterLanguageOptions(options, "فارسی", "fa")).toEqual(["Persian (Farsi)"]);
+ expect(filterLanguageOptions(options, "انگلیسی", "fa")).toEqual(["English"]);
+ expect(filterLanguageOptions(options, "آلمانی", "fa")).toEqual(["German"]);
+
+ // Search in English
+ expect(filterLanguageOptions(options, "persian", "en")).toEqual(["Persian (Farsi)"]);
+ expect(filterLanguageOptions(options, "farsi", "en")).toEqual(["Persian (Farsi)"]);
+ expect(filterLanguageOptions(options, "ger", "en")).toEqual(["German"]);
+
+ // Search for other
+ expect(filterLanguageOptions(options, "سایر", "fa")).toEqual(["Other"]);
+ expect(filterLanguageOptions(options, "other", "en")).toEqual(["Other"]);
+ });
});
diff --git a/src/data/languages.ts b/src/data/languages.ts
index 7444472..70dba9d 100644
--- a/src/data/languages.ts
+++ b/src/data/languages.ts
@@ -48,7 +48,6 @@ export const LANGUAGES_EN = [
"Nepali",
"Norwegian",
"Pashto",
- "Persian",
"Persian (Farsi)",
"Polish",
"Portuguese",
@@ -405,3 +404,125 @@ export function getLanguageList(locale: string = "en"): string[] {
return LANGUAGES_EN.map((enLang) => resolveLanguageName(enLang, locale));
}
+
+export const LANGUAGE_ALIASES: Record = {
+ fa: ["persian", "farsi", "فارسی", "پارسی", "فارسی دری", "دری", "تاجیکی"],
+ en: ["english", "انگلیسی", "اینگلیسی", "انگلیش"],
+ ar: ["arabic", "عربی", "عربى"],
+ tr: ["turkish", "ترکی", "ترکی استانبولی", "turkce", "türkçe"],
+ ur: ["urdu", "اردو", "اردو رومن", "roman urdu"],
+ fr: ["french", "فرانسوی", "فرانسه", "francais", "français"],
+ de: ["german", "آلمانی", "deutsch"],
+ es: ["spanish", "اسپانیایی", "espanol", "español"],
+ ru: ["russian", "روسی", "русский"],
+ zh: ["chinese", "چینی", "ماندارین", "mandarin"],
+ hi: ["hindi", "هندی"],
+ az: ["azerbaijani", "azeri", "آذربایجانی", "ترکی آذربایجانی", "ترکی آذری"],
+ ku: ["kurdish", "کردی", "کوردی", "سورانی", "کورمانجی"],
+ bal: ["balochi", "بلوچی"],
+ ps: ["pashto", "پشتو", "پختو"],
+ it: ["italian", "ایتالیایی"],
+ ja: ["japanese", "ژاپنی"],
+ ko: ["korean", "کره ای", "کرهای"],
+ pt: ["portuguese", "پرتغالی"],
+ nl: ["dutch", "هلندی"],
+ sv: ["swedish", "سوئدی"],
+ pl: ["polish", "لهستانی"],
+ other: ["other", "سایر", "دیگر", "سایر زبانها", "سایر زبان ها"],
+};
+
+export function normalizeLanguageSearch(str: string): string {
+ return str
+ .toLowerCase()
+ .trim()
+ .replace(/[\u064B-\u065F\u0670]/g, "")
+ .replace(/[\u200B-\u200D\uFEFF]/g, "")
+ .replace(/ي/g, "ی")
+ .replace(/ك/g, "ک")
+ .replace(/[\s\-_.,/\\()]+/g, " ");
+}
+
+export function getLanguageSearchKeywords(
+ langOrCode: string | undefined | null,
+ locale?: string,
+): string[] {
+ if (!langOrCode) return [];
+ const keywords = new Set();
+
+ const addTerm = (term: string | undefined | null) => {
+ if (!term) return;
+ const norm = normalizeLanguageSearch(term);
+ if (norm) keywords.add(norm);
+ };
+
+ addTerm(langOrCode);
+
+ const code = getLanguageCode(langOrCode);
+ if (code) {
+ addTerm(code);
+ const aliases = LANGUAGE_ALIASES[code] || [];
+ aliases.forEach(addTerm);
+
+ const enKey = Object.keys(LANGUAGE_EN_TO_CODE).find(
+ (k) => LANGUAGE_EN_TO_CODE[k] === code,
+ );
+ if (enKey) {
+ addTerm(enKey);
+ if (LANGUAGE_EN_TO_FA[enKey]) {
+ addTerm(LANGUAGE_EN_TO_FA[enKey]);
+ }
+ }
+
+ try {
+ const enNames = new Intl.DisplayNames(["en"], { type: "language" });
+ addTerm(enNames.of(code));
+ } catch {}
+
+ try {
+ const faNames = new Intl.DisplayNames(["fa"], { type: "language" });
+ addTerm(faNames.of(code));
+ } catch {}
+
+ if (locale && locale !== "en" && locale !== "fa") {
+ try {
+ const locNames = new Intl.DisplayNames([locale], { type: "language" });
+ addTerm(locNames.of(code));
+ } catch {}
+ }
+ }
+
+ const trimmedLower = langOrCode.toLowerCase().trim();
+ if (
+ trimmedLower === "other" ||
+ trimmedLower === "سایر" ||
+ code === "other"
+ ) {
+ const otherAliases = ["other", "سایر", "دیگر", "دیگری", "غیره"];
+ otherAliases.forEach(addTerm);
+ }
+
+ return Array.from(keywords);
+}
+
+export function filterLanguageOptions(
+ options: string[],
+ searchQuery: string,
+ locale?: string,
+): string[] {
+ const rawQ = searchQuery.trim();
+ if (!rawQ) return options;
+ const normQ = normalizeLanguageSearch(rawQ);
+ const lowerQ = rawQ.toLowerCase();
+
+ return options.filter((option) => {
+ if (
+ option.toLowerCase().includes(lowerQ) ||
+ normalizeLanguageSearch(option).includes(normQ)
+ ) {
+ return true;
+ }
+ const keywords = getLanguageSearchKeywords(option, locale);
+ return keywords.some((k) => k.includes(normQ));
+ });
+}
+
diff --git a/src/lib/marriage-profile-contract.ts b/src/lib/marriage-profile-contract.ts
index 3390f60..844ac54 100644
--- a/src/lib/marriage-profile-contract.ts
+++ b/src/lib/marriage-profile-contract.ts
@@ -7,7 +7,7 @@ import type {
MarriageRecommendedPlan,
} from "@/hooks/marriage/types";
-export const REQUIRED_PROFILE_SECTION_COUNT = 10;
+export const REQUIRED_PROFILE_SECTION_COUNT = 9;
/**
* Normalizes a single MarriageField from various potential backend or Flutter bridge formats.
diff --git a/src/lib/schema-adapter.ts b/src/lib/schema-adapter.ts
index 5ae2056..be7366c 100644
--- a/src/lib/schema-adapter.ts
+++ b/src/lib/schema-adapter.ts
@@ -224,10 +224,24 @@ export function mapBackendQuestionToFrontend(
if (!uiConfig.picker_type) uiConfig.picker_type = uiConfig.source || "file_system";
}
+ let questionType = bq.type;
+ if (
+ questionType === "text" &&
+ (bq.id?.includes("contact_number") ||
+ bq.id?.includes("phone") ||
+ bq.ui_config?.input_type === "phone" ||
+ Boolean(bq.title?.toLowerCase().includes("contact number")) ||
+ Boolean(bq.title?.toLowerCase().includes("phone")) ||
+ Boolean(bq.title?.includes("شماره تماس")) ||
+ Boolean(bq.title?.includes("شماره تلفن")))
+ ) {
+ questionType = "phone";
+ }
+
return {
id: bq.id,
title: bq.title || "Untitled",
- type: bq.type,
+ type: questionType,
order: bq.order !== undefined ? bq.order : index,
required: bq.is_required !== undefined ? bq.is_required : bq.required,
baseRequired: bq.required,