From c84f305f59bcc79483573a867a6f544b2c9a6d59 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Fri, 4 Sep 2026 23:39:45 +0000 Subject: [PATCH 01/27] feat(staging): configure staging environment and auto-deploy scripts for Iran server --- auto_deploy_staging.sh | 63 ++++++++++++++++++++++++++++++++++++++ docker-compose.staging.yml | 32 +++++++++++++++++++ next.config.ts | 4 +++ watch_master_and_deploy.sh | 12 ++++++++ 4 files changed, 111 insertions(+) create mode 100755 auto_deploy_staging.sh create mode 100644 docker-compose.staging.yml create mode 100755 watch_master_and_deploy.sh diff --git a/auto_deploy_staging.sh b/auto_deploy_staging.sh new file mode 100755 index 0000000..ab3777a --- /dev/null +++ b/auto_deploy_staging.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -eo pipefail + +PROJECT_DIR="/home/alialavi/projects/habib/marriage" +cd "$PROJECT_DIR" + +LOG_TAG="[Marriage-Staging-Watcher]" + +# Ensure we are on staging branch +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$CURRENT_BRANCH" != "staging" ]; then + echo "$LOG_TAG ⚠️ Currently on '$CURRENT_BRANCH', switching to 'staging'..." + git checkout staging +fi + +# Fetch latest master and staging from origin +git fetch origin master staging --quiet + +# Check 1: Commits ahead on origin/master +MASTER_COMMITS=$(git rev-list --count HEAD..origin/master 2>/dev/null || echo 0) + +# Check 2: Commits ahead on origin/staging +STAGING_COMMITS=$(git rev-list --count HEAD..origin/staging 2>/dev/null || echo 0) + +NEED_BUILD=0 + +if [ "$MASTER_COMMITS" -gt 0 ]; then + echo "$LOG_TAG 🚀 Detected $MASTER_COMMITS new commit(s) on origin/master!" + git log --oneline HEAD..origin/master + echo "$LOG_TAG Merging origin/master into staging branch..." + git merge origin/master --no-edit -m "chore(staging): auto-merge origin/master into staging" + echo "$LOG_TAG Pushing updated staging branch to origin/staging..." + git push origin staging --quiet + NEED_BUILD=1 +fi + +if [ "$STAGING_COMMITS" -gt 0 ]; then + echo "$LOG_TAG 🚀 Detected $STAGING_COMMITS new commit(s) on origin/staging!" + git log --oneline HEAD..origin/staging + echo "$LOG_TAG Pulling latest origin/staging..." + git merge origin/staging --no-edit || git pull origin staging --no-edit + NEED_BUILD=1 +fi + +if [ "$NEED_BUILD" -eq 0 ]; then + # Staging is completely up to date + exit 0 +fi + +# Rebuild and restart Next.js staging web container +echo "$LOG_TAG Building and restarting marriage_staging_web container..." +docker compose -f docker-compose.staging.yml up -d --build + +# Verify container health +echo "$LOG_TAG Verifying service health..." +sleep 5 +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5561/healthz || echo "000") + +if [ "$HTTP_STATUS" -eq 200 ]; then + echo "$LOG_TAG ✅ SUCCESS: Marriage staging updated and healthy at commit $(git rev-parse --short HEAD) (HTTP 200 OK)." +else + echo "$LOG_TAG ⚠️ Health probe returned HTTP status $HTTP_STATUS" +fi diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..b8f5ce1 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,32 @@ +services: + web: + build: + context: . + dockerfile: Dockerfile + args: + NEXT_PUBLIC_API_BASE_URL: https://habib.nwhco.ir + NEXT_PUBLIC_SECURITY_KEY: t5yugymks5458fd4ghfg6h6fg + image: marriage_staging_web:latest + container_name: marriage_staging_web + restart: unless-stopped + env_file: + - .env.staging + ports: + - "127.0.0.1:5561:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_BASE_URL=https://habib.nwhco.ir + - API_BASE_URL=http://web:8000 + - NEXT_PUBLIC_SECURITY_KEY=t5yugymks5458fd4ghfg6h6fg + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + networks: + - najm_staging_net + +networks: + najm_staging_net: + external: true diff --git a/next.config.ts b/next.config.ts index 0d9f95f..9001e6c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -21,6 +21,10 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "habibapp.com", }, + { + protocol: "https", + hostname: "habib.nwhco.ir", + }, ], }, diff --git a/watch_master_and_deploy.sh b/watch_master_and_deploy.sh new file mode 100755 index 0000000..65de845 --- /dev/null +++ b/watch_master_and_deploy.sh @@ -0,0 +1,12 @@ +#!/bin/bash +PROJECT_DIR="/home/alialavi/projects/habib/marriage" +cd "$PROJECT_DIR" || exit 1 + +echo "[$(date -u +"%Y-%m-%d %H:%M:%S UTC")] Starting Marriage Staging Auto-Deploy Watcher (interval: 30s)..." + +while true; do + "$PROJECT_DIR/auto_deploy_staging.sh" || { + echo "[$(date -u +"%Y-%m-%d %H:%M:%S UTC")] Auto-deploy encountered an error, will retry in next cycle." + } + sleep 30 +done From f8a07df60e5dfacb72f8473b20a3531ff8ff6945 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Fri, 4 Sep 2026 23:44:59 +0000 Subject: [PATCH 02/27] fix(healthcheck): use 127.0.0.1 instead of localhost to prevent IPv6 Alpine resolution failure --- docker-compose.staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index b8f5ce1..b5963e8 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -19,7 +19,7 @@ services: - API_BASE_URL=http://web:8000 - NEXT_PUBLIC_SECURITY_KEY=t5yugymks5458fd4ghfg6h6fg healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"] + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz"] interval: 30s timeout: 5s retries: 3 From 880e91ac21907c963e4a2a73144042ba6931eb68 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 5 Sep 2026 12:30:27 +0330 Subject: [PATCH 03/27] feat: add faminela font file to public assets --- public/assets/fonts/faminela.woff2 | Bin 39436 -> 30768 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/public/assets/fonts/faminela.woff2 b/public/assets/fonts/faminela.woff2 index 66b924f9bb5b340b3fdc1219c6ef86f774f88a3f..9b4d7f6225d8710c733991ccbd7fc8d7d3ff23b1 100644 GIT binary patch literal 30768 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 N5BG3ft5Kt%{14vpdpiIC From 121665dd5df5d532ba094a70153f242b037575d7 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 5 Sep 2026 14:02:13 +0330 Subject: [PATCH 04/27] feat: add support for 17 new languages and create client-side question detail component --- .../questions-list/[slug]/question-detail-client.tsx | 9 +++++---- src/translations/locales/az.json | 10 +++++----- src/translations/locales/bn.json | 10 +++++----- src/translations/locales/da.json | 10 +++++----- src/translations/locales/fr.json | 6 +++--- src/translations/locales/gu.json | 10 +++++----- src/translations/locales/ha.json | 10 +++++----- src/translations/locales/he.json | 10 +++++----- src/translations/locales/hi.json | 10 +++++----- src/translations/locales/id.json | 10 +++++----- src/translations/locales/ks.json | 10 +++++----- src/translations/locales/pt.json | 10 +++++----- src/translations/locales/sw.json | 10 +++++----- src/translations/locales/tg.json | 10 +++++----- src/translations/locales/ul.json | 10 +++++----- src/translations/locales/ur.json | 10 +++++----- src/translations/locales/uz.json | 10 +++++----- src/translations/locales/zh.json | 10 +++++----- 18 files changed, 88 insertions(+), 87 deletions(-) diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 04e28e9..d14beb3 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -474,20 +474,21 @@ export default function QuestionDetailClient({ ? rawOptions.map((opt: any, idx: number) => { const score = idx + 1; if (typeof opt === "string") { - return { id: String(score), value: score, label: opt }; + return { id: String(score), value: score, label: t[opt] || opt }; } + const defaultLabel = t[DEFAULT_LABELS_EN[idx]] || defaultLabels[idx] || String(score); return { id: String(opt.id || opt.value || score), value: typeof opt.value === "number" ? opt.value : score, label: String( - opt.label || opt.text || defaultLabels[idx] || String(score), + opt.label || opt.text || defaultLabel, ), }; }) : [1, 2, 3, 4, 5].map((score, idx) => ({ id: String(score), value: score, - label: defaultLabels[idx] || String(score), + label: t[DEFAULT_LABELS_EN[idx]] || defaultLabels[idx] || String(score), })); return { @@ -502,7 +503,7 @@ export default function QuestionDetailClient({ options, }; }); - }, [glasserQuery.data, locale]); + }, [glasserQuery.data, locale, t]); diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index 2dc17cd..64c152f 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Əyləncəli vaxtlar və şənliklər, şənliklər, oyunlar və əyləncələr kimi sevincli hadisələri nə dərəcədə fəal axtarırsınız?", "How important is laughing and making others laugh in your life?": "Gülmək və başqalarını güldürmək həyatınızda nə qədər vacibdir?", "In your view, how essential is fun, play, and joy in living a good life?": "Sizcə, yaxşı həyat sürmək üçün əyləncə, oyun və sevinc nə qədər vacibdir?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Çox aşağı", + "Low": "Aşağı", + "Moderate": "Orta", + "High": "Yüksək", + "Very High": "Çox yüksək", "1 child": "1 uşaq", "1 to 3 years": "1 ildən 3 ilə qədər", "160 to 170 cm": "160 ilə 170 sm arasında", diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index 380a8e2..e80784e 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "আপনি কতটা সক্রিয়ভাবে মজার সময় এবং আনন্দের উপলক্ষ যেমন পার্টি, উদযাপন, গেমস এবং বিনোদন খোঁজেন?", "How important is laughing and making others laugh in your life?": "আপনার জীবনে হাসানো এবং অন্যকে হাসানো কতটা গুরুত্বপূর্ণ?", "In your view, how essential is fun, play, and joy in living a good life?": "আপনার দৃষ্টিতে, একটি ভাল জীবনযাপনের জন্য মজা, খেলা এবং আনন্দ কতটা অপরিহার্য?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "খুব কম", + "Low": "কম", + "Moderate": "মাঝারি", + "High": "বেশি", + "Very High": "খুব বেশি", "1 child": "1 সন্তান", "1 to 3 years": "1 থেকে 3 বছর", "160 to 170 cm": "160 থেকে 170 সেমি", diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 15ffdbf..225350b 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Hvor aktivt søger du sjove tider og glædelige lejligheder som fester, fester, spil og underholdning?", "How important is laughing and making others laugh in your life?": "Hvor vigtigt er det at grine og få andre til at grine i dit liv?", "In your view, how essential is fun, play, and joy in living a good life?": "Hvor vigtigt er sjov, leg og glæde efter din mening i at leve et godt liv?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Meget lav", + "Low": "Lav", + "Moderate": "Moderat", + "High": "Høj", + "Very High": "Meget høj", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 4b258fa..bddeaa1 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -1491,9 +1491,9 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Dans quelle mesure recherchez-vous activement des moments amusants et des occasions joyeuses comme des fêtes, des célébrations, des jeux et des divertissements ?", "How important is laughing and making others laugh in your life?": "Quelle est l’importance de rire et de faire rire les autres dans votre vie ?", "In your view, how essential is fun, play, and joy in living a good life?": "À votre avis, dans quelle mesure le plaisir, le jeu et la joie sont-ils essentiels pour vivre une bonne vie ?", - "Very Low": "Très bas", - "Low": "Bas", - "Moderate": "Modéré", + "Very Low": "Très faible", + "Low": "Faible", + "Moderate": "Moyen", "High": "Élevé", "Very High": "Très élevé", "1 child": "1 child", diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index bac1225..7d2d102 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "તમે પાર્ટીઓ, ઉજવણીઓ, રમતો અને મનોરંજન જેવા આનંદના સમય અને આનંદના પ્રસંગોને કેટલી સક્રિય રીતે શોધો છો?", "How important is laughing and making others laugh in your life?": "તમારા જીવનમાં હસવું અને બીજાને હસાવવું કેટલું મહત્વનું છે?", "In your view, how essential is fun, play, and joy in living a good life?": "તમારા મતે, સારું જીવન જીવવા માટે આનંદ, રમત અને આનંદ કેટલો જરૂરી છે?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "ખૂબ ઓછું", + "Low": "ઓછું", + "Moderate": "મધ્યમ", + "High": "વધુ", + "Very High": "ખૂબ વધુ", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 2a40086..6a4f4d5 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Yaya kuke neman lokutan jin daɗi da lokutan farin ciki kamar bukukuwa, bukukuwa, wasanni, da nishaɗi?", "How important is laughing and making others laugh in your life?": "Yaya muhimmancin dariya da sanya dariya a rayuwar ku?", "In your view, how essential is fun, play, and joy in living a good life?": "A ganin ku, yaya mahimmancin nishaɗi, wasa, da farin ciki ke da rayuwa mai kyau?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Kadan sosai", + "Low": "Kadan", + "Moderate": "Matsakaici", + "High": "Da yawa", + "Very High": "Da yawa sosai", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index 958ad20..e70b7b5 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "?? ??? ??? ???? ????? ???? ????? ????? ???????? ?????? ??? ??????, ??????, ?????? ???????", "How important is laughing and making others laugh in your life?": "?? ??? ?????? ????? ????? ??????? ?????? ??????", "In your view, how essential is fun, play, and joy in living a good life?": "?????, ?? ??? ??????? ????, ????? ?????? ????? ??????", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "נמוך מאוד", + "Low": "נמוך", + "Moderate": "בינוני", + "High": "גבוה", + "Very High": "גבוה מאוד", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index abdebbe..886166d 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "?????????, ????????, ????? ?? ??????? ???? ???????? ??? ?? ??????? ?????? ?? ?? ????? ???????? ?? ???? ???? ????", "How important is laughing and making others laugh in your life?": "???? ???? ??? ????? ?? ?????? ?? ?????? ????? ?????????? ???", "In your view, how essential is fun, play, and joy in living a good life?": "???? ????? ?? ????? ???? ?? ??? ???????, ??? ?? ???? ????? ?????? ????", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "बहुत कम", + "Low": "कम", + "Moderate": "मध्यम", + "High": "अधिक", + "Very High": "बहुत अधिक", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index 0fd099d..7c9607a 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Seberapa aktif Anda mencari saat-saat menyenangkan dan menggembirakan seperti pesta, perayaan, permainan, dan hiburan?", "How important is laughing and making others laugh in your life?": "Seberapa pentingkah tertawa dan membuat orang lain tertawa dalam hidup Anda?", "In your view, how essential is fun, play, and joy in living a good life?": "Menurut Anda, seberapa pentingkah kesenangan, permainan, dan kegembiraan dalam menjalani kehidupan yang baik?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Sangat Rendah", + "Low": "Rendah", + "Moderate": "Sedang", + "High": "Tinggi", + "Very High": "Sangat Tinggi", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index 7fca812..621ec3e 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "آپ پارٹیوں، تقریبات، گیمز اور تفریح ​​جیسے تفریحی اوقات اور خوشی کے مواقع کو کتنی سرگرمی سے تلاش کرتے ہیں؟", "How important is laughing and making others laugh in your life?": "آپ کی زندگی میں ہنسنا اور دوسروں کو ہنسانا کتنا ضروری ہے؟", "In your view, how essential is fun, play, and joy in living a good life?": "آپ کی نظر میں اچھی زندگی گزارنے کے لیے تفریح، کھیل اور خوشی کتنی ضروری ہے؟", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "واریاہ کم", + "Low": "کم", + "Moderate": "درمیانہ", + "High": "زیادہ", + "Very High": "واریاہ زیادہ", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index 32ac6eb..aaa4f95 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Quão ativamente você busca momentos divertidos e ocasiões alegres, como festas, comemorações, jogos e entretenimento?", "How important is laughing and making others laugh in your life?": "Quão importante é rir e fazer os outros rirem em sua vida?", "In your view, how essential is fun, play, and joy in living a good life?": "Na sua opinião, até que ponto a diversão, a brincadeira e a alegria são essenciais para viver uma vida boa?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Muito Baixo", + "Low": "Baixo", + "Moderate": "Moderado", + "High": "Alto", + "Very High": "Muito Alto", "1 child": "1 child", "1 to 3 years": "1 to 3 years", "160 to 170 cm": "160 to 170 cm", diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 99b96e2..37125b0 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Je, unatafuta kwa umakini kiasi gani nyakati za kufurahisha na matukio ya furaha kama vile karamu, sherehe, michezo na burudani?", "How important is laughing and making others laugh in your life?": "Je, kucheka na kufanya wengine kucheka kuna umuhimu gani katika maisha yako?", "In your view, how essential is fun, play, and joy in living a good life?": "Kwa maoni yako, furaha, kucheza na furaha ni muhimu kwa kiasi gani katika kuishi maisha mazuri?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Chini Sana", + "Low": "Chini", + "Moderate": "Wastani", + "High": "Juu", + "Very High": "Juu Sana", "Modern / Casual styling": "Modern / Casual styling", "Less than 1 year": "Less than 1 year", "More than 3 years later": "More than 3 years later", diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index a4f0a2d..4094938 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Шумо то чӣ андоза фаъолона вақтҳои шавқовар ва рӯйдодҳои хурсандиоварро ба монанди зиёфатҳо, ҷашнҳо, бозиҳо ва вақтхушиҳо меҷӯед?", "How important is laughing and making others laugh in your life?": "Ханда ва ханда кардани дигарон дар ҳаёти шумо то чӣ андоза муҳим аст?", "In your view, how essential is fun, play, and joy in living a good life?": "Ба назари шумо, барои зиндагии хуб лаззат бурдан, бозӣ кардан ва шодӣ кардан то чӣ андоза муҳим аст?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Хеле паст", + "Low": "Паст", + "Moderate": "Миёна", + "High": "Баланд", + "Very High": "Хеле баланд", "Modern / Casual styling": "Modern / Casual styling", "Less than 1 year": "Less than 1 year", "More than 3 years later": "More than 3 years later", diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index d0d8052..3d947a2 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "آپ پارٹیوں، تقریبات، گیمز اور تفریح ​​جیسے تفریحی اوقات اور خوشی کے مواقع کو کتنی سرگرمی سے تلاش کرتے ہیں؟", "How important is laughing and making others laugh in your life?": "آپ کی زندگی میں ہنسنا اور دوسروں کو ہنسانا کتنا ضروری ہے؟", "In your view, how essential is fun, play, and joy in living a good life?": "آپ کی نظر میں اچھی زندگی گزارنے کے لیے تفریح، کھیل اور خوشی کتنی ضروری ہے؟", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Bohat Kam", + "Low": "Kam", + "Moderate": "Darmiyana", + "High": "Zyada", + "Very High": "Bohat Zyada", "Modern / Casual styling": "Modern / Casual styling", "Less than 1 year": "Less than 1 year", "More than 3 years later": "More than 3 years later", diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 3162202..48ce59c 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "آپ پارٹیوں، تقریبات، گیمز اور تفریح ​​جیسے تفریحی اوقات اور خوشی کے مواقع کو کتنی سرگرمی سے تلاش کرتے ہیں؟", "How important is laughing and making others laugh in your life?": "آپ کی زندگی میں ہنسنا اور دوسروں کو ہنسانا کتنا ضروری ہے؟", "In your view, how essential is fun, play, and joy in living a good life?": "آپ کی نظر میں اچھی زندگی گزارنے کے لیے تفریح، کھیل اور خوشی کتنی ضروری ہے؟", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "بہت کم", + "Low": "کم", + "Moderate": "درمیانہ", + "High": "زیادہ", + "Very High": "بہت زیادہ", "Modern / Casual styling": "Modern / Casual styling", "Less than 1 year": "Less than 1 year", "More than 3 years later": "More than 3 years later", diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index 59668a8..7c0d6d4 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -1757,11 +1757,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "Bayramlar, bayramlar, o'yinlar va o'yin-kulgilar kabi qiziqarli vaqtlar va quvonchli vaziyatlarni qanchalik faol qidirasiz?", "How important is laughing and making others laugh in your life?": "Sizning hayotingizda kulish va boshqalarni kuldirish qanchalik muhim?", "In your view, how essential is fun, play, and joy in living a good life?": "Sizningcha, yaxshi hayot kechirish uchun o'yin-kulgi, o'yin va quvonch qanchalik muhim?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "Juda past", + "Low": "Past", + "Moderate": "O'rtacha", + "High": "Yuqori", + "Very High": "Juda yuqori", "Modern / Casual styling": "Modern / Casual styling", "Less than 1 year": "Less than 1 year", "More than 3 years later": "More than 3 years later", diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index 86d7095..47e072b 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -1491,11 +1491,11 @@ "How actively do you seek fun times and joyous occasions like parties, celebrations, games, and entertainment?": "您如何积极地寻求聚会、庆祝活动、游戏和娱乐等有趣的时光和欢乐场合?", "How important is laughing and making others laugh in your life?": "笑和让别人笑在你的生活中有多重要?", "In your view, how essential is fun, play, and joy in living a good life?": "在您看来,乐趣、玩耍和快乐对于美好生活有多重要?", - "Very Low": "Very Low", - "Low": "Low", - "Moderate": "Moderate", - "High": "High", - "Very High": "Very High", + "Very Low": "极低", + "Low": "较低", + "Moderate": "中等", + "High": "较高", + "Very High": "极高", "1 child": "1 名儿童", "1 to 3 years": "1至3年", "160 to 170 cm": "160 至 170 厘米", From 24f332c064cc68b0f2c4da024a910ab5063391b6 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Sat, 5 Sep 2026 10:52:57 +0000 Subject: [PATCH 05/27] fix(build): resolve TypeScript errors in preview-upload and question-detail-client, track deployed commit in auto_deploy_staging --- .gitignore | 1 + auto_deploy_staging.sh | 9 +++++++++ src/app/[lang]/preview-upload/page.tsx | 19 +++++++------------ src/app/preview-upload/page.tsx | 7 ++++++- .../[slug]/question-detail-client.tsx | 7 ++++--- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 82d94f5..a0a7c90 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.last_deployed_commit diff --git a/auto_deploy_staging.sh b/auto_deploy_staging.sh index ab3777a..2c1a43d 100755 --- a/auto_deploy_staging.sh +++ b/auto_deploy_staging.sh @@ -22,8 +22,16 @@ MASTER_COMMITS=$(git rev-list --count HEAD..origin/master 2>/dev/null || echo 0) # Check 2: Commits ahead on origin/staging STAGING_COMMITS=$(git rev-list --count HEAD..origin/staging 2>/dev/null || echo 0) +DEPLOYED_COMMIT_FILE="$PROJECT_DIR/.last_deployed_commit" +LAST_DEPLOYED=$(cat "$DEPLOYED_COMMIT_FILE" 2>/dev/null || echo "") +CURRENT_HEAD=$(git rev-parse HEAD) + NEED_BUILD=0 +if [ "$CURRENT_HEAD" != "$LAST_DEPLOYED" ]; then + NEED_BUILD=1 +fi + if [ "$MASTER_COMMITS" -gt 0 ]; then echo "$LOG_TAG 🚀 Detected $MASTER_COMMITS new commit(s) on origin/master!" git log --oneline HEAD..origin/master @@ -57,6 +65,7 @@ sleep 5 HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5561/healthz || echo "000") if [ "$HTTP_STATUS" -eq 200 ]; then + echo "$(git rev-parse HEAD)" > "$DEPLOYED_COMMIT_FILE" echo "$LOG_TAG ✅ SUCCESS: Marriage staging updated and healthy at commit $(git rev-parse --short HEAD) (HTTP 200 OK)." else echo "$LOG_TAG ⚠️ Health probe returned HTTP status $HTTP_STATUS" diff --git a/src/app/[lang]/preview-upload/page.tsx b/src/app/[lang]/preview-upload/page.tsx index 5fa986a..0c876e9 100644 --- a/src/app/[lang]/preview-upload/page.tsx +++ b/src/app/[lang]/preview-upload/page.tsx @@ -17,25 +17,20 @@ const mockQuestion: QuestionField = { description: "", tooltip: "Supporting documents for identity verification", extras: { + placeHolder: "", + range: [0, 0], options: [".pdf", ".jpg", ".jpeg", ".png"], }, options: [], }; export default function PreviewUploadPage() { - const initialAnswers = useMemo( - () => ({ - "documents_verification.other_supporting_documents": [ - "/media/tmp/MKROrQ-Screenshot_2026.png", - "/media/tmp/0UKhA-Screenshot_2026.png", - ], - }), - [], - ); - return ( - - + +
{/* Top Bar */}
diff --git a/src/app/preview-upload/page.tsx b/src/app/preview-upload/page.tsx index ed33874..031aef4 100644 --- a/src/app/preview-upload/page.tsx +++ b/src/app/preview-upload/page.tsx @@ -17,6 +17,8 @@ const mockQuestion: QuestionField = { description: "", tooltip: "Supporting documents for identity verification", extras: { + placeHolder: "", + range: [0, 0], options: [".pdf", ".jpg", ".jpeg", ".png"], }, options: [], @@ -38,7 +40,10 @@ const mockDocs = [ export default function RootPreviewUploadPage() { return ( - +
{/* Top Bar */}
diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index d14beb3..4533518 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -464,6 +464,7 @@ export default function QuestionDetailClient({ "Very High", ]; const defaultLabels = locale === "fa" ? DEFAULT_LABELS_FA : DEFAULT_LABELS_EN; + const tMap = t as Record; return questionsList .filter((q: any) => q && (q.question_number || q.id) && q.text) @@ -474,9 +475,9 @@ export default function QuestionDetailClient({ ? rawOptions.map((opt: any, idx: number) => { const score = idx + 1; if (typeof opt === "string") { - return { id: String(score), value: score, label: t[opt] || opt }; + return { id: String(score), value: score, label: tMap[opt] || opt }; } - const defaultLabel = t[DEFAULT_LABELS_EN[idx]] || defaultLabels[idx] || String(score); + const defaultLabel = tMap[DEFAULT_LABELS_EN[idx]] || defaultLabels[idx] || String(score); return { id: String(opt.id || opt.value || score), value: typeof opt.value === "number" ? opt.value : score, @@ -488,7 +489,7 @@ export default function QuestionDetailClient({ : [1, 2, 3, 4, 5].map((score, idx) => ({ id: String(score), value: score, - label: t[DEFAULT_LABELS_EN[idx]] || defaultLabels[idx] || String(score), + label: tMap[DEFAULT_LABELS_EN[idx]] || defaultLabels[idx] || String(score), })); return { From d13c6810adbe324844e3b522f92a8ef62551ed31 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 5 Sep 2026 17:07:13 +0330 Subject: [PATCH 06/27] feat: add LoadingBorderSpinner component, implement multi-language support, and introduce new match client workflows. --- .../finding-match/finding-match-client.tsx | 10 +- src/app/intro/intro-client.test.tsx | 28 +- src/app/intro/intro-client.tsx | 5 +- src/app/layout.tsx | 24 ++ src/app/new-match/new-match-client.tsx | 202 ++---------- src/app/new-match/profile/page.tsx | 138 ++++---- .../request-accepted-client.tsx | 263 +++++++++------ .../Componentes/dismiss-reason-sheet.tsx | 7 +- src/components/Componentes/swipe-button.tsx | 30 +- src/components/ui/loading-border-spinner.tsx | 49 +++ src/hooks/marriage/types.ts | 2 +- src/hooks/marriage/use-case-respond.ts | 2 +- src/icons.tsx | 1 + src/lib/auth-bridge.test.ts | 26 ++ src/lib/auth-bridge.ts | 77 +++++ src/lib/marriage-field-formatter.ts | 301 ++++++++++++++++++ src/translations/locales/ar.json | 23 +- src/translations/locales/az.json | 29 +- src/translations/locales/bn.json | 21 +- src/translations/locales/da.json | 33 +- src/translations/locales/de.json | 31 +- src/translations/locales/en.json | 36 ++- src/translations/locales/es.json | 29 +- src/translations/locales/fa.json | 22 +- src/translations/locales/fr.json | 35 +- src/translations/locales/gu.json | 31 +- src/translations/locales/ha.json | 35 +- src/translations/locales/he.json | 31 +- src/translations/locales/hi.json | 29 +- src/translations/locales/id.json | 31 +- src/translations/locales/ks.json | 35 +- src/translations/locales/pt.json | 35 +- src/translations/locales/ru.json | 29 +- src/translations/locales/sw.json | 25 +- src/translations/locales/tg.json | 31 +- src/translations/locales/tr.json | 35 +- src/translations/locales/ul.json | 35 +- src/translations/locales/ur.json | 35 +- src/translations/locales/uz.json | 25 +- src/translations/locales/zh.json | 35 +- 40 files changed, 1279 insertions(+), 622 deletions(-) create mode 100644 src/components/ui/loading-border-spinner.tsx diff --git a/src/app/finding-match/finding-match-client.tsx b/src/app/finding-match/finding-match-client.tsx index e8b0b6d..48cc5be 100644 --- a/src/app/finding-match/finding-match-client.tsx +++ b/src/app/finding-match/finding-match-client.tsx @@ -195,15 +195,17 @@ export default function FindingMatchClient() { id="rejection-notice-title" className="text-[16px] font-bold leading-[1.3] text-[#171717]" > - {t["Your request was rejected"]} + {t["Your request was declined"] || + t["Your request was rejected"]}

- { + {t[ + "Your request was declined by the lady. You will be introduced to other candidates in the future." + ] || t[ "Your request was rejected by the lady. You will be introduced to other candidates in the future." - ] - } + ]}

diff --git a/src/app/intro/intro-client.test.tsx b/src/app/intro/intro-client.test.tsx index ab1a1d3..22f6982 100644 --- a/src/app/intro/intro-client.test.tsx +++ b/src/app/intro/intro-client.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, cleanup } from "@testing-library/react"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi, afterEach } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import IntroClient from "./intro-client"; @@ -52,6 +52,18 @@ vi.mock("@/hooks/use-hardware-back-handler", () => ({ useHardwareBackHandler: vi.fn(), })); +vi.mock("@/components/Componentes/slider-page", () => ({ + default: () =>
Slider Content
, +})); + +vi.mock("@/lib/auth-bridge", () => ({ + authBridge: { + isAuthenticated: vi.fn(() => false), + ensureToken: vi.fn(() => Promise.resolve(null)), + getToken: vi.fn(() => null), + }, +})); + describe("IntroClient", () => { afterEach(() => { cleanup(); @@ -73,4 +85,18 @@ describe("IntroClient", () => { // Video thumbnail image should NOT be rendered expect(screen.queryByAltText("video")).not.toBeInTheDocument(); }); + + it("opens onboarding steps when Submit is clicked even if user has no auth token", async () => { + const queryClient = new QueryClient(); + render( + + + , + ); + + const submitButton = screen.getByText("Submit"); + fireEvent.click(submitButton); + + expect(await screen.findByTestId("slider-page")).toBeInTheDocument(); + }); }); diff --git a/src/app/intro/intro-client.tsx b/src/app/intro/intro-client.tsx index f342b90..7fd3d68 100644 --- a/src/app/intro/intro-client.tsx +++ b/src/app/intro/intro-client.tsx @@ -96,7 +96,10 @@ export default function IntroClient() { if (!authBridge.isAuthenticated()) { const token = await authBridge.ensureToken(); if (!token) { - console.warn("No token from bridge – login was not completed"); + console.warn( + "No token from bridge – opening intro onboarding steps", + ); + handleOpenSteps(); return; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ce81bfa..2752b60 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -226,6 +226,17 @@ export default async function RootLayout({ }); } + // Check URL query parameters for auth token on initial script execution + try { + if (window.location && window.location.search) { + var searchParams = new URLSearchParams(window.location.search); + var urlTok = searchParams.get('token') || searchParams.get('auth_token') || searchParams.get('habib_token') || searchParams.get('HABIB_TOKEN'); + if (urlTok && urlTok.trim() !== '' && urlTok !== 'NO_TOKEN') { + window.HABIB_TOKEN = urlTok.trim(); + } + } + } catch (e) {} + if (!Object.getOwnPropertyDescriptor(window, 'HABIB_COINS')) { Object.defineProperty(window, 'HABIB_COINS', { configurable: true, @@ -253,6 +264,19 @@ export default async function RootLayout({ if (!config) return; configApplied = true; window.__HABIB_BOOTSTRAP__ = config; + + var configToken = + config.token || + config.auth_token || + config.authToken || + (config.data && (config.data.token || config.data.auth_token || config.data.authToken)) || + (config.payload && (config.payload.token || config.payload.auth_token || config.payload.authToken)); + + if (configToken && typeof configToken === 'string' && configToken.trim() !== '' && configToken !== 'NO_TOKEN') { + console.log('⚡ [Layout Bootstrap] Applying auth token from Flutter bootstrap config'); + window.HABIB_TOKEN = configToken.trim(); + } + var marriageData = config.marriage || config.marriageData || diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index d841eaf..f228e10 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -176,8 +176,10 @@ import { formatFieldLabel, formatFieldValue, formatOptionValue, + getOrderedSummaryFields, isMarriagePhoneFieldValue, titleFromKey, + type SummaryFieldItem, } from "@/lib/marriage-field-formatter"; function toDisplayField( @@ -267,143 +269,19 @@ function useMatchSummaryDisplay( displayName = `Profile #${matchSummary.id}`; } - // 2. Age (extract and calculate from date_of_birth if available) - const dobIdx = fields.findIndex( - (f) => - f.key === "personal_identity.date_of_birth" || - f.key?.endsWith(".date_of_birth") || - f.key?.toLowerCase().includes("date_of_birth") || - f.key?.toLowerCase().includes("birth_date"), - ); - let age: DisplayField | null = null; - if (dobIdx !== -1 && fields[dobIdx].value) { - usedIndexes.add(dobIdx); - const calculatedAge = calculateAgeFromDob(String(fields[dobIdx].value)); - if (calculatedAge) { - age = { - id: fields[dobIdx].key, - label: t ? t["Age"] || "Age" : "Age", - value: `${calculatedAge}`, - }; - } - } - if (!age) { - age = pickField(fields, fieldCandidateMatchers.age, usedIndexes, t); - } - - // 3. Country of Current Residence - let currentCountry = pickField( - fields, - fieldCandidateMatchers.currentCountry, - usedIndexes, - t, - ); - - // 4. City / State of Current Residence - let currentCity = pickField( - fields, - fieldCandidateMatchers.currentCity, - usedIndexes, - t, - ); - - // If either country or city was not matched as a standalone field, check composite residence - if (!currentCountry || !currentCity) { - const residenceIdx = fields.findIndex( - (f, idx) => - !usedIndexes.has(idx) && - matchesCandidate(f, fieldCandidateMatchers.residence), - ); - - if (residenceIdx !== -1) { - const residenceField = fields[residenceIdx]; - if ( - typeof residenceField.value === "object" && - residenceField.value !== null && - !Array.isArray(residenceField.value) - ) { - const valObj = residenceField.value as { - country?: string; - city?: string; - state?: string; - }; - if (valObj.country && !currentCountry) { - currentCountry = { - id: `${residenceField.key}.country`, - label: - (t && - (t["Current Country of Residence"] || - t["Country of Residence"] || - t["Country"])) || - "Country of Residence", - value: formatOptionValue(valObj.country, t) || valObj.country, - }; - } - const cityVal = [valObj.city, valObj.state].filter(Boolean).join(", "); - if (cityVal && !currentCity) { - currentCity = { - id: `${residenceField.key}.city`, - label: - (t && - (t["Current City / State of Residence"] || - t["City / State of Residence"] || - t["City"])) || - "City / State of Residence", - value: formatOptionValue(cityVal, t) || cityVal, - }; - } - usedIndexes.add(residenceIdx); - } else if (!currentCountry && !currentCity) { - const disp = toDisplayField(residenceField, t); - if (disp) { - usedIndexes.add(residenceIdx); - currentCountry = disp; - } - } - } - } - - // 5. Highest level of education - const educationLevel = pickField( - fields, - fieldCandidateMatchers.educationLevel, - usedIndexes, - t, - ); - - // 6. Field of study - const fieldOfStudy = pickField( - fields, - fieldCandidateMatchers.fieldOfStudy, - usedIndexes, - t, - ); - - // 7. Job Title - const jobTitle = pickField( - fields, - fieldCandidateMatchers.jobTitle, - usedIndexes, - t, - ); - - // 8. Hobbies & Main Interests - const hobbies = pickField( - fields, - fieldCandidateMatchers.hobbies, - usedIndexes, - t, - ); + // 7 ordered summary items: + // 1. Place of Birth (Country and City) + // 2. Current Place of Residence (Country, City / State) + // 3. Height in Centimeters + // 4. What is your highest completed formal educational degree? + // 5. Field of Study + // 6. What is your current employment status? + // 7. Job Title and Field of Activity + const items = getOrderedSummaryFields(fields, t); return { name: displayName, - age, - currentCountry, - currentCity, - educationLevel, - fieldOfStudy, - jobTitle, - hobbies, + items, }; }, [matchSummary, t]); } @@ -817,54 +695,13 @@ export default function NewMatchClient() { {/* Info Items List */}
- {matchDisplay.age && ( + {matchDisplay.items.map((item) => ( } + key={item.id} + field={item} + icon={} /> - )} - - {matchDisplay.currentCountry && ( - } - /> - )} - - {matchDisplay.currentCity && ( - } - /> - )} - - {matchDisplay.educationLevel && ( - } - /> - )} - - {matchDisplay.fieldOfStudy && ( - } - /> - )} - - {matchDisplay.jobTitle && ( - } - /> - )} - - {matchDisplay.hobbies && ( - } - /> - )} + ))} {/* Button */}
)} - onClose={() => setIsRejectSheetOpen(false)} + onClose={() => setIsDeclineSheetOpen(false)} /> ) : null} - {isMaleRejectWarningOpen ? ( + {isMaleDeclineWarningOpen ? ( - {t.Reject} + {t.Decline || t["Decline"] || t.Reject}
)} - onClose={() => setIsMaleRejectWarningOpen(false)} + onClose={() => setIsMaleDeclineWarningOpen(false)} /> ) : null} {isDismissReasonSheetOpen ? ( @@ -683,7 +671,6 @@ export default function NewMatchProfilePage({ @@ -692,7 +679,10 @@ export default function NewMatchProfilePage({ style={{ paddingBottom: `calc(16px + var(--safe-bottom, 0px))` }} className="shrink-0 z-30 w-full rounded-t-[24px] bg-white px-[17px] pt-4 shadow-[0_-4px_24px_rgba(0,0,0,0.08)]" > - {caseStatus === "payment_done" || + {(Boolean(onClose) && !isAcceptProfileEnabled) || + caseStatus === "female_accepted" || + caseStatus === "payment_pending" || + caseStatus === "payment_done" || caseStatus === "contacted" || caseStatus === "finalized" || profile?.status === "matched" ? ( @@ -707,17 +697,17 @@ export default function NewMatchProfilePage({
+ onSuccess={async () => { + try { + await handleNoContactReport(); + setIsNoContactConfirmOpen(false); + } catch (err) { + console.error("Failed to report no contact", err); + } + }} + /> } - onClose={() => setIsNoContactConfirmOpen(false)} - closeOnOutside={true} + onClose={() => { + if (!contactStatusMutation.isPending) { + setIsNoContactConfirmOpen(false); + } + }} + closeOnOutside={!contactStatusMutation.isPending} /> ) : null} @@ -670,19 +694,19 @@ export default function RequestAcceptedClient() {

)}
+ ) : noContactReportedSuccess ? ( +

+ {t["Your report has been submitted to support."]} +

) : (

- {noContactReportedSuccess + {isFemaleProfile ? t[ - "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." + "The selected candidate will contact your family shortly." ] - : isFemaleProfile - ? t[ - "The selected candidate will contact your family shortly." - ] - : t[ - "You can now view their family's contact details and arrange further steps." - ]} + : t[ + "You can now view their family's contact details and arrange further steps." + ]}

)} @@ -693,18 +717,33 @@ export default function RequestAcceptedClient() {
{isFemaleProfile && contactStatusMutation.isPending ? null : isFemaleProfile ? ( - + <> + + + + ) : ( <> @@ -735,70 +774,116 @@ export default function RequestAcceptedClient() { )}
+ ) : noContactReportedSuccess ? ( +
+
+
+ +
+

+ {t["Report Registered"] || "Report Registered"} +

+

+ {t[ + "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." + ]} +

+
+ + +
) : ( -
- {isFemaleProfile ? ( - - ) : ( +
+ {isFemaleProfile && ( )} - ) : ( -
- {paymentMutation.isPending || isFetchingContact ? ( - +
+ )} - + + +
)} diff --git a/src/components/Componentes/dismiss-reason-sheet.tsx b/src/components/Componentes/dismiss-reason-sheet.tsx index 789ad7f..13b8462 100644 --- a/src/components/Componentes/dismiss-reason-sheet.tsx +++ b/src/components/Componentes/dismiss-reason-sheet.tsx @@ -146,11 +146,12 @@ export function DismissReasonSheet({

- { + {t[ + "Please provide the full reason for declining the submitted item" + ] || t[ "Please provide the full reason for rejecting the submitted item" - ] - } + ]}

void; + onSuccess: () => void | Promise; onCancel?: () => void; text: string; cancelText?: string; disabled?: boolean; isLoading?: boolean; + isSubmitting?: boolean; theme?: "default" | "green"; }; @@ -22,10 +23,11 @@ export function SwipeButton({ cancelText, disabled = false, isLoading = false, + isSubmitting = false, theme = "default", }: SwipeButtonProps) { const { dictionary: t } = useI18n(); - const [clicked, setClicked] = useState(false); + const [internalSubmitting, setInternalSubmitting] = useState(false); if (isLoading) { if (onCancel) { @@ -43,9 +45,16 @@ export function SwipeButton({ ); } - const handleClick = () => { - setClicked(true); - onSuccess(); + const busy = isSubmitting || internalSubmitting; + + const handleClick = async () => { + if (disabled || busy) return; + setInternalSubmitting(true); + try { + await onSuccess(); + } finally { + setInternalSubmitting(false); + } }; const cancelLabel = cancelText || t?.["Cancel"] || "Cancel"; @@ -55,12 +64,12 @@ export function SwipeButton({ const actionButton = (
From e90d78a2d333ba82e4235d1de75396d76e398985 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sun, 6 Sep 2026 16:07:53 +0330 Subject: [PATCH 10/27] fix(webview): eliminate stale HTML caching, fix bootstrap locale routing, add client error logging bridge, and remove overlay barrier on mobile --- next.config.ts | 12 +++++++-- src/app/globals.css | 2 ++ src/app/layout.tsx | 27 +++++++++++++++++-- .../request-accepted-client.tsx | 18 ------------- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/next.config.ts b/next.config.ts index 8439580..67fa43e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -86,12 +86,20 @@ const nextConfig: NextConfig = { ], }, { - // Application HTML shell – App Shell + Stale-While-Revalidate pattern for instant WebView rendering + // Application HTML pages – must never serve stale HTML so hashed JS chunks match current deployment source: "/:path((?!api/|_next/|fonts/|assets/).*)", headers: [ { key: "Cache-Control", - value: "public, max-age=0, stale-while-revalidate=86400", + value: "no-store, no-cache, must-revalidate, max-age=0", + }, + { + key: "Pragma", + value: "no-cache", + }, + { + key: "Expires", + value: "0", }, ], }, diff --git a/src/app/globals.css b/src/app/globals.css index 4f8f63c..02514ce 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -265,6 +265,8 @@ a, label, [role="button"] { touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + pointer-events: auto; } input, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2752b60..04c0a99 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -304,9 +304,10 @@ export default async function RootLayout({ document.cookie = 'habib_language=' + encodeURIComponent(String(code)) + '; Path=/; Max-Age=31536000; SameSite=Lax' + secure; root.lang = String(code); root.dir = (locale.isRTL || locale.isRtl || locale.is_rtl) ? 'rtl' : 'ltr'; + var knownLocales = ['fa', 'ar', 'en', 'ur', 'tr', 'ru', 'fr', 'es', 'de', 'id', 'bn', 'hi', 'pt', 'ha', 'sw', 'uz', 'az', 'tg', 'ks', 'zh', 'da', 'he', 'gu', 'ul']; var segments = window.location.pathname.split('/'); var currentCode = segments[1]; - if (currentCode && currentCode !== String(code)) { + if (currentCode && knownLocales.indexOf(currentCode) !== -1 && currentCode !== String(code)) { root.dataset.webBootstrap = 'pending'; segments[1] = String(code); window.location.replace(segments.join('/') + window.location.search + window.location.hash); @@ -317,6 +318,28 @@ export default async function RootLayout({ window.dispatchEvent(new CustomEvent('habib:bootstrap-ready', { detail: config })); } + // Global Error Forwarding to Flutter Native Log Channel + function forwardErrorToFlutter(type, err) { + try { + var message = '[WEB_ERROR] ' + type + ': ' + (err && (err.stack || err.message || String(err)) || 'Unknown error'); + console.error(message); + if (window.HabibApp && window.HabibApp.postMessage) { + window.HabibApp.postMessage(JSON.stringify({ + action: 'client_log', + message: message + })); + } + } catch (e) {} + } + + window.addEventListener('error', function(event) { + forwardErrorToFlutter('Runtime Error', event.error || event.message); + }); + + window.addEventListener('unhandledrejection', function(event) { + forwardErrorToFlutter('Unhandled Rejection', event.reason); + }); + if (window.__HABIB_BOOTSTRAP__) { apply(window.__HABIB_BOOTSTRAP__); } @@ -415,7 +438,7 @@ export default async function RootLayout({ var msg = '[WEB_DEEP_LIFECYCLE] [' + Date.now() + '] Event: ' + name + ' | visibility: ' + document.visibilityState + ' | hasFocus: ' + document.hasFocus() + ' | bodyBg: ' + bodyBg + ' | htmlBg: ' + htmlBg + (detail ? ' | ' + JSON.stringify(detail) : ''); console.log(msg); if (window.HabibApp && window.HabibApp.postMessage) { - window.HabibApp.postMessage(JSON.stringify({ action: 'debug_log', message: msg })); + window.HabibApp.postMessage(JSON.stringify({ action: 'client_log', message: msg })); } } catch (e) {} } diff --git a/src/app/request-accepted/request-accepted-client.tsx b/src/app/request-accepted/request-accepted-client.tsx index 4133fb2..dd1e423 100644 --- a/src/app/request-accepted/request-accepted-client.tsx +++ b/src/app/request-accepted/request-accepted-client.tsx @@ -972,25 +972,7 @@ export default function RequestAcceptedClient() { onGetAdvisor={openAdvisors} /> -
-
-
- lock -

- {t["Profile is locked"]} -

-
-
-
From 1d35fd7307ec7ca3d392c7c81b8d23f2cbc16e6f Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sun, 6 Sep 2026 17:13:19 +0330 Subject: [PATCH 11/27] feat: implement request accepted client view with contact information display and interaction components --- .../request-accepted-client.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/app/request-accepted/request-accepted-client.tsx b/src/app/request-accepted/request-accepted-client.tsx index dd1e423..4868714 100644 --- a/src/app/request-accepted/request-accepted-client.tsx +++ b/src/app/request-accepted/request-accepted-client.tsx @@ -23,6 +23,7 @@ import PageHeader from "@/components/Componentes/page-header"; import SubscriptionRequiredSheet from "@/components/Componentes/subscription-required-sheet"; import SwipeButton from "@/components/Componentes/swipe-button"; import { LoadingBorderSpinner } from "@/components/ui/loading-border-spinner"; +import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import type { MarriageField, MarriagePhoneFieldValue, @@ -223,6 +224,18 @@ function getContactInfoPhoneItems( .filter((item): item is ContactInfoPhoneItem => item !== null); } +function ContactInfoSkeleton() { + return ( +
+
+ + +
+ +
+ ); +} + function ContactInfoPhoneCard({ item, copyText, @@ -614,11 +627,8 @@ export default function RequestAcceptedClient() { } buttons={ (contactInfoQuery.isLoading || isFetchingContact) && !contactInfoPhoneItems.length ? ( -
- - - {t["Loading..."]} - +
+
) : contactInfoPhoneItems.length ? (
From ce10e4149486c53da72f49505bd886afa2b79107 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Mon, 7 Sep 2026 16:01:44 +0330 Subject: [PATCH 12/27] feat: implement marriage outcome reporting flow with internationalization and testing support --- AGENTS.md | 3 + .../candidate-contact-client.tsx | 58 +- .../request-accepted-client.test.tsx | 342 ++++++++++++ .../request-accepted-client.tsx | 504 +++++++++++------- .../Componentes/female-outcome-sheet.test.tsx | 28 +- .../Componentes/female-outcome-sheet.tsx | 66 ++- .../Componentes/navigation-button.tsx | 22 +- src/components/Componentes/support-access.ts | 27 + src/components/Componentes/swipe-button.tsx | 10 +- .../Componentes/test-completed-sheet.tsx | 1 + src/hooks/marriage/types.ts | 5 + src/translations/locales/ar.json | 26 +- src/translations/locales/az.json | 22 +- src/translations/locales/bn.json | 26 +- src/translations/locales/da.json | 64 ++- src/translations/locales/de.json | 62 ++- src/translations/locales/en.json | 9 +- src/translations/locales/es.json | 62 ++- src/translations/locales/fa.json | 19 +- src/translations/locales/fr.json | 62 ++- src/translations/locales/gu.json | 62 ++- src/translations/locales/ha.json | 64 ++- src/translations/locales/he.json | 24 +- src/translations/locales/hi.json | 62 ++- src/translations/locales/id.json | 24 +- src/translations/locales/ks.json | 24 +- src/translations/locales/pt.json | 36 +- src/translations/locales/ru.json | 20 +- src/translations/locales/sw.json | 24 +- src/translations/locales/tg.json | 24 +- src/translations/locales/tr.json | 24 +- src/translations/locales/ul.json | 24 +- src/translations/locales/ur.json | 24 +- src/translations/locales/uz.json | 24 +- src/translations/locales/zh.json | 64 ++- 35 files changed, 1323 insertions(+), 619 deletions(-) create mode 100644 src/app/request-accepted/request-accepted-client.test.tsx diff --git a/AGENTS.md b/AGENTS.md index 4f53884..de6c274 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,4 +11,7 @@ This version has breaking changes — APIs, conventions, and file structure may - CRITICAL AUTOMATION RULE: Whenever any new static text, data, field, component label, tooltip, help description, key, or text item is added or updated in the project, its translation key and values MUST automatically be added/updated in ALL 24 locale JSON files (`src/translations/locales/` & `src/data/questions/`). - QUESTION HELP & TOOLTIPS RULE: All question help texts, tooltips, and modal descriptions must be stored with key-value entries in the respective locale JSON files, ensuring complete synchronization across all supported languages. +# Database Source-of-Truth & API Target Protection +- فرانت همواره باید به بک‌اند محلی (`http://127.0.0.1:8000`) که به دیتابیس `najm2` (سرور staging) متصل است وصل باشد. تغییر آدرس و تارگت اتصال دیتابیس/API به سرورهای دیگر تنها با اجازه صریح کاربر مجاز است، اما اعمال تغییرات در بک‌اند یا دیتابیس نیازی به اجازه ندارد (Ask for permission ONLY when you want to change the address of database to a different one; changes in backend or database are OK and do not need permission). + diff --git a/src/app/candidate-contact/candidate-contact-client.tsx b/src/app/candidate-contact/candidate-contact-client.tsx index fa0d9e2..df9ae51 100644 --- a/src/app/candidate-contact/candidate-contact-client.tsx +++ b/src/app/candidate-contact/candidate-contact-client.tsx @@ -77,6 +77,41 @@ export default function CandidateContactClient() { const isFinalized = caseStatus === "finalized" || profile?.status === "matched"; + const contactSharedAtStr = + profile?.active_case?.contact_shared_at || + (profile?.active_case as any)?.payment_done_at; + + const FORTY_EIGHT_HOURS_MS = 48 * 60 * 60 * 1000; + + const [currentTime, setCurrentTime] = useState(() => Date.now()); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(Date.now()); + }, 15000); + return () => clearInterval(interval); + }, []); + + const contactSharedTimestamp = useMemo(() => { + if (!contactSharedAtStr) return null; + const parsed = new Date(contactSharedAtStr).getTime(); + return isNaN(parsed) ? null : parsed; + }, [contactSharedAtStr]); + + const canReportNoContact = useMemo(() => { + if (profile?.active_case?.can_report_no_contact) { + return true; + } + if (contactSharedTimestamp) { + return currentTime - contactSharedTimestamp >= FORTY_EIGHT_HOURS_MS; + } + return false; + }, [ + profile?.active_case?.can_report_no_contact, + contactSharedTimestamp, + currentTime, + ]); + const contactStatusMutation = useSubmitMarriageContactStatusMutation( caseId ?? "", ); @@ -84,7 +119,7 @@ export default function CandidateContactClient() { const outcomeMutation = useSubmitMarriageOutcomeMutation(caseId ?? ""); const handleNoContactReport = async () => { - if (!caseId || contactStatusMutation.isPending) return; + if (!caseId || contactStatusMutation.isPending || !canReportNoContact) return; await contactStatusMutation.mutateAsync({ action: "no_contact", custom_note: @@ -224,9 +259,24 @@ export default function CandidateContactClient() {
+ + +
+ ) : caseStatus === "contacted" || isFemaleContactConfirmed || (isFemaleProfile && contactStatusMutation.isPending) ? (
{isFemaleProfile && - contactStatusMutation.isPending ? null : isFemaleProfile ? ( - <> - - - - - ) : ( + contactStatusMutation.isPending ? null : ( <> @@ -899,12 +997,17 @@ export default function RequestAcceptedClient() { {isFemaleProfile ? ( - ), -})); - function renderSheet(onSubmit = vi.fn()) { render( @@ -48,7 +32,11 @@ describe("FemaleOutcomeSheet", () => { it("requires a cancellation reason and submits it as a failure", async () => { const onSubmit = renderSheet(); - await userEvent.click(screen.getByLabelText("Canceled")); + await userEvent.click( + screen.getByLabelText( + /We did not reach the agreement needed to continue acquaintance/i, + ), + ); const confirmation = screen.getByRole("button", { name: "Confirm", @@ -67,7 +55,11 @@ describe("FemaleOutcomeSheet", () => { it("requires a written note when the other cancellation reason is selected", async () => { renderSheet(); - await userEvent.click(screen.getByLabelText("Canceled")); + await userEvent.click( + screen.getByLabelText( + /We did not reach the agreement needed to continue acquaintance/i, + ), + ); await userEvent.click( screen.getByRole("button", { name: "Other reasons" }), ); diff --git a/src/components/Componentes/female-outcome-sheet.tsx b/src/components/Componentes/female-outcome-sheet.tsx index ee698d2..dda9a37 100644 --- a/src/components/Componentes/female-outcome-sheet.tsx +++ b/src/components/Componentes/female-outcome-sheet.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; -import SwipeButton from "./swipe-button"; const EXIT_ANIMATION_MS = 200; @@ -100,7 +99,7 @@ export function FemaleOutcomeSheet({ return null; } - const isCanceledSwipeDisabled = + const isCanceledDisabled = selectedReasons.length === 0 || (selectedReasons.includes(reasons[reasons.length - 1]) && reasonText.trim() === ""); @@ -169,7 +168,7 @@ export function FemaleOutcomeSheet({ className={[ "flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-left border transition-all", outcome === "ongoing" - ? "bg-[#E8F8F3] border-[#00AC78]/30" + ? "bg-[#FFECEF] border-[#F0445B]/30" : "bg-[#ECECEC] border-transparent", ].join(" ")} > @@ -188,12 +187,12 @@ export function FemaleOutcomeSheet({ className={[ "flex h-6 w-6 shrink-0 items-center justify-center rounded-full border transition-colors", outcome === "ongoing" - ? "border-[#00AC78] bg-transparent" + ? "border-[#F0445B] bg-transparent" : "border-[#9E9E9E] bg-transparent", ].join(" ")} > {outcome === "ongoing" ? ( - + ) : null} @@ -205,7 +204,7 @@ export function FemaleOutcomeSheet({ - {/* Option B: Canceled */} + {/* Option B: Canceled / Decline */} @@ -339,35 +349,45 @@ export function FemaleOutcomeSheet({
- {/* SwipeButton Confirmation Area */} -
+ {/* Buttons Area */} +
+ + {outcome === "ongoing" ? ( - { + ) : ( - { + )}
diff --git a/src/components/Componentes/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx index 4d452a0..35bddcc 100644 --- a/src/components/Componentes/navigation-button.tsx +++ b/src/components/Componentes/navigation-button.tsx @@ -10,7 +10,7 @@ import { isInFlutterWebView } from "@/lib/webview-actions"; import { useI18n } from "@/translations/provider"; import HelpModal from "./help-modal"; import InformationSheet from "./information-sheet"; -import { hasSupportAccess } from "./support-access"; +import { hasSupportAccess, openDirectSupportContact } from "./support-access"; import SupportSheet from "./support-sheet"; import TermsSheet from "./terms-sheet"; import { UiIcon } from "./ui-icon"; @@ -89,25 +89,7 @@ export function NavigationButton({ profile.active_subscription.is_valid !== false; const handleDirectSupportContact = () => { - if (typeof window !== "undefined" && "HabibApp" in window) { - try { - const app = ( - window as Window & { - HabibApp?: { postMessage: (msg: string) => void }; - } - ).HabibApp; - app?.postMessage( - JSON.stringify({ - action: "open_consultant_page", - data: { consultant: "habib@gmail.com" }, - }), - ); - } catch (err) { - console.error("Error calling HabibApp bridge", err); - } - } else if (typeof window !== "undefined") { - window.open("mailto:habib@gmail.com", "_blank"); - } + openDirectSupportContact(); }; const handleOpenSubscriptionModal = () => { diff --git a/src/components/Componentes/support-access.ts b/src/components/Componentes/support-access.ts index c15d30f..7f155d7 100644 --- a/src/components/Componentes/support-access.ts +++ b/src/components/Componentes/support-access.ts @@ -6,3 +6,30 @@ import type { MarriageProfile } from "@/hooks/marriage/types"; export function hasSupportAccess(profile?: MarriageProfile): boolean { return true; } + +/** + * Triggers direct support contact via HabibApp bridge or email fallback, + * identical to the three-dots menu Support button. + */ +export function openDirectSupportContact(): void { + if (typeof window !== "undefined" && "HabibApp" in window) { + try { + const app = ( + window as Window & { + HabibApp?: { postMessage: (msg: string) => void }; + } + ).HabibApp; + app?.postMessage( + JSON.stringify({ + action: "open_consultant_page", + data: { consultant: "habib@gmail.com" }, + }), + ); + } catch (err) { + console.error("Error calling HabibApp bridge", err); + } + } else if (typeof window !== "undefined") { + window.open("mailto:habib@gmail.com", "_blank"); + } +} + diff --git a/src/components/Componentes/swipe-button.tsx b/src/components/Componentes/swipe-button.tsx index 4b03f0d..d9c5199 100644 --- a/src/components/Componentes/swipe-button.tsx +++ b/src/components/Componentes/swipe-button.tsx @@ -13,7 +13,7 @@ type SwipeButtonProps = { disabled?: boolean; isLoading?: boolean; isSubmitting?: boolean; - theme?: "default" | "green"; + theme?: "default" | "green" | "pink"; }; export function SwipeButton({ @@ -58,8 +58,12 @@ export function SwipeButton({ }; const cancelLabel = cancelText || t?.["Cancel"] || "Cancel"; - const isGreen = theme === "green"; - const buttonBg = isGreen ? "bg-[#00AC78]" : "bg-[#F0445B]"; + const buttonBg = + theme === "green" + ? "bg-[#00AC78]" + : theme === "pink" + ? "bg-gradient-to-r from-[#FF6687] to-[#FF456C] shadow-[0_8px_16px_rgba(255,69,108,0.25)]" + : "bg-[#F0445B]"; const actionButton = (
- ) : caseStatus === "contacted" || - isFemaleContactConfirmed || - (isFemaleProfile && contactStatusMutation.isPending) ? ( + ) : caseStatus === "contacted" || isFemaleContactConfirmed ? (
- {isFemaleProfile && contactStatusMutation.isPending ? ( - - ) : ( -

- {isFemaleProfile ? ( - t[ - "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized." - ] - ) : ( - <> - {t[ - "Thank you for giving us feedback, we would be very happy if you also let us know the final result." - ]} - {!canReportOutcome && ( - <> - {" "} - {t[ - "The 'Share Result' option will become active after 48 hours." - ] || - "The 'Share Result' option will become active after 48 hours."} - - )} - - )} -

- )} +

+ {isFemaleProfile ? ( + t[ + "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized." + ] + ) : ( + <> + {t[ + "Thank you for giving us feedback, we would be very happy if you also let us know the final result." + ]} + {!canReportOutcome && ( + <> + {" "} + {t[ + "The 'Share Result' option will become active after 48 hours." + ] || + "The 'Share Result' option will become active after 48 hours."} + + )} + + )} +

) : noContactReportedSuccess ? (

@@ -896,56 +892,49 @@ export default function RequestAcceptedClient() { {t["Contact Support"] || "Contact Support"}

- ) : caseStatus === "contacted" || - isFemaleContactConfirmed || - (isFemaleProfile && contactStatusMutation.isPending) ? ( + ) : caseStatus === "contacted" || isFemaleContactConfirmed ? (
- {isFemaleProfile && - contactStatusMutation.isPending ? null : ( - <> - + - - - )} +
) : noContactReportedSuccess ? (
@@ -1009,11 +998,7 @@ export default function RequestAcceptedClient() { : "border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#475569] shadow-sm cursor-pointer hover:bg-[#F8FAFC] active:scale-[0.98]" }`} > - {contactStatusMutation.isPending ? ( - - ) : ( - primaryActionText - )} + {primaryActionText} ) : ( +
+ + + ); +} diff --git a/src/app/banned/page.tsx b/src/app/banned/page.tsx new file mode 100644 index 0000000..a0ca578 --- /dev/null +++ b/src/app/banned/page.tsx @@ -0,0 +1,5 @@ +import BannedClient from "./banned-client"; + +export default function BannedPage() { + return ; +} diff --git a/src/app/request-accepted/request-accepted-client.test.tsx b/src/app/request-accepted/request-accepted-client.test.tsx index c771b4b..8b601b8 100644 --- a/src/app/request-accepted/request-accepted-client.test.tsx +++ b/src/app/request-accepted/request-accepted-client.test.tsx @@ -307,10 +307,10 @@ describe("RequestAcceptedClient", () => { expect(screen.getByText(/acquaintance concluded/i)).toBeInTheDocument(); expect( screen.getByText( - "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.", ), ).toBeInTheDocument(); - expect(screen.getByText("Contact Support")).toBeInTheDocument(); + expect(screen.getByText("Contact Advisors")).toBeInTheDocument(); expect(screen.getByText("View all detail")).toBeInTheDocument(); expect(screen.queryByText("Share Result")).not.toBeInTheDocument(); }); @@ -405,10 +405,10 @@ describe("RequestAcceptedClient", () => { expect(screen.getByText(/acquaintance concluded/i)).toBeInTheDocument(); expect( screen.getByText( - "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.", ), ).toBeInTheDocument(); - expect(screen.getByText("Contact Support")).toBeInTheDocument(); + expect(screen.getByText("Contact Advisors")).toBeInTheDocument(); expect(screen.getByText("View all detail")).toBeInTheDocument(); expect(screen.queryByText("Share Result")).not.toBeInTheDocument(); }); diff --git a/src/app/request-accepted/request-accepted-client.tsx b/src/app/request-accepted/request-accepted-client.tsx index 521c48a..f533c5c 100644 --- a/src/app/request-accepted/request-accepted-client.tsx +++ b/src/app/request-accepted/request-accepted-client.tsx @@ -821,6 +821,9 @@ export default function RequestAcceptedClient() {

{ + t[ + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps." + ] || t[ "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps." ] @@ -854,7 +857,8 @@ export default function RequestAcceptedClient() {

) : noContactReportedSuccess ? (

- {t["Your report has been submitted to support."]} + {t["Your report has been submitted to advisors."] || + t["Your report has been submitted to support."]}

) : (

@@ -889,7 +893,7 @@ export default function RequestAcceptedClient() { onClick={() => openDirectSupportContact()} className="flex-1 h-[50px] px-3 rounded-[14px] bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[14px] flex items-center justify-center shadow-[0_8px_16px_rgba(255,69,108,0.25)] transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95" > - {t["Contact Support"] || "Contact Support"} + {t["Contact Advisors"] || t["Contact Support"] || "Contact Advisors"} ) : caseStatus === "contacted" || isFemaleContactConfirmed ? ( @@ -947,8 +951,11 @@ export default function RequestAcceptedClient() {

{t[ - "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." - ]} + "Thank you for your feedback. Our advisors will investigate the matter and notify you of the result. Please wait patiently during the review; our advisors will contact you." + ] || + t[ + "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." + ]}

diff --git a/src/components/Componentes/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx index 35bddcc..bab91ea 100644 --- a/src/components/Componentes/navigation-button.tsx +++ b/src/components/Componentes/navigation-button.tsx @@ -345,7 +345,7 @@ export function NavigationButton({ className="flex w-full items-center gap-3 px-3.5 py-2.5 rounded-[12px] text-start font-medium text-[13.5px] text-[#1F2937] hover:bg-[#F3F4F6] active:bg-[#E5E7EB] transition-colors cursor-pointer border-0 bg-transparent" > - {t["Support"] || "Support"} + {t["Advisors"] || t["Support"] || "Advisors"} )} diff --git a/src/components/Componentes/subscription-required-sheet.tsx b/src/components/Componentes/subscription-required-sheet.tsx index d550e1c..909d690 100644 --- a/src/components/Componentes/subscription-required-sheet.tsx +++ b/src/components/Componentes/subscription-required-sheet.tsx @@ -86,9 +86,12 @@ export function SubscriptionRequiredSheet({

{t[ + "To view profiles, a subscription is required. This helps cover our advisory and matching services and applies only to male users" + ] || + t[ "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users" ] || - "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users"} + "To view profiles, a subscription is required. This helps cover our advisory and matching services and applies only to male users"}

diff --git a/src/components/Componentes/support-sheet.tsx b/src/components/Componentes/support-sheet.tsx index 6a9115f..e6d6e97 100644 --- a/src/components/Componentes/support-sheet.tsx +++ b/src/components/Componentes/support-sheet.tsx @@ -140,11 +140,14 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) { {/* Text Content */}

- {t["Contact Support"]} + {t["Contact Advisors"] || t["Contact Support"]}

{ + t[ + "If you encounter any issues, please feel free to contact our advisors in WhatsApp" + ] || t[ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp" ] diff --git a/src/hooks/marriage/types.ts b/src/hooks/marriage/types.ts index fd6847a..a4f32b3 100644 --- a/src/hooks/marriage/types.ts +++ b/src/hooks/marriage/types.ts @@ -12,7 +12,8 @@ export type MarriageProfileStatus = | "waiting" | "in_case" | "suspended" - | "matched"; + | "matched" + | "banned"; export type MarriageCaseStatus = | "introduced" @@ -130,6 +131,12 @@ export type MarriageProfile = { custom_note?: string; created_at: string; } | null; + ban_reason?: string | null; + ban_info?: { + reason: string; + banned_at?: string; + previous_status?: string; + } | null; }; export type MarriageProfileResponse = MarriageProfile; diff --git a/src/lib/get-submit-path.ts b/src/lib/get-submit-path.ts index ab47cb5..cad2dfd 100644 --- a/src/lib/get-submit-path.ts +++ b/src/lib/get-submit-path.ts @@ -23,6 +23,10 @@ export function getSubmitPath( return "/intro"; } + if (profile.status === "banned") { + return "/banned"; + } + const activeCase = profile.active_case; if (activeCase) { diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index f4627db..98c6808 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -127,7 +127,8 @@ "Contact Detail": "تفاصيل الاتصال", "Contact Information Released": "تم إصدار معلومات الاتصال", "Contact Received": "تم استلام الاتصال", - "Contact Support": "اتصل بالدعم", + "Contact Support": "تواصل مع المستشارين", + "Contact Advisors": "تواصل مع المستشارين", "Contact details and residence.": "تفاصيل الاتصال والإقامة.", "Contact details are shared only after your approval.": "تتم مشاركة تفاصيل الاتصال فقط بعد موافقتك.", "Contact info released": "تم إصدار معلومات الاتصال", @@ -2089,10 +2090,12 @@ "Direct Contact Number (Candidate)": "رقم الاتصال المباشر (المرشحة)", "We did not reach the agreement needed to continue acquaintance and decided not to proceed": "لم نصل إلى التوافق اللازم لمواصلة التعارف وقررنا عدم الاستمرار.", "Acquaintance Concluded": "انتهاء مرحلة التعارف", - "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.": "لم يصل هذا التعارف إلى نتيجة وتم إيقاف المسار. لمزيد من المعلومات حول الخطوات التالية، يرجى التواصل مع الدعم.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.": "لم يصل هذا التعارف إلى نتيجة وتم إيقاف المسار. لمزيد من المعلومات حول الخطوات التالية، يرجى التواصل مع المستشارين.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.": "لم يصل هذا التعارف إلى نتيجة وتم إيقاف المسار. لمزيد من المعلومات حول الخطوات التالية، يرجى التواصل مع المستشارين.", "Report Registered": "تم تسجيل البلاغ", "View all detail": "عرض كافة التفاصيل", - "Your report has been submitted to support.": "تم إرسال بلاغك بنجاح إلى فريق الدعم.", + "Your report has been submitted to support.": "تم إرسال بلاغك بنجاح إلى المستشارين.", + "Your report has been submitted to advisors.": "تم إرسال بلاغك بنجاح إلى المستشارين.", "This option will become active 48 hours after contact details are shared.": "سيكون هذا الخيار متاحاً بعد مرور 48 ساعة على مشاركة بيانات الاتصال.", "The 'Share Result' option will become active after 48 hours.": "سيكون خيار «تسجيل النتيجة» متاحاً بعد مرور 48 ساعة." } diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index a74f85c..6922fdc 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -129,7 +129,8 @@ "Contact Detail": "Contact Detail", "Contact Information Released": "Contact Information Released", "Contact Received": "Contact Received", - "Contact Support": "Contact Support", + "Contact Support": "Contact Advisors", + "Contact Advisors": "Contact Advisors", "Contact details and residence.": "Contact details and residence.", "Contact details are shared only after your approval.": "Contact details are shared only after your approval.", "Contact info released": "Contact info released", @@ -291,7 +292,8 @@ "Identity & Demographics": "Identity & Demographics", "Identity Verification and Documents": "Identity Verification and Documents", "Identity Verification:": "Identity Verification:", - "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "If you encounter any issues, please feel free to contact our support specialists in WhatsApp", + "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "If you encounter any issues, please feel free to contact our advisors in WhatsApp", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "If you encounter any issues, please feel free to contact our advisors in WhatsApp", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.", "Important Note": "Important Note", @@ -617,7 +619,8 @@ "Submit Woman": "Submit Woman", "Subscription": "Subscription", "Subscription Status": "Subscription Status", - "Support": "Support", + "Support": "Advisors", + "Advisors": "Advisors", "Supporter of the current government, but a difference in view is not a red line.": "Supporter of the current government, but a difference in view is not a red line.", "Supporter of the current government; serious opposition from my spouse is a red line.": "Supporter of the current government; serious opposition from my spouse is a red line.", "Sweden": "Sweden", @@ -636,7 +639,8 @@ "Temporary conditions": "Temporary conditions", "Temporary with family okay": "Temporary with family okay", "Thank you for giving us feedback, we would be very happy if you also let us know the final result.": "Thank you for giving us feedback, we would be very happy if you also let us know the final result.", - "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you.": "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you.", + "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you.": "Thank you for your feedback. Our advisors will investigate the matter and notify you of the result. Please wait patiently during the review; our advisors will contact you.", + "Thank you for your feedback. Our advisors will investigate the matter and notify you of the result. Please wait patiently during the review; our advisors will contact you.": "Thank you for your feedback. Our advisors will investigate the matter and notify you of the result. Please wait patiently during the review; our advisors will contact you.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.", "The call may start 10-15 minutes earlier or later than scheduled.": "The call may start 10-15 minutes earlier or later than scheduled.", "The selected candidate will contact your family shortly.": "The selected candidate will contact your family shortly.", @@ -698,13 +702,15 @@ "View profile": "View profile", "View all detail": "View all detail", "Report Registered": "Report Registered", - "Your report has been submitted to support.": "Your report has been submitted to support.", + "Your report has been submitted to support.": "Your report has been submitted to advisors.", + "Your report has been submitted to advisors.": "Your report has been submitted to advisors.", "Watch Video": "Watch Video", "We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet", "We did not reach an agreement": "We did not reach an agreement", "We did not reach the agreement needed to continue acquaintance and decided not to proceed": "We did not reach the agreement needed to continue acquaintance and decided not to proceed", "Acquaintance Concluded": "Acquaintance Concluded", - "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.": "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.": "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.": "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.", "We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims": "We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims", "We hope the acquaintance process is going well. Please let us know if you want to continue the acquaintance process or if the match has been canceled.": "We hope the acquaintance process is going well. Please let us know if you want to continue the acquaintance process or if the match has been canceled.", "We provide a safe and respectful environment at every step.": "We provide a safe and respectful environment at every step.", @@ -2070,5 +2076,18 @@ "Representative's Phone": "Representative's Phone", "Direct Contact Number (Candidate)": "Direct Contact Number (Candidate)", "This option will become active 48 hours after contact details are shared.": "This option will become active 48 hours after contact details are shared.", - "The 'Share Result' option will become active after 48 hours.": "The 'Share Result' option will become active after 48 hours." + "The 'Share Result' option will become active after 48 hours.": "The 'Share Result' option will become active after 48 hours.", + "ACCOUNT SUSPENDED": "ACCOUNT SUSPENDED", + "Account Suspended": "Account Suspended", + "Account Suspended Notice": "In accordance with platform guidelines and supervisory review, access to your account services has been suspended. For further details or inquiries, please contact our advisors.", + "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.", + "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.", + "Ban Reason": "Suspension Reason:", + "Profile editing is temporarily disabled": "Profile editing is disabled during account suspension.", + "inappropriate_behavior": "Violation of ethical conduct & respectful behavior", + "false_information": "Providing inaccurate or false identity information", + "unserious_intent": "Lack of serious intention regarding marriage", + "harassment_complaint": "Reports or complaints regarding inappropriate conduct", + "commercial_activity": "Commercial, promotional, or non-marriage activities", + "admin_discretion": "Administrative and supervisory team discretion" } diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index d4eb2de..481b11d 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -129,7 +129,8 @@ "Contact Detail": "جزئیات تماس", "Contact Information Released": "اطلاعات تماس آزاد شد", "Contact Received": "تماس دریافت شد", - "Contact Support": "ارتباط با پشتیبانی", + "Contact Support": "ارتباط با مشاوران", + "Contact Advisors": "ارتباط با مشاوران", "Contact details and residence.": "اطلاعات تماس و سکونت.", "Contact details are shared only after your approval.": "اطلاعات تماس شما فقط پس از تأیید خودتان به اشتراک گذاشته می‌شود.", "Contact info released": "اطلاعات تماس آزاد شد", @@ -291,7 +292,8 @@ "Identity & Demographics": "هویت و مشخصات فردی", "Identity Verification and Documents": "بخش ۹: بارگذاری مدارک، احراز هویت و تصاویر", "Identity Verification:": "احراز هویت:", - "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "اگر مشکلی دارید، لطفاً با تیم پشتیبانی ما در واتس‌اپ تماس بگیرید", + "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "اگر مشکلی دارید، لطفاً با مشاوران ما در واتس‌اپ در ارتباط باشید", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "اگر مشکلی دارید، لطفاً با مشاوران ما در واتس‌اپ در ارتباط باشید", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "اگر شرایط خاصی درباره کار، درآمد، اجاره، خرید خانه، مهاجرت یا محل زندگی آینده دارید، کوتاه توضیح دهید.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "اگر فرآیند را ادامه دهید ما به طرف مقابل اعلام می‌کنیم و پس از تأیید ایشان، می‌توانید اطلاعات ارتباطی یکدیگر را مشاهده کنید.", "Important Note": "نکته مهم", @@ -617,7 +619,8 @@ "Submit Woman": "ثبت‌نام خانم", "Subscription": "اشتراک", "Subscription Status": "وضعیت اشتراک", - "Support": "پشتیبانی", + "Support": "مشاوران", + "Advisors": "مشاوران", "Supporter of the current government, but a difference in view is not a red line.": "موافق و حامی نظام فعلی، اما تفاوت دیدگاه همسرم خط قرمز نیست.", "Supporter of the current government; serious opposition from my spouse is a red line.": "موافق و حامی نظام فعلی؛ مخالفت جدی همسرم خط قرمز است.", "Sweden": "سوئد", @@ -636,7 +639,8 @@ "Temporary conditions": "فعلاً شرایط موقت دارم.", "Temporary with family okay": "زندگی موقت با خانواده در ابتدای ازدواج قابل قبول است.", "Thank you for giving us feedback, we would be very happy if you also let us know the final result.": "ممنون از اینکه به ما فیدبک دادید، بسیار خوشحال میشویم نتیجه نهایی را نیز به ما اعلام کنید", - "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you.": "ممنون از بازخورد شما و تیم پشتیبانی ما در اسرع وقت موضوع رو بررسی و به اطلاع شما میرسونن", + "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you.": "ممنون از بازخورد شما؛ مشاوران ما در اسرع وقت موضوع را بررسی و به اطلاع شما می‌رسانند", + "Thank you for your feedback. Our advisors will investigate the matter and notify you of the result. Please wait patiently during the review; our advisors will contact you.": "ممنون از بازخورد شما؛ مشاوران ما در اسرع وقت موضوع را بررسی و به اطلاع شما می‌رسانند", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "از ثبت بازخورد شما متشکریم. برای تکمیل فرآیند، لطفاً نتیجه نهایی این معرفی/ارتباط را مشخص کنید تا وضعیت پرونده نهایی شود. در صورتی که هنوز وضعیت نهایی مشخص نشده، می‌توانید تا زمان مشخص شدن در همین وضعیت باقی بمانید.", "The call may start 10-15 minutes earlier or later than scheduled.": "تماس ممکن است ۱۰ الی ۱۵ دقیقه زودتر یا دیرتر از زمان تعیین‌شده برقرار شود.", "The selected candidate will contact your family shortly.": "گزینه انتخاب‌شده به‌زودی با خانواده شما تماس می‌گیرد.", @@ -698,13 +702,15 @@ "View profile": "مشاهده پروفایل", "View all detail": "مشاهده تمام جزئیات", "Report Registered": "گزارش ثبت شد", - "Your report has been submitted to support.": "گزارش شما با موفقیت برای پشتیبانی ارسال شد.", + "Your report has been submitted to support.": "گزارش شما با موفقیت برای مشاوران ارسال شد.", + "Your report has been submitted to advisors.": "گزارش شما با موفقیت برای مشاوران ارسال شد.", "Watch Video": "مشاهده ویدیو", "We are in the acquaintance/proposal process and nothing is finalized yet": "در مسیر آشنایی و خواستگاری هستیم و هنوز هیچی نهایی نشده", "We did not reach an agreement": "به تفاهم نرسیدیم", "We did not reach the agreement needed to continue acquaintance and decided not to proceed": "به تفاهم لازم برای ادامه آشنایی نرسیدیم و تصمیم گرفتیم مسیر را ادامه ندهیم.", "Acquaintance Concluded": "پایان فرآیند آشنایی", - "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.": "این آشنایی به نتیجه نرسیده و ادامه مسیر متوقف شده است. برای اطلاع از مراحل بعدی لطفاً با پشتیبانی تماس بگیرید.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact support for information on the next steps.": "این آشنایی به نتیجه نرسیده و ادامه مسیر متوقف شده است. برای اطلاع از مراحل بعدی لطفاً با مشاوران در ارتباط باشید.", + "This acquaintance has not reached a conclusion and the process has been stopped. Please contact advisors for information on the next steps.": "این آشنایی به نتیجه نرسیده و ادامه مسیر متوقف شده است. برای اطلاع از مراحل بعدی لطفاً با مشاوران در ارتباط باشید.", "We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims": "ما با هدف ایجاد مسیری امن و محرمانه برای ازدواج دائم میان مسلمانان کنار هم آمده‌ایم", "We hope the acquaintance process is going well. Please let us know if you want to continue the acquaintance process or if the match has been canceled.": "امیدواریم فرآیند آشنایی شما به خوبی پیش برود. لطفاً مشخص کنید که آیا همچنان در حال ادامه فرآیند آشنایی هستید یا این معرفی کنسل شده است؟", "We provide a safe and respectful environment at every step.": "ما در هر مرحله فضایی امن و محترمانه را فراهم می‌کنیم.", @@ -824,7 +830,8 @@ "Buy Habib Coins": "خرید سکه حبیب", "Subscription required": "اشتراک الزامی است", "To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "برای مشاهده پروفایل‌ها، نیاز به اشتراک دارید. هر اشتراک شامل حداکثر {count} همسان‌یابی است.", - "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "برای مشاهده پروفایل‌ها، اشتراک الزامی است. این هزینه صرف خدمات پشتیبانی و همسان‌یابی می‌شود و فقط شامل کاربران مرد است", + "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "برای مشاهده پروفایل‌ها، اشتراک الزامی است. این هزینه صرف خدمات مشاوره و همسان‌یابی می‌شود و فقط شامل کاربران مرد است", + "To view profiles, a subscription is required. This helps cover our advisory and matching services and applies only to male users": "برای مشاهده پروفایل‌ها، اشتراک الزامی است. این هزینه صرف خدمات مشاوره و همسان‌یابی می‌شود و فقط شامل کاربران مرد است", "{count} case introductions remaining of your subscription.": "{count} معرفی از اعتبار اشتراک شما باقی مانده است.", "{days} days and {count} case introductions remaining of your subscription.": "{days} روز و {count} معرفی از اعتبار اشتراک شما باقی مانده است.", "You do not have enough Habib Coins": "شما حبیب کوین کافی ندارید", @@ -2022,7 +2029,8 @@ "Cooperation with Law Enforcement": "همکاری با مراجع قانونی", "In the event of cybercrimes, fraud, or threats, the application will refer the necessary information to judicial and law enforcement authorities.": "در صورت وقوع جرایم سایبری، کلاهبرداری یا تهدید، برنامه اطلاعات لازم را به مراجع قضایی و انتظامی ارجاع خواهد داد.", "Dispute Resolution": "حل اختلاف", - "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "هرگونه اختلاف حقوقی ابتدا باید از طریق پشتیبانی برنامه پیگیری شود. در صورت عدم حصول توافق، مرجع صالح دادگاه‌های شهر محل ثبت برنامه خواهند بود.", + "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "هرگونه اختلاف حقوقی ابتدا باید از طریق مشاوران برنامه پیگیری شود. در صورت عدم حصول توافق، مرجع صالح دادگاه‌های شهر محل ثبت برنامه خواهند بود.", + "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "هرگونه اختلاف حقوقی ابتدا باید از طریق مشاوران برنامه پیگیری شود. در صورت عدم حصول توافق، مرجع صالح دادگاه‌های شهر محل ثبت برنامه خواهند بود.", "7. Disclaimer and In-Person Meetings": "۷. حدود مسئولیت و دیدارهای حضوری", "Role of the Application": "نقش برنامه", "This platform is only a facilitator for initial communication and does not replace family background research, counseling, and pre-marital investigations.": "این پلتفرم تنها تسهیل‌کننده ارتباط اولیه است و جایگزین تحقیق خانوادگی، مشاوره و بررسی‌های پیش از ازدواج نیست.", @@ -2090,5 +2098,16 @@ "Representative's Phone": "شماره تماس رابط", "Direct Contact Number (Candidate)": "شماره مستقیم خانم", "This option will become active 48 hours after contact details are shared.": "این گزینه ۴۸ ساعت پس از آزادسازی اطلاعات تماس فعال می‌شود.", - "The 'Share Result' option will become active after 48 hours.": "گزینه «ثبت نتیجه نهایی» بعد از ۴۸ ساعت فعال خواهد شد." + "The 'Share Result' option will become active after 48 hours.": "گزینه «ثبت نتیجه نهایی» بعد از ۴۸ ساعت فعال خواهد شد.", + "ACCOUNT SUSPENDED": "دسترسی شما محدود شده است", + "Account Suspended": "دسترسی شما محدود شده است", + "Account Suspended Notice": "بر اساس قوانین و موازین سامانه، دسترسی حساب کاربری شما به بخش خدمات موقتاً مسدود گردیده است. برای بررسی موضوع و پیگیری، با مشاوران ما در ارتباط باشید.", + "Ban Reason": "دلیل محدودیت:", + "Profile editing is temporarily disabled": "امکان ویرایش اطلاعات حساب در زمان محدودیت وجود ندارد.", + "inappropriate_behavior": "عدم رعایت شئونات اخلاقی و رفتار محترمانه", + "false_information": "ثبت مشخصات یا اطلاعات هویتی غیرواقعی", + "unserious_intent": "عدم جدیت در امر ازدواج و نقض اهداف سامانه", + "harassment_complaint": "گزارش نارضایتی یا ایجاد مزاحمت برای سایر کاربران", + "commercial_activity": "فعالیت تجاری، تبلیغاتی یا مقاصد غیرمرتبط", + "admin_discretion": "صلاحدید و تصمیم کارشناسی مدیریت سامانه" } diff --git a/src/translations/path_to_english.json b/src/translations/path_to_english.json index 53bbfe5..bb741ca 100644 --- a/src/translations/path_to_english.json +++ b/src/translations/path_to_english.json @@ -5,7 +5,7 @@ "common.continue": "Continue", "common.cancel": "Cancel", "common.confirm": "Confirm", - "common.support": "Support", + "common.support": "Advisors", "common.back": "Back", "common.required": "Required", "common.estimateTime": "Estimate time", @@ -23,8 +23,8 @@ "common.itemsPerPage": "Items Per Page", "common.other": "Other", "common.consultation": "Consultation", - "common.supportTitle": "Contact Support", - "common.supportDescription": "If you encounter any issues, please feel free to contact our support specialists in WhatsApp", + "common.supportTitle": "Contact Advisors", + "common.supportDescription": "If you encounter any issues, please feel free to contact our advisors in WhatsApp", "common.supportSwipeText": "Contact", "common.subscription": "Subscription", "common.subscriptionActive": "Your subscription is active", From 2e61edfa1fdadd8d6d2171d71930f3bc4c590c40 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Thu, 10 Sep 2026 17:14:21 +0330 Subject: [PATCH 15/27] feat: add translation files for 22 new languages to support internationalization --- src/translations/locales/ar.json | 4 +++- src/translations/locales/az.json | 5 ++++- src/translations/locales/bn.json | 5 ++++- src/translations/locales/da.json | 5 ++++- src/translations/locales/de.json | 5 ++++- src/translations/locales/es.json | 5 ++++- src/translations/locales/fr.json | 5 ++++- src/translations/locales/gu.json | 5 ++++- src/translations/locales/ha.json | 5 ++++- src/translations/locales/he.json | 5 ++++- src/translations/locales/hi.json | 5 ++++- src/translations/locales/id.json | 5 ++++- src/translations/locales/ks.json | 5 ++++- src/translations/locales/pt.json | 5 ++++- src/translations/locales/ru.json | 5 ++++- src/translations/locales/sw.json | 5 ++++- src/translations/locales/tg.json | 5 ++++- src/translations/locales/tr.json | 5 ++++- src/translations/locales/ul.json | 5 ++++- src/translations/locales/ur.json | 5 ++++- src/translations/locales/uz.json | 5 ++++- src/translations/locales/zh.json | 5 ++++- 22 files changed, 87 insertions(+), 22 deletions(-) diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index 98c6808..e58c1f7 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -2097,5 +2097,7 @@ "Your report has been submitted to support.": "تم إرسال بلاغك بنجاح إلى المستشارين.", "Your report has been submitted to advisors.": "تم إرسال بلاغك بنجاح إلى المستشارين.", "This option will become active 48 hours after contact details are shared.": "سيكون هذا الخيار متاحاً بعد مرور 48 ساعة على مشاركة بيانات الاتصال.", - "The 'Share Result' option will become active after 48 hours.": "سيكون خيار «تسجيل النتيجة» متاحاً بعد مرور 48 ساعة." + "The 'Share Result' option will become active after 48 hours.": "سيكون خيار «تسجيل النتيجة» متاحاً بعد مرور 48 ساعة.", + "Advisors": "المستشارون", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "إذا واجهت أي مشكلات، فلا تتردد في الاتصال بمستشارينا في WhatsApp" } diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index a8478f2..251008b 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -2094,5 +2094,8 @@ "View all detail": "Bütün təfərrüatlara baxın", "Your report has been submitted to support.": "Hesabatınız dəstək xidmətinə uğurla göndərildi.", "This option will become active 48 hours after contact details are shared.": "Bu seçim əlaqə məlumatları paylaşıldıqdan 48 saat sonra aktivləşəcək.", - "The 'Share Result' option will become active after 48 hours.": "«Nəticəni paylaşın» seçimi 48 saatdan sonra aktivləşəcək." + "The 'Share Result' option will become active after 48 hours.": "«Nəticəni paylaşın» seçimi 48 saatdan sonra aktivləşəcək.", + "Advisors": "Məsləhətçilər", + "Contact Advisors": "Məsləhətçilərlə əlaqə saxlayın", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hər hansı problemlə qarşılaşsanız, WhatsApp-da məsləhətçilərimizlə əlaqə saxlayın" } diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index e4d2f2c..9d3e86d 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -2094,5 +2094,8 @@ "View all detail": "সমস্ত বিবরণ দেখুন", "Your report has been submitted to support.": "আপনার রিপোর্ট সফলভাবে সাপোর্টে পাঠানো হয়েছে।", "This option will become active 48 hours after contact details are shared.": "যোগাযোগের বিবরণ শেয়ার করার ৪৮ ঘণ্টা পর এই বিকল্পটি সক্রিয় হবে।", - "The 'Share Result' option will become active after 48 hours.": "৪৮ ঘণ্টা পর 'ফলাফল শেয়ার করুন' বিকল্পটি সক্রিয় হবে।" + "The 'Share Result' option will become active after 48 hours.": "৪৮ ঘণ্টা পর 'ফলাফল শেয়ার করুন' বিকল্পটি সক্রিয় হবে।", + "Advisors": "পরামর্শক", + "Contact Advisors": "পরামর্শকদের সাথে যোগাযোগ করুন", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "আপনি যদি কোনো সমস্যার সম্মুখীন হন, অনুগ্রহ করে WhatsApp-এ আমাদের পরামর্শকদের সাথে নির্দ্বিধায় যোগাযোগ করুন" } diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 9853848..85a5559 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -2094,5 +2094,8 @@ "View all detail": "Se alle detaljer", "Your report has been submitted to support.": "Din rapport er blevet sendt til support.", "This option will become active 48 hours after contact details are shared.": "Denne mulighed bliver aktiv 48 timer efter, at kontaktoplysningerne er delt.", - "The 'Share Result' option will become active after 48 hours.": "Valgmuligheden 'Del resultat' bliver aktiv efter 48 timer." + "The 'Share Result' option will become active after 48 hours.": "Valgmuligheden 'Del resultat' bliver aktiv efter 48 timer.", + "Advisors": "Rådgivere", + "Contact Advisors": "Kontakt rådgivere", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hvis du støder på problemer, er du velkommen til at kontakte vores rådgivere på WhatsApp" } diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index 12b595f..6869860 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -2094,5 +2094,8 @@ "View all detail": "Alle Details anzeigen", "Your report has been submitted to support.": "Ihre Meldung wurde erfolgreich an den Support übermittelt.", "This option will become active 48 hours after contact details are shared.": "Diese Option wird 48 Stunden nach Freigabe der Kontaktdaten aktiviert.", - "The 'Share Result' option will become active after 48 hours.": "Die Option 'Ergebnis teilen' wird nach 48 Stunden aktiviert." + "The 'Share Result' option will become active after 48 hours.": "Die Option 'Ergebnis teilen' wird nach 48 Stunden aktiviert.", + "Advisors": "Berater", + "Contact Advisors": "Berater kontaktieren", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Bei Problemen kontaktieren Sie bitte unsere Berater über WhatsApp" } diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index 1bdd77a..05ea896 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -2094,5 +2094,8 @@ "View all detail": "Ver todos los detalles", "Your report has been submitted to support.": "Su reporte ha sido enviado con éxito al soporte técnico.", "This option will become active 48 hours after contact details are shared.": "Esta opción se activará 48 horas después de compartir los datos de contacto.", - "The 'Share Result' option will become active after 48 hours.": "La opción 'Compartir resultado' se activará después de 48 horas." + "The 'Share Result' option will become active after 48 hours.": "La opción 'Compartir resultado' se activará después de 48 horas.", + "Advisors": "Consejeros", + "Contact Advisors": "Contactar consejeros", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si tiene algún problema, no dude en ponerse en contacto con nuestros consejeros en WhatsApp" } diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 702e040..1ad4ff8 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -2094,5 +2094,8 @@ "View all detail": "Voir tous les détails", "Your report has been submitted to support.": "Votre signalement a été transmis avec succès à l'assistance.", "This option will become active 48 hours after contact details are shared.": "Cette option sera activée 48 heures après le partage des coordonnées.", - "The 'Share Result' option will become active after 48 hours.": "L'option « Partager le résultat » sera activée après 48 heures." + "The 'Share Result' option will become active after 48 hours.": "L'option « Partager le résultat » sera activée après 48 heures.", + "Advisors": "Conseillers", + "Contact Advisors": "Contacter les conseillers", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si vous rencontrez des problèmes, n'hésitez pas à contacter nos conseillers sur WhatsApp" } diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index 98a4704..d9eee96 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -2096,5 +2096,8 @@ "View all detail": "બધી વિગતો જુઓ", "Your report has been submitted to support.": "તમારો રિપોર્ટ સફળતાપૂર્વક સપોર્ટ પર મોકલી દેવાયો છે.", "This option will become active 48 hours after contact details are shared.": "સંપર્ક વિગતો શેર કર્યાના 48 કલાક પછી આ વિકલ્પ સક્રિય થશે.", - "The 'Share Result' option will become active after 48 hours.": "'પરિણામ શેર કરો' વિકલ્પ 48 કલાક પછી સક્રિય થશે." + "The 'Share Result' option will become active after 48 hours.": "'પરિણામ શેર કરો' વિકલ્પ 48 કલાક પછી સક્રિય થશે.", + "Advisors": "સલાહકારો", + "Contact Advisors": "સલાહકારોનો સંપર્ક કરો", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "જો તમને કોઈ સમસ્યા આવે, તો કૃપા કરીને WhatsApp પર અમારા સલાહકારોનો સંપર્ક કરો" } diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 631d39a..9cc1338 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -2094,5 +2094,8 @@ "View all detail": "Duba duk cikakkun bayanai", "Your report has been submitted to support.": "An aika rahotonku zuwa ga sashin tallafi cikin nasara.", "This option will become active 48 hours after contact details are shared.": "Wannan zaɓin zai fara aiki ne bayan awanni 48 da raba bayanan tuntuɓa.", - "The 'Share Result' option will become active after 48 hours.": "Za a kunna zaɓin 'Raba Sakamako' bayan awanni 48." + "The 'Share Result' option will become active after 48 hours.": "Za a kunna zaɓin 'Raba Sakamako' bayan awanni 48.", + "Advisors": "Masu ba da shawara", + "Contact Advisors": "Tuntuɓi masu ba da shawara", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Idan kun ci karo da matsala, ku tuntuɓi masu ba da shawara a WhatsApp" } diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index 06d2d8c..c6279db 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -2359,5 +2359,8 @@ "View all detail": "הצג את כל הפרטים", "Your report has been submitted to support.": "הדיווח שלך הועבר בהצלחה לתמיכה.", "This option will become active 48 hours after contact details are shared.": "אפשרות זו תופעל 48 שעות לאחר שיתוף פרטי הקשר.", - "The 'Share Result' option will become active after 48 hours.": "האפשרות 'שתף תוצאה' תופעל לאחר 48 שעות." + "The 'Share Result' option will become active after 48 hours.": "האפשרות 'שתף תוצאה' תופעל לאחר 48 שעות.", + "Advisors": "יועצים", + "Contact Advisors": "פנייה ליועצים", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "אם נתקלת בבעיה כלשהי, ניתן לפנות ליועצים שלנו בוואטסאפ." } diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index 16ec34f..1c7417e 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -2094,5 +2094,8 @@ "View all detail": "सभी विवरण देखें", "Your report has been submitted to support.": "आपकी रिपोर्ट सफलतापूर्वक सहायता टीम को भेज दी गई है।", "This option will become active 48 hours after contact details are shared.": "संपर्क विवरण साझा किए जाने के 48 घंटे बाद यह विकल्प सक्रिय हो जाएगा।", - "The 'Share Result' option will become active after 48 hours.": "'परिणाम साझा करें' विकल्प 48 घंटे बाद सक्रिय हो जाएगा।" + "The 'Share Result' option will become active after 48 hours.": "'परिणाम साझा करें' विकल्प 48 घंटे बाद सक्रिय हो जाएगा।", + "Advisors": "सलाहकार", + "Contact Advisors": "सलाहकारों से संपर्क करें", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "किसी भी समस्या के लिए, कृपया व्हाट्सएप पर हमारे सलाहकारों से संपर्क करें" } diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index e492aa1..6fb31fe 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -2359,5 +2359,8 @@ "View all detail": "Lihat semua detail", "Your report has been submitted to support.": "Laporan Anda telah berhasil dikirim ke tim dukungan.", "This option will become active 48 hours after contact details are shared.": "Opsi ini akan aktif 48 jam setelah detail kontak dibagikan.", - "The 'Share Result' option will become active after 48 hours.": "Opsi 'Bagikan Hasil' akan aktif setelah 48 jam." + "The 'Share Result' option will become active after 48 hours.": "Opsi 'Bagikan Hasil' akan aktif setelah 48 jam.", + "Advisors": "Penasihat", + "Contact Advisors": "Hubungi Penasihat", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Jika Anda mengalami kendala, jangan ragu untuk menghubungi penasihat kami di WhatsApp." } diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index f160c3d..3c734d9 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -2359,5 +2359,8 @@ "View all detail": "تمام تفصیلات وچھِو", "Your report has been submitted to support.": "تہنٛز رپورٹ گیہِ کامِیابی سان سپورٹس کنہِ روانہٕ۔", "This option will become active 48 hours after contact details are shared.": "یہِ آپشن گژھِ رابطہٕ تفصِیلات شیئر کرنہٕ پتہٕ ۴۸ گنٛٹن مَنٛز فعال۔", - "The 'Share Result' option will become active after 48 hours.": "'نتیجہ شیئر کٔرِو' آپشن گژھِ ۴۸ گنٛٹن پتہٕ فعال۔" + "The 'Share Result' option will become active after 48 hours.": "'نتیجہ شیئر کٔرِو' آپشن گژھِ ۴۸ گنٛٹن پتہٕ فعال۔", + "Advisors": "مشیر", + "Contact Advisors": "مشیرن سٟتؠ رابطہ کٔریو", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "کانہہ دشواری پیش یِنہٕ وزِ ہیکیو واٹس ایپ پؠٹھ مشیرن سٟتؠ رابطہ کٔرِتھ۔" } diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index 337752b..cbc9ba7 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -2359,5 +2359,8 @@ "View all detail": "Ver todos os detalhes", "Your report has been submitted to support.": "O seu relatório foi enviado com sucesso para o suporte.", "This option will become active 48 hours after contact details are shared.": "Esta opção ficará ativa 48 horas após o compartilhamento dos dados de contato.", - "The 'Share Result' option will become active after 48 hours.": "A opção 'Compartilhar resultado' ficará ativa após 48 horas." + "The 'Share Result' option will become active after 48 hours.": "A opção 'Compartilhar resultado' ficará ativa após 48 horas.", + "Advisors": "Consultores", + "Contact Advisors": "Contatar consultores", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Se encontrar algum problema, entre em contato com nossos consultores no WhatsApp." } diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 6f2180c..554da24 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -2090,5 +2090,8 @@ "View all detail": "Посмотреть все детали", "Your report has been submitted to support.": "Ваше обращение успешно отправлено в службу поддержки.", "This option will become active 48 hours after contact details are shared.": "Эта опция станет активна через 48 часов после открытия контактных данных.", - "The 'Share Result' option will become active after 48 hours.": "Опция «Поделиться результатом» станет активна через 48 часов." + "The 'Share Result' option will become active after 48 hours.": "Опция «Поделиться результатом» станет активна через 48 часов.", + "Advisors": "Консультанты", + "Contact Advisors": "Связаться с консультантами", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Если у вас возникнут какие-либо проблемы, пожалуйста, свяжитесь с нашими консультантами в WhatsApp." } diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 4cd2cc6..75d5b9d 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -2359,5 +2359,8 @@ "View all detail": "Tazama maelezo yote", "Your report has been submitted to support.": "Ripoti yako imetumwa kwa usaidizi kwa mafanikio.", "This option will become active 48 hours after contact details are shared.": "Chaguo hili litafanya kazi baada ya saa 48 tangu maelezo ya mawasiliano yashirikiwe.", - "The 'Share Result' option will become active after 48 hours.": "Chaguo la 'Shiriki Matokeo' litafanya kazi baada ya saa 48." + "The 'Share Result' option will become active after 48 hours.": "Chaguo la 'Shiriki Matokeo' litafanya kazi baada ya saa 48.", + "Advisors": "Washauri", + "Contact Advisors": "Wasiliana na Washauri", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Ukikumbana na changamoto yoyote, jisikie huru kuwasiliana na washauri wetu kupitia WhatsApp." } diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index 9940568..25e05b1 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -2359,5 +2359,8 @@ "View all detail": "Мушоҳидаи тамоми ҷузъиёт", "Your report has been submitted to support.": "Гузориши шумо бомуваفфақият ба бахши дастгирӣ фиристода шуд.", "This option will become active 48 hours after contact details are shared.": "Ин интихоб 48 соат пас аз мубодилаи маълумоти тамос фаъол мешавад.", - "The 'Share Result' option will become active after 48 hours.": "Гузинаи «Сабти натиҷа» баъд аз 48 соат фаъол хоҳад шуд." + "The 'Share Result' option will become active after 48 hours.": "Гузинаи «Сабти натиҷа» баъд аз 48 соат фаъол хоҳад шуд.", + "Advisors": "Мушовирон", + "Contact Advisors": "Тамос бо мушовирон", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Агар мушкиле пеш ояд, лутфан ба мушовирони мо дар WhatsApp муроҷиат кунед." } diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index 3118ef1..70c8a1b 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -2359,5 +2359,8 @@ "View all detail": "Tüm ayrıntıları görüntüle", "Your report has been submitted to support.": "Bildiriminiz destek ekibine başarıyla iletildi.", "This option will become active 48 hours after contact details are shared.": "Bu seçenek, iletişim bilgileri paylaşıldıktan 48 saat sonra aktif olacaktır.", - "The 'Share Result' option will become active after 48 hours.": "'Sonucu Paylaş' seçeneği 48 saat sonra aktif olacaktır." + "The 'Share Result' option will become active after 48 hours.": "'Sonucu Paylaş' seçeneği 48 saat sonra aktif olacaktır.", + "Advisors": "Danışmanlar", + "Contact Advisors": "Danışmanlarla İletişime Geçin", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Herhangi bir sorunla karşılaşırsanız WhatsApp üzerinden danışmanlarımızla iletişime geçebilirsiniz." } diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index ba21bf2..978ce07 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -2359,5 +2359,8 @@ "View all detail": "تمام تفصیلات دیکھیں", "Your report has been submitted to support.": "آپ کی رپورٹ کامیابی کے ساتھ سپورٹ کو بھیج دی گئی ہے۔", "This option will become active 48 hours after contact details are shared.": "Ye option rabtay ki tafseelat share honay ke 48 ghantay baad active hoga.", - "The 'Share Result' option will become active after 48 hours.": "«نەتىجىنى ھەمبەھىرلەش» تاللىشى 48 سائەتتىن كېيىن ئاكتىپلىنىدۇ." + "The 'Share Result' option will become active after 48 hours.": "«نەتىجىنى ھەمبەھىرلەش» تاللىشى 48 سائەتتىن كېيىن ئاكتىپلىنىدۇ.", + "Advisors": "Musheeran", + "Contact Advisors": "Musheeran se Rabta Karein", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar koi masla darpesh ho to WhatsApp par hamare musheeran se bila-jhijhak rabta karein." } diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index bb97c82..8f4f9bc 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -2359,5 +2359,8 @@ "View all detail": "تمام تفصیلات دیکھیں", "Your report has been submitted to support.": "آپ کی رپورٹ کامیابی سے سپورٹ کو بھیج دی گئی ہے۔", "This option will become active 48 hours after contact details are shared.": "یہ آپشن رابطہ کی تفصیلات شیئر کیے جانے کے 48 گھنٹے بعد فعال ہوگا۔", - "The 'Share Result' option will become active after 48 hours.": "'نتیجہ شیئر کریں' کا آپشن 48 گھنٹے بعد فعال ہو جائے گا۔" + "The 'Share Result' option will become active after 48 hours.": "'نتیجہ شیئر کریں' کا آپشن 48 گھنٹے بعد فعال ہو جائے گا۔", + "Advisors": "مشیران", + "Contact Advisors": "مشیروں سے رابطہ کریں", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "اگر کوئی مسئلہ درپیش ہو تو واٹس ایپ پر ہمارے مشیروں سے بلا جھجھک رابطہ کریں۔" } diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index a365548..e37cd02 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -2359,5 +2359,8 @@ "View all detail": "Barcha tafsilotlarni ko'rish", "Your report has been submitted to support.": "Hisobotingiz qo'llab-quvvatlash xizmatiga muvaffaqiyatli yuborildi.", "This option will become active 48 hours after contact details are shared.": "Bu parametr aloqa ma'lumotlari ulashilgandan 48 soat o'tgach faollashadi.", - "The 'Share Result' option will become active after 48 hours.": "'Natijani ulashish' opsiyasi 48 soatdan keyin faollashadi." + "The 'Share Result' option will become active after 48 hours.": "'Natijani ulashish' opsiyasi 48 soatdan keyin faollashadi.", + "Advisors": "Maslahatchilar", + "Contact Advisors": "Maslahatchilar bilan bog'lanish", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar biron bir muammo yuzaga kelsa, WhatsApp orqali maslahatchilarimiz bilan bog'laning." } diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index 85bc2ee..e769c64 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -2094,5 +2094,8 @@ "View all detail": "查看所有详情", "Your report has been submitted to support.": "您的报告已成功提交至客服支持。", "This option will become active 48 hours after contact details are shared.": "此选项将在联系方式共享48小时后启用。", - "The 'Share Result' option will become active after 48 hours.": "“分享结果”选项将在48小时后启用。" + "The 'Share Result' option will become active after 48 hours.": "“分享结果”选项将在48小时后启用。", + "Advisors": "顾问", + "Contact Advisors": "联系顾问", + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "如果遇到任何问题,请随时通过 WhatsApp 联系我们的顾问" } From 6d961ee87869d055d0a3b69be244850c50196faa Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Thu, 10 Sep 2026 17:26:54 +0330 Subject: [PATCH 16/27] feat: add multi-language support files and implement question flow and detail client components --- .../[slug]/question-detail-client.tsx | 4 +++ .../Componentes/test-questions-flow.tsx | 25 +++++++++++++------ src/translations/locales/ar.json | 4 ++- src/translations/locales/az.json | 4 ++- src/translations/locales/bn.json | 4 ++- src/translations/locales/da.json | 8 +++--- src/translations/locales/de.json | 8 +++--- src/translations/locales/en.json | 7 +++--- src/translations/locales/es.json | 8 +++--- src/translations/locales/fa.json | 4 ++- src/translations/locales/fr.json | 8 +++--- src/translations/locales/gu.json | 8 +++--- src/translations/locales/ha.json | 8 +++--- src/translations/locales/he.json | 4 ++- src/translations/locales/hi.json | 8 +++--- src/translations/locales/id.json | 4 ++- src/translations/locales/ks.json | 4 ++- src/translations/locales/pt.json | 4 ++- src/translations/locales/ru.json | 6 +++-- src/translations/locales/sw.json | 4 ++- src/translations/locales/tg.json | 4 ++- src/translations/locales/tr.json | 4 ++- src/translations/locales/ul.json | 4 ++- src/translations/locales/ur.json | 4 ++- src/translations/locales/uz.json | 4 ++- src/translations/locales/zh.json | 8 +++--- 26 files changed, 111 insertions(+), 51 deletions(-) diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 731a252..06d2d2c 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -745,6 +745,10 @@ export default function QuestionDetailClient({ questions={activeTestQuestions} closeLabel={closeLabel} informationLabel={informationLabel} + previousLabel={t["Previous"] || (locale === "fa" ? "قبلی" : "Previous")} + nextLabel={t["Next"] || (locale === "fa" ? "بعدی" : "Next")} + finishLabel={t["Finish"] || (locale === "fa" ? "پایان" : "Finish")} + stepsLabel={t["Steps"] || "Steps"} onClose={() => { setIsTestStarted(false); handleExit(); diff --git a/src/components/Componentes/test-questions-flow.tsx b/src/components/Componentes/test-questions-flow.tsx index 71dfd8b..8df8fb8 100644 --- a/src/components/Componentes/test-questions-flow.tsx +++ b/src/components/Componentes/test-questions-flow.tsx @@ -32,6 +32,7 @@ type TestQuestionsFlowProps = { closeLabel: string; informationLabel: string; previousLabel?: string; + nextLabel?: string; finishLabel?: string; stepsLabel?: string; onFinish?: (answers: Record) => void; @@ -79,15 +80,23 @@ export default function TestQuestionsFlow({ questions, closeLabel, informationLabel, - previousLabel = "Previous", - finishLabel = "Finish", - stepsLabel = "Steps", + previousLabel, + nextLabel, + finishLabel, + stepsLabel, onFinish, onClose, draftStorageKey, }: TestQuestionsFlowProps) { const router = useRouter(); - const { locale } = useI18n(); + const { locale, dictionary: t } = useI18n(); + const resolvedStepsLabel = stepsLabel ?? (t["Steps"] || "Steps"); + const resolvedPreviousLabel = + previousLabel ?? (t["Previous"] || (locale === "fa" ? "قبلی" : "Previous")); + const resolvedNextLabel = + nextLabel ?? (t["Next"] || (locale === "fa" ? "بعدی" : "Next")); + const resolvedFinishLabel = + finishLabel ?? (t["Finish"] || (locale === "fa" ? "پایان" : "Finish")); const [currentIndex, setCurrentIndex] = useState(0); const [answers, setAnswers] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); @@ -261,7 +270,7 @@ export default function TestQuestionsFlow({ {/* Progress Section */}

- {stepsLabel} + {resolvedStepsLabel} {currentIndex + 1} /{totalQuestions} @@ -384,7 +393,7 @@ export default function TestQuestionsFlow({ className="flex items-center gap-1.5 group-14 font-semibold text-[#4A4A4A] disabled:opacity-30 disabled:pointer-events-none transition-opacity py-2 px-1" > - {locale === "fa" ? "قبلی" : previousLabel} + {resolvedPreviousLabel} {/* Next Button */} @@ -397,7 +406,7 @@ export default function TestQuestionsFlow({ onClick={handleNext} className="flex items-center gap-1.5 group-14 font-semibold text-[#4A4A4A] transition-opacity py-2 px-1" > - {locale === "fa" ? "بعدی" : "Next"} + {resolvedNextLabel} )} @@ -414,7 +423,7 @@ export default function TestQuestionsFlow({ isLoading={isSubmitting} onClick={handleFinishSubmit} > - {finishLabel} + {resolvedFinishLabel} ) : null}
diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index e58c1f7..f5fd461 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -2099,5 +2099,7 @@ "This option will become active 48 hours after contact details are shared.": "سيكون هذا الخيار متاحاً بعد مرور 48 ساعة على مشاركة بيانات الاتصال.", "The 'Share Result' option will become active after 48 hours.": "سيكون خيار «تسجيل النتيجة» متاحاً بعد مرور 48 ساعة.", "Advisors": "المستشارون", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "إذا واجهت أي مشكلات، فلا تتردد في الاتصال بمستشارينا في WhatsApp" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "إذا واجهت أي مشكلات، فلا تتردد في الاتصال بمستشارينا في WhatsApp", + "Steps": "الخطوات", + "Previous": "السابق" } diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index 251008b..b2fb81e 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "«Nəticəni paylaşın» seçimi 48 saatdan sonra aktivləşəcək.", "Advisors": "Məsləhətçilər", "Contact Advisors": "Məsləhətçilərlə əlaqə saxlayın", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hər hansı problemlə qarşılaşsanız, WhatsApp-da məsləhətçilərimizlə əlaqə saxlayın" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hər hansı problemlə qarşılaşsanız, WhatsApp-da məsləhətçilərimizlə əlaqə saxlayın", + "Steps": "Addımlar", + "Previous": "Əvvəlki" } diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index 9d3e86d..5a2aeb0 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "৪৮ ঘণ্টা পর 'ফলাফল শেয়ার করুন' বিকল্পটি সক্রিয় হবে।", "Advisors": "পরামর্শক", "Contact Advisors": "পরামর্শকদের সাথে যোগাযোগ করুন", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "আপনি যদি কোনো সমস্যার সম্মুখীন হন, অনুগ্রহ করে WhatsApp-এ আমাদের পরামর্শকদের সাথে নির্দ্বিধায় যোগাযোগ করুন" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "আপনি যদি কোনো সমস্যার সম্মুখীন হন, অনুগ্রহ করে WhatsApp-এ আমাদের পরামর্শকদের সাথে নির্দ্বিধায় যোগাযোগ করুন", + "Steps": "ধাপ", + "Previous": "পূর্ববর্তী" } diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 85a5559..49181e6 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "Afslut", "Fit/Average": "Fit/Gennemsnit", "Flexible": "Fleksibel", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "Aldrig brugt", "New Marriage Proposal": "Nyt Ægteskabsforslag", "New Match": "New Match", - "Next": "Next", + "Next": "Næste", "Next Page": "Next Page", "No Active Subscription": "Intet aktivt abonnement", "No Contact Received": "Ingen kontakt modtaget", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "Valgmuligheden 'Del resultat' bliver aktiv efter 48 timer.", "Advisors": "Rådgivere", "Contact Advisors": "Kontakt rådgivere", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hvis du støder på problemer, er du velkommen til at kontakte vores rådgivere på WhatsApp" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hvis du støder på problemer, er du velkommen til at kontakte vores rådgivere på WhatsApp", + "Steps": "Trin", + "Previous": "Forrige" } diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index 6869860..4d408e9 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "Abschließen", "Fit/Average": "Fit/Durchschnitt", "Flexible": "Flexibel", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "Nie benutzt", "New Marriage Proposal": "Neuer Heiratsantrag", "New Match": "New Match", - "Next": "Next", + "Next": "Weiter", "Next Page": "Next Page", "No Active Subscription": "Kein aktives Abonnement", "No Contact Received": "Kein Kontakt erhalten", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "Die Option 'Ergebnis teilen' wird nach 48 Stunden aktiviert.", "Advisors": "Berater", "Contact Advisors": "Berater kontaktieren", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Bei Problemen kontaktieren Sie bitte unsere Berater über WhatsApp" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Bei Problemen kontaktieren Sie bitte unsere Berater über WhatsApp", + "Steps": "Schritte", + "Previous": "Zurück" } diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 6922fdc..9cef4b7 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -2008,7 +2008,7 @@ "Cooperation with Law Enforcement": "Cooperation with Law Enforcement", "In the event of cybercrimes, fraud, or threats, the application will refer the necessary information to judicial and law enforcement authorities.": "In the event of cybercrimes, fraud, or threats, the application will refer the necessary information to judicial and law enforcement authorities.", "Dispute Resolution": "Dispute Resolution", - "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.", + "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.", "7. Disclaimer and In-Person Meetings": "7. Disclaimer and In-Person Meetings", "Role of the Application": "Role of the Application", "This platform is only a facilitator for initial communication and does not replace family background research, counseling, and pre-marital investigations.": "This platform is only a facilitator for initial communication and does not replace family background research, counseling, and pre-marital investigations.", @@ -2080,7 +2080,6 @@ "ACCOUNT SUSPENDED": "ACCOUNT SUSPENDED", "Account Suspended": "Account Suspended", "Account Suspended Notice": "In accordance with platform guidelines and supervisory review, access to your account services has been suspended. For further details or inquiries, please contact our advisors.", - "Any legal dispute must first be pursued through application support. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.", "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.": "Any legal dispute must first be pursued through application advisors. If no agreement is reached, the competent authority will be the courts of the city where the application is registered.", "Ban Reason": "Suspension Reason:", "Profile editing is temporarily disabled": "Profile editing is disabled during account suspension.", @@ -2089,5 +2088,7 @@ "unserious_intent": "Lack of serious intention regarding marriage", "harassment_complaint": "Reports or complaints regarding inappropriate conduct", "commercial_activity": "Commercial, promotional, or non-marriage activities", - "admin_discretion": "Administrative and supervisory team discretion" + "admin_discretion": "Administrative and supervisory team discretion", + "Steps": "Steps", + "Previous": "Previous" } diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index 05ea896..c97839c 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "Finalizar", "Fit/Average": "Ajuste/Promedio", "Flexible": "Flexibles", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "Nunca usado", "New Marriage Proposal": "Nueva propuesta de matrimonio", "New Match": "New Match", - "Next": "Next", + "Next": "Siguiente", "Next Page": "Next Page", "No Active Subscription": "Sin suscripción activa", "No Contact Received": "Contacto no recibido", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "La opción 'Compartir resultado' se activará después de 48 horas.", "Advisors": "Consejeros", "Contact Advisors": "Contactar consejeros", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si tiene algún problema, no dude en ponerse en contacto con nuestros consejeros en WhatsApp" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si tiene algún problema, no dude en ponerse en contacto con nuestros consejeros en WhatsApp", + "Steps": "Pasos", + "Previous": "Anterior" } diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 481b11d..172bd70 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -2109,5 +2109,7 @@ "unserious_intent": "عدم جدیت در امر ازدواج و نقض اهداف سامانه", "harassment_complaint": "گزارش نارضایتی یا ایجاد مزاحمت برای سایر کاربران", "commercial_activity": "فعالیت تجاری، تبلیغاتی یا مقاصد غیرمرتبط", - "admin_discretion": "صلاحدید و تصمیم کارشناسی مدیریت سامانه" + "admin_discretion": "صلاحدید و تصمیم کارشناسی مدیریت سامانه", + "Steps": "مراحل", + "Previous": "قبلی" } diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 1ad4ff8..3af31e3 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "Terminer", "Fit/Average": "Ajustement/Moyenne", "Flexible": "Flexible", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "Jamais utilisé", "New Marriage Proposal": "Nouvelle demande en mariage", "New Match": "New Match", - "Next": "Next", + "Next": "Suivant", "Next Page": "Next Page", "No Active Subscription": "Aucun abonnement actif", "No Contact Received": "Aucun contact reçu", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "L'option « Partager le résultat » sera activée après 48 heures.", "Advisors": "Conseillers", "Contact Advisors": "Contacter les conseillers", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si vous rencontrez des problèmes, n'hésitez pas à contacter nos conseillers sur WhatsApp" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si vous rencontrez des problèmes, n'hésitez pas à contacter nos conseillers sur WhatsApp", + "Steps": "Étapes", + "Previous": "Précédent" } diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index d9eee96..9e3fd9a 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "સમાપ્ત", "Fit/Average": "ફિટ/સરેરાશ", "Flexible": "લવચીક", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "ક્યારેય ઉપયોગ કર્યો નથી", "New Marriage Proposal": "લગ્નનો નવો પ્રસ્તાવ", "New Match": "New Match", - "Next": "Next", + "Next": "આગળ", "Next Page": "Next Page", "No Active Subscription": "કોઈ સક્રિય સબ્સ્ક્રિપ્શન નથી", "No Contact Received": "કોઈ સંપર્ક મળ્યો નથી", @@ -2099,5 +2099,7 @@ "The 'Share Result' option will become active after 48 hours.": "'પરિણામ શેર કરો' વિકલ્પ 48 કલાક પછી સક્રિય થશે.", "Advisors": "સલાહકારો", "Contact Advisors": "સલાહકારોનો સંપર્ક કરો", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "જો તમને કોઈ સમસ્યા આવે, તો કૃપા કરીને WhatsApp પર અમારા સલાહકારોનો સંપર્ક કરો" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "જો તમને કોઈ સમસ્યા આવે, તો કૃપા કરીને WhatsApp પર અમારા સલાહકારોનો સંપર્ક કરો", + "Steps": "પગલાં", + "Previous": "પાછલું" } diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 9cc1338..54da21c 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "Kammala", "Fit/Average": "Fit/Matsakaici", "Flexible": "M", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "Ba a taɓa amfani da shi ba", "New Marriage Proposal": "Sabuwar Shawarar Aure", "New Match": "New Match", - "Next": "Next", + "Next": "Na gaba", "Next Page": "Next Page", "No Active Subscription": "Babu Biyan Kuɗi Mai Aiki", "No Contact Received": "Ba a karɓi tuntuɓa ba", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "Za a kunna zaɓin 'Raba Sakamako' bayan awanni 48.", "Advisors": "Masu ba da shawara", "Contact Advisors": "Tuntuɓi masu ba da shawara", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Idan kun ci karo da matsala, ku tuntuɓi masu ba da shawara a WhatsApp" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Idan kun ci karo da matsala, ku tuntuɓi masu ba da shawara a WhatsApp", + "Steps": "Matakai", + "Previous": "Baya" } diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index c6279db..1eeea6c 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "האפשרות 'שתף תוצאה' תופעל לאחר 48 שעות.", "Advisors": "יועצים", "Contact Advisors": "פנייה ליועצים", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "אם נתקלת בבעיה כלשהי, ניתן לפנות ליועצים שלנו בוואטסאפ." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "אם נתקלת בבעיה כלשהי, ניתן לפנות ליועצים שלנו בוואטסאפ.", + "Steps": "שלבים", + "Previous": "הקודם" } diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index 1c7417e..0e458b2 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "समाप्त", "Fit/Average": "फ़िट/औसत", "Flexible": "लचीला", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "कभी उपयोग नहीं किया गया", "New Marriage Proposal": "नये विवाह का प्रस्ताव", "New Match": "New Match", - "Next": "Next", + "Next": "आगे", "Next Page": "Next Page", "No Active Subscription": "कोई सक्रिय सदस्यता नहीं", "No Contact Received": "कोई संपर्क प्राप्त नहीं हुआ", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "'परिणाम साझा करें' विकल्प 48 घंटे बाद सक्रिय हो जाएगा।", "Advisors": "सलाहकार", "Contact Advisors": "सलाहकारों से संपर्क करें", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "किसी भी समस्या के लिए, कृपया व्हाट्सएप पर हमारे सलाहकारों से संपर्क करें" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "किसी भी समस्या के लिए, कृपया व्हाट्सएप पर हमारे सलाहकारों से संपर्क करें", + "Steps": "चरण", + "Previous": "पिछला" } diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index 6fb31fe..ebfb45c 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "Opsi 'Bagikan Hasil' akan aktif setelah 48 jam.", "Advisors": "Penasihat", "Contact Advisors": "Hubungi Penasihat", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Jika Anda mengalami kendala, jangan ragu untuk menghubungi penasihat kami di WhatsApp." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Jika Anda mengalami kendala, jangan ragu untuk menghubungi penasihat kami di WhatsApp.", + "Steps": "Langkah", + "Previous": "Sebelumnya" } diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index 3c734d9..4e0266f 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "'نتیجہ شیئر کٔرِو' آپشن گژھِ ۴۸ گنٛٹن پتہٕ فعال۔", "Advisors": "مشیر", "Contact Advisors": "مشیرن سٟتؠ رابطہ کٔریو", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "کانہہ دشواری پیش یِنہٕ وزِ ہیکیو واٹس ایپ پؠٹھ مشیرن سٟتؠ رابطہ کٔرِتھ۔" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "کانہہ دشواری پیش یِنہٕ وزِ ہیکیو واٹس ایپ پؠٹھ مشیرن سٟتؠ رابطہ کٔرِتھ۔", + "Steps": "مراحل", + "Previous": "پچھلا" } diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index cbc9ba7..4cfe83d 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "A opção 'Compartilhar resultado' ficará ativa após 48 horas.", "Advisors": "Consultores", "Contact Advisors": "Contatar consultores", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Se encontrar algum problema, entre em contato com nossos consultores no WhatsApp." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Se encontrar algum problema, entre em contato com nossos consultores no WhatsApp.", + "Steps": "Etapas", + "Previous": "Anterior" } diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 554da24..0992525 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -227,7 +227,7 @@ "Final Match Introduction": "Введение в финальный матч", "Final Notice": "Последнее уведомление", "Find Matches": "Найти совпадения", - "Finish": "Заканчивать", + "Finish": "Завершить", "Fit/Average": "Подходит/Средний", "Flexible": "Гибкий", "Form Sections Data": "Данные разделов формы", @@ -2093,5 +2093,7 @@ "The 'Share Result' option will become active after 48 hours.": "Опция «Поделиться результатом» станет активна через 48 часов.", "Advisors": "Консультанты", "Contact Advisors": "Связаться с консультантами", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Если у вас возникнут какие-либо проблемы, пожалуйста, свяжитесь с нашими консультантами в WhatsApp." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Если у вас возникнут какие-либо проблемы, пожалуйста, свяжитесь с нашими консультантами в WhatsApp.", + "Steps": "Шаги", + "Previous": "Предыдущий" } diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 75d5b9d..61e5ccb 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "Chaguo la 'Shiriki Matokeo' litafanya kazi baada ya saa 48.", "Advisors": "Washauri", "Contact Advisors": "Wasiliana na Washauri", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Ukikumbana na changamoto yoyote, jisikie huru kuwasiliana na washauri wetu kupitia WhatsApp." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Ukikumbana na changamoto yoyote, jisikie huru kuwasiliana na washauri wetu kupitia WhatsApp.", + "Steps": "Hatua", + "Previous": "Iliyopita" } diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index 25e05b1..99c83c1 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "Гузинаи «Сабти натиҷа» баъд аз 48 соат фаъол хоҳад шуд.", "Advisors": "Мушовирон", "Contact Advisors": "Тамос бо мушовирон", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Агар мушкиле пеш ояд, лутфан ба мушовирони мо дар WhatsApp муроҷиат кунед." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Агар мушкиле пеш ояд, лутфан ба мушовирони мо дар WhatsApp муроҷиат кунед.", + "Steps": "Марҳилаҳо", + "Previous": "Қаблӣ" } diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index 70c8a1b..c8fc44d 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "'Sonucu Paylaş' seçeneği 48 saat sonra aktif olacaktır.", "Advisors": "Danışmanlar", "Contact Advisors": "Danışmanlarla İletişime Geçin", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Herhangi bir sorunla karşılaşırsanız WhatsApp üzerinden danışmanlarımızla iletişime geçebilirsiniz." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Herhangi bir sorunla karşılaşırsanız WhatsApp üzerinden danışmanlarımızla iletişime geçebilirsiniz.", + "Steps": "Adımlar", + "Previous": "Önceki" } diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index 978ce07..b32340a 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "«نەتىجىنى ھەمبەھىرلەش» تاللىشى 48 سائەتتىن كېيىن ئاكتىپلىنىدۇ.", "Advisors": "Musheeran", "Contact Advisors": "Musheeran se Rabta Karein", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar koi masla darpesh ho to WhatsApp par hamare musheeran se bila-jhijhak rabta karein." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar koi masla darpesh ho to WhatsApp par hamare musheeran se bila-jhijhak rabta karein.", + "Steps": "Marahil", + "Previous": "Pichla" } diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 8f4f9bc..410ac70 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "'نتیجہ شیئر کریں' کا آپشن 48 گھنٹے بعد فعال ہو جائے گا۔", "Advisors": "مشیران", "Contact Advisors": "مشیروں سے رابطہ کریں", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "اگر کوئی مسئلہ درپیش ہو تو واٹس ایپ پر ہمارے مشیروں سے بلا جھجھک رابطہ کریں۔" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "اگر کوئی مسئلہ درپیش ہو تو واٹس ایپ پر ہمارے مشیروں سے بلا جھجھک رابطہ کریں۔", + "Steps": "مراحل", + "Previous": "پچھلا" } diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index e37cd02..5c60a91 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -2362,5 +2362,7 @@ "The 'Share Result' option will become active after 48 hours.": "'Natijani ulashish' opsiyasi 48 soatdan keyin faollashadi.", "Advisors": "Maslahatchilar", "Contact Advisors": "Maslahatchilar bilan bog'lanish", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar biron bir muammo yuzaga kelsa, WhatsApp orqali maslahatchilarimiz bilan bog'laning." + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar biron bir muammo yuzaga kelsa, WhatsApp orqali maslahatchilarimiz bilan bog'laning.", + "Steps": "Bosqichlar", + "Previous": "Oldingi" } diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index e769c64..9a2b0e1 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -225,7 +225,7 @@ "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", "Find Matches": "Find Matches", - "Finish": "Finish", + "Finish": "完成", "Fit/Average": "适合/平均", "Flexible": "灵活", "Form Sections Data": "Form Sections Data", @@ -372,7 +372,7 @@ "Never used": "从未使用过", "New Marriage Proposal": "新求婚", "New Match": "New Match", - "Next": "Next", + "Next": "下一步", "Next Page": "Next Page", "No Active Subscription": "没有活跃订阅", "No Contact Received": "未收到联系", @@ -2097,5 +2097,7 @@ "The 'Share Result' option will become active after 48 hours.": "“分享结果”选项将在48小时后启用。", "Advisors": "顾问", "Contact Advisors": "联系顾问", - "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "如果遇到任何问题,请随时通过 WhatsApp 联系我们的顾问" + "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "如果遇到任何问题,请随时通过 WhatsApp 联系我们的顾问", + "Steps": "步骤", + "Previous": "上一步" } From f89f3ead923d8aadc48229cf761202bf3d09a468 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 12 Sep 2026 13:10:18 +0330 Subject: [PATCH 17/27] feat: add multi-language localization support with locale files, translation formatters, and question components --- .../[slug]/question-detail-client.tsx | 3 +- .../Componentes/question-card.test.tsx | 66 +++++++++++++++++++ src/components/Componentes/question-card.tsx | 3 +- .../required-steps-card-locale.test.tsx | 66 +++++++++++++++++++ src/translations/config.ts | 2 + src/translations/format-estimate.test.ts | 39 +++++++++++ src/translations/format-estimate.ts | 35 ++++++++++ src/translations/locales/ar.json | 19 +++++- src/translations/locales/az.json | 19 +++++- src/translations/locales/bn.json | 19 +++++- src/translations/locales/da.json | 41 ++++++++---- src/translations/locales/de.json | 41 ++++++++---- src/translations/locales/en.json | 19 +++++- src/translations/locales/es.json | 41 ++++++++---- src/translations/locales/fa.json | 19 +++++- src/translations/locales/fr.json | 41 ++++++++---- src/translations/locales/gu.json | 41 ++++++++---- src/translations/locales/ha.json | 41 ++++++++---- src/translations/locales/he.json | 19 +++++- src/translations/locales/hi.json | 41 ++++++++---- src/translations/locales/id.json | 19 +++++- src/translations/locales/ks.json | 19 +++++- src/translations/locales/pt.json | 19 +++++- src/translations/locales/ru.json | 19 +++++- src/translations/locales/sw.json | 19 +++++- src/translations/locales/tg.json | 19 +++++- src/translations/locales/tr.json | 19 +++++- src/translations/locales/ul.json | 19 +++++- src/translations/locales/ur.json | 19 +++++- src/translations/locales/uz.json | 19 +++++- src/translations/locales/zh.json | 41 ++++++++---- src/translations/path_to_english.json | 3 +- 32 files changed, 734 insertions(+), 115 deletions(-) create mode 100644 src/components/Componentes/question-card.test.tsx create mode 100644 src/components/Componentes/required-steps-card-locale.test.tsx create mode 100644 src/translations/format-estimate.test.ts create mode 100644 src/translations/format-estimate.ts diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 06d2d2c..3f92a32 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -49,6 +49,7 @@ import { import { isQuestionVisible, isQuestionRequired } from "@/lib/conditional-rules"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { defaultLocale, type Locale } from "@/translations/config"; +import { formatEstimateTime } from "@/translations/format-estimate"; import { useI18n } from "@/translations/provider"; import { useCurrentProfileId } from "@/hooks/use-current-profile-id"; import { @@ -811,7 +812,7 @@ export default function QuestionDetailClient({
= ar; +let mockLocale = "ar"; + +vi.mock("@/translations/provider", () => ({ + useI18n: () => ({ + dictionary: mockDictionary, + locale: mockLocale, + }), +})); + +const mockItem: QuestionListItem = { + slug: "personal_identity", + title: "البيانات الشخصية والتعريفية", + estimate: "2 min", + progress: 100, + icon: "profile", + required: true, + summary: "", + checkpoints: [], + questions: [], +}; + +describe("QuestionCard - Time Estimate Localization", () => { + it("translates '2 min' to Arabic '2 دقيقة' in Arabic locale", () => { + mockDictionary = ar; + mockLocale = "ar"; + + render(); + + // In Arabic: "تقدير الوقت: 2 دقيقة" + expect(screen.getByText("تقدير الوقت: 2 دقيقة")).toBeDefined(); + }); + + it("translates '3 min' to Arabic '3 دقائق' in Arabic locale", () => { + mockDictionary = ar; + mockLocale = "ar"; + + const item3Min: QuestionListItem = { + ...mockItem, + title: "التعليم، والمهنة، والوضع المالي", + estimate: "3 min", + }; + + render(); + + // In Arabic: "تقدير الوقت: 3 دقائق" + expect(screen.getByText("تقدير الوقت: 3 دقائق")).toBeDefined(); + }); + + it("translates '2 min' to Persian '۲ دقیقه' in Persian locale", () => { + mockDictionary = fa; + mockLocale = "fa"; + + render(); + + // In Persian: "زمان تقریبی: ۲ دقیقه" + expect(screen.getByText("زمان تقریبی: ۲ دقیقه")).toBeDefined(); + }); +}); diff --git a/src/components/Componentes/question-card.tsx b/src/components/Componentes/question-card.tsx index fd2796f..41958d7 100644 --- a/src/components/Componentes/question-card.tsx +++ b/src/components/Componentes/question-card.tsx @@ -7,6 +7,7 @@ import { resolveSectionIcon, } from "@/lib/schema-adapter"; import { localizePath } from "@/translations/config"; +import { formatEstimateTime } from "@/translations/format-estimate"; import { useI18n } from "@/translations/provider"; import { SectionIcon } from "./section-icon"; import { UiIcon, type UiIconName } from "./ui-icon"; @@ -124,7 +125,7 @@ export function QuestionCard({ {item.title}

- {t["Estimate time"]}: {item.estimate} + {t["Estimate time"]}: {formatEstimateTime(item.estimate, t)}

diff --git a/src/components/Componentes/required-steps-card-locale.test.tsx b/src/components/Componentes/required-steps-card-locale.test.tsx new file mode 100644 index 0000000..f63d9da --- /dev/null +++ b/src/components/Componentes/required-steps-card-locale.test.tsx @@ -0,0 +1,66 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import RequiredStepsCard from "./required-steps-card"; +import fr from "@/translations/locales/fr.json"; +import fa from "@/translations/locales/fa.json"; +import ar from "@/translations/locales/ar.json"; + +let mockDictionary: Record = fr; + +vi.mock("@/translations/provider", () => ({ + useI18n: () => ({ + dictionary: mockDictionary, + locale: "fr", + }), +})); + +describe("RequiredStepsCard - Locale Translations", () => { + afterEach(() => { + cleanup(); + }); + + it("renders French translations correctly when incomplete (0/9)", () => { + mockDictionary = fr; + render(); + + expect(screen.getByText("Étapes obligatoires")).toBeDefined(); + expect( + screen.getByText( + "Veuillez compléter les informations requises afin que nous puissions trouver les profils qui vous correspondent", + ), + ).toBeDefined(); + expect(screen.getByText("0/9")).toBeDefined(); + }); + + it("renders French translations correctly when completed (9/9)", () => { + mockDictionary = fr; + render(); + + expect(screen.getByText("Étapes obligatoires")).toBeDefined(); + expect( + screen.getByText( + "Vous pouvez maintenant envoyer votre demande afin que nous commencions à chercher le profil qui vous convient", + ), + ).toBeDefined(); + expect(screen.getByText("9/9")).toBeDefined(); + }); + + it("renders Persian translations correctly", () => { + mockDictionary = fa; + render(); + + expect(screen.getByText("مراحل ضروری")).toBeDefined(); + expect( + screen.getByText( + "لطفا اطلاعات ضروری را کامل کنید تا بتوانیم گزینه‌های مناسب را پیدا کنیم", + ), + ).toBeDefined(); + }); + + it("renders Arabic translations correctly", () => { + mockDictionary = ar; + render(); + + expect(screen.getByText("الخطوات المطلوبة")).toBeDefined(); + }); +}); diff --git a/src/translations/config.ts b/src/translations/config.ts index a66e0fe..e481f69 100644 --- a/src/translations/config.ts +++ b/src/translations/config.ts @@ -66,3 +66,5 @@ export function localizePath(pathname: string, locale: Locale) { return `/${locale}${suffix}`; } + +export { formatEstimateTime } from "./format-estimate"; diff --git a/src/translations/format-estimate.test.ts b/src/translations/format-estimate.test.ts new file mode 100644 index 0000000..01c116d --- /dev/null +++ b/src/translations/format-estimate.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { formatEstimateTime } from "./format-estimate"; +import ar from "./locales/ar.json"; +import en from "./locales/en.json"; +import fa from "./locales/fa.json"; + +describe("formatEstimateTime", () => { + it("translates estimates correctly in Arabic", () => { + expect(formatEstimateTime("2 min", ar)).toBe("2 دقيقة"); + expect(formatEstimateTime("3 min", ar)).toBe("3 دقائق"); + expect(formatEstimateTime("4 min", ar)).toBe("4 دقائق"); + expect(formatEstimateTime("5 min", ar)).toBe("5 دقائق"); + expect(formatEstimateTime("16 min", ar)).toBe("16 دقيقة"); + expect(formatEstimateTime("min", ar)).toBe("دقيقة"); + }); + + it("translates estimates correctly in Persian", () => { + expect(formatEstimateTime("2 min", fa)).toBe("۲ دقیقه"); + expect(formatEstimateTime("3 min", fa)).toBe("۳ دقیقه"); + expect(formatEstimateTime("4 min", fa)).toBe("۴ دقیقه"); + expect(formatEstimateTime("16 min", fa)).toBe("۱۶ دقیقه"); + expect(formatEstimateTime("min", fa)).toBe("دقیقه"); + }); + + it("handles English locale properly", () => { + expect(formatEstimateTime("2 min", en)).toBe("2 min"); + expect(formatEstimateTime("3 min", en)).toBe("3 min"); + expect(formatEstimateTime("16 min", en)).toBe("16 min"); + expect(formatEstimateTime("min", en)).toBe("min"); + }); + + it("falls back gracefully for unknown counts or missing keys", () => { + const mockT = { min: "دقيقة" }; + expect(formatEstimateTime("99 min", mockT)).toBe("99 دقيقة"); + expect(formatEstimateTime("", mockT)).toBe(""); + expect(formatEstimateTime(null, mockT)).toBe(""); + expect(formatEstimateTime("unformatted string", mockT)).toBe("unformatted string"); + }); +}); diff --git a/src/translations/format-estimate.ts b/src/translations/format-estimate.ts new file mode 100644 index 0000000..798c63a --- /dev/null +++ b/src/translations/format-estimate.ts @@ -0,0 +1,35 @@ +/** + * Localizes section estimated time strings (e.g., "2 min", "3 min", "16 min", "5 minutes") + * using the current translation dictionary. + */ +export function formatEstimateTime( + estimate: string | undefined | null, + t: Record, +): string { + if (!estimate) return ""; + const trimmed = estimate.trim(); + if (!trimmed) return ""; + + // 1. Direct match in dictionary (e.g., "2 min", "5 minutes", "15 min") + if (t[trimmed]) return t[trimmed]; + + // 2. Pattern match: " min" or " minutes" + const match = trimmed.match(/^(\d+)\s*(?:min|minutes?)$/i); + if (match) { + const count = match[1]; + + // Try "{count} min" (e.g. "2 min") + const minKey = `${count} min`; + if (t[minKey]) return t[minKey]; + + // Try "{count} minutes" (e.g. "2 minutes") + const minutesKey = `${count} minutes`; + if (t[minutesKey]) return t[minutesKey]; + + // Fallback: "{count} " + const unit = t["min"] || "min"; + return `${count} ${unit}`; + } + + return trimmed; +} diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index f5fd461..873fe50 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -2101,5 +2101,22 @@ "Advisors": "المستشارون", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "إذا واجهت أي مشكلات، فلا تتردد في الاتصال بمستشارينا في WhatsApp", "Steps": "الخطوات", - "Previous": "السابق" + "Previous": "السابق", + "min": "دقيقة", + "1 minute": "دقيقة واحدة", + "10 minutes": "10 دقائق", + "15 minutes": "15 دقيقة", + "16 minutes": "16 دقيقة", + "20 minutes": "20 دقيقة", + "30 minutes": "30 دقيقة", + "1 min": "1 دقيقة", + "2 min": "2 دقيقة", + "3 min": "3 دقائق", + "4 min": "4 دقائق", + "5 min": "5 دقائق", + "6 min": "6 دقائق", + "8 min": "8 دقائق", + "10 min": "10 دقائق", + "15 min": "15 دقيقة", + "16 min": "16 دقيقة" } diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index b2fb81e..6f39530 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -2099,5 +2099,22 @@ "Contact Advisors": "Məsləhətçilərlə əlaqə saxlayın", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hər hansı problemlə qarşılaşsanız, WhatsApp-da məsləhətçilərimizlə əlaqə saxlayın", "Steps": "Addımlar", - "Previous": "Əvvəlki" + "Previous": "Əvvəlki", + "min": "dəq", + "1 minute": "1 dəqiqə", + "10 minutes": "10 dəqiqə", + "15 minutes": "15 dəqiqə", + "16 minutes": "16 dəqiqə", + "20 minutes": "20 dəqiqə", + "30 minutes": "30 dəqiqə", + "1 min": "1 dəq", + "2 min": "2 dəq", + "3 min": "3 dəq", + "4 min": "4 dəq", + "5 min": "5 dəq", + "6 min": "6 dəq", + "8 min": "8 dəq", + "10 min": "10 dəq", + "15 min": "15 dəq", + "16 min": "16 dəq" } diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index 5a2aeb0..d1f90f3 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -2099,5 +2099,22 @@ "Contact Advisors": "পরামর্শকদের সাথে যোগাযোগ করুন", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "আপনি যদি কোনো সমস্যার সম্মুখীন হন, অনুগ্রহ করে WhatsApp-এ আমাদের পরামর্শকদের সাথে নির্দ্বিধায় যোগাযোগ করুন", "Steps": "ধাপ", - "Previous": "পূর্ববর্তী" + "Previous": "পূর্ববর্তী", + "min": "মিনিট", + "1 minute": "১ মিনিট", + "10 minutes": "১০ মিনিট", + "15 minutes": "১৫ মিনিট", + "16 minutes": "১৬ মিনিট", + "20 minutes": "২০ মিনিট", + "30 minutes": "৩০ মিনিট", + "1 min": "১ মিনিট", + "2 min": "২ মিনিট", + "3 min": "৩ মিনিট", + "4 min": "৪ মিনিট", + "5 min": "৫ মিনিট", + "6 min": "৬ মিনিট", + "8 min": "৮ মিনিট", + "10 min": "১০ মিনিট", + "15 min": "১৫ মিনিট", + "16 min": "১৬ মিনিট" } diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 49181e6..5226e0a 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "By, region eller kvarter", "Close and active": "Tæt og aktiv", - "Close questions list": "Close questions list", + "Close questions list": "Luk spørgsmålsliste", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "Indsamler detaljer om dit fysiske udseende, helbredstilstand og mentale velbefindende.", "Collects information about your educational background, employment status, and financial situation.": "Indsamler oplysninger om din uddannelsesmæssige baggrund, beskæftigelsesstatus og økonomiske situation.", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "Find matches", "Finish": "Afslut", "Fit/Average": "Fit/Gennemsnit", "Flexible": "Fleksibel", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "Hvis du støder på problemer, er du velkommen til at kontakte vores supportspecialister på WhatsApp", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "Hvis du har særlige forhold vedrørende arbejde, indkomst, leje, boligkøb, migration eller fremtidig bopæl, bedes du forklare kort.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "Hvis du fortsætter, giver vi den anden part besked, og efter deres godkendelse kan I se hinandens kontaktoplysninger.", - "Important Note": "Important Note", + "Important Note": "Vigtig bemærkning", "In career growth path": "I karrierevækstvej", "In treatment or recovery": "I behandling eller bedring", "Income is variable": "Indkomst er variabel", @@ -465,7 +465,7 @@ "Planner": "Planlægger", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "Beskriv venligst kort forældremyndighedsstatus, tidsplan for barnets samvær, eventuelle begrænsninger på flytning eller emigration og relaterede økonomiske forpligtelser. Undgå at angive barnets navn, den anden forælders navn eller unødige personlige oplysninger.", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "Angiv venligst kort typen af bopæl, varighed, omfang af den økonomiske eller plejemæssige støtte samt dens potentielle indvirkning på dit fremtidige bopæl, flytning eller fremtidige ægteskabelige forhold.", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "Udfyld venligst de påkrævede oplysninger, så vi kan finde passende matches til dig", "Please mention during the call that you were introduced by the Habib Marriage app.": "Nævn venligst under opkaldet, at I blev introduceret via Habib Marriage-appen.", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "Bemærk venligst, at afvisning af denne sag kan medføre en forsinkelse i at anbefale den næste kamp, ​​men der er absolut ingen forpligtelse til at acceptere, og du er helt fri til at vælge.", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "Faglig certifikat", "Profile Picture": "Profilbillede", "Profile is locked": "Profilen er låst", - "Profile registration": "Profile registration", + "Profile registration": "Profilregistrering", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "Velstående", "Provide more details if you have any health conditions or limitations.": "Angiv flere detaljer, hvis du har sundhedsmæssige forhold eller begrænsninger.", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "Påkrævede trin", "Residence Preference after Marriage": "Opholdspræference efter ægteskab", "Residence Status": "Bopælsstatus", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "Respektfuld og konventionel (Ingen intimitet) - Høflig interaktion med klare personlige grænser.", @@ -603,7 +603,7 @@ "Student": "Student", "Student Visa": "Studievisum", "Student and Job Seeking": "Studerende og jobsøgning", - "Submit": "Submit", + "Submit": "Indsend", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "Indsend endeligt resultat", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "Abonnement", "Subscription Status": "Abonnementsstatus", - "Support": "Support", + "Support": "Rådgivere", "Supporter of the current government, but a difference in view is not a red line.": "Tilhænger af den nuværende regering, men en forskel i synet er ikke en rød linje.", "Supporter of the current government; serious opposition from my spouse is a red line.": "Tilhænger af den nuværende regering; alvorlig modstand fra min ægtefælle er en rød streg.", "Sweden": "Sverige", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "Ja, de bor hos mig permanent.", "Yes, they live with me temporarily or periodically.": "Ja, de bor hos mig midlertidigt eller periodisk.", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "Du kan nu indsende din anmodning, så vi kan begynde at finde det rette match til dig", "You can now view their family's contact details and arrange further steps.": "Du kan nu se deres families kontaktoplysninger og arrangere de næste skridt.", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "Vilkår og betingelser", "user profiles": "user profiles", "user@example.com": "bruger@eksempel.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "{completed} af {total} påkrævede trin fuldført", "{days} days remaining of your subscription.": "{days} dage tilbage af dit abonnement.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "Tak for din feedback. For at fuldføre processen bedes du indsende det endelige resultat af dette bekendtskab/denne kontakt, så den endelige status kan fastlægges. Hvis den endelige status endnu ikke er afklaret, kan du forblive i denne tilstand, indtil det er afgjort.", @@ -2099,5 +2099,22 @@ "Contact Advisors": "Kontakt rådgivere", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Hvis du støder på problemer, er du velkommen til at kontakte vores rådgivere på WhatsApp", "Steps": "Trin", - "Previous": "Forrige" + "Previous": "Forrige", + "min": "min", + "1 minute": "1 minut", + "10 minutes": "10 minutter", + "15 minutes": "15 minutter", + "16 minutes": "16 minutter", + "20 minutes": "20 minutter", + "30 minutes": "30 minutter", + "1 min": "1 min", + "2 min": "2 min", + "3 min": "3 min", + "4 min": "4 min", + "5 min": "5 min", + "6 min": "6 min", + "8 min": "8 min", + "10 min": "10 min", + "15 min": "15 min", + "16 min": "16 min" } diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index 4d408e9..9e47f26 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "Stadt, Region oder Stadtteil", "Close and active": "Nah und aktiv", - "Close questions list": "Close questions list", + "Close questions list": "Fragenliste schließen", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "Sammelt Details über Ihr körperliches Erscheinungsbild, Ihren Gesundheitszustand und Ihr geistiges Wohlbefinden.", "Collects information about your educational background, employment status, and financial situation.": "Sammelt Informationen über Ihren Bildungshintergrund, Ihren Beschäftigungsstatus und Ihre finanzielle Situation.", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "Passende Vorschläge finden", "Finish": "Abschließen", "Fit/Average": "Fit/Durchschnitt", "Flexible": "Flexibel", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "Bei Problemen kontaktieren Sie bitte unsere Support-Spezialisten über WhatsApp", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "Sollten bei Ihnen besondere Bedingungen bezüglich Arbeit, Einkommen, Miete, Hauskauf, Migration oder zukünftigem Wohnort bestehen, erläutern Sie diese bitte kurz.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "Wenn Sie fortfahren, benachrichtigen wir die andere Partei und nach deren Genehmigung können Sie die Kontaktinformationen der anderen Partei einsehen.", - "Important Note": "Important Note", + "Important Note": "Wichtiger Hinweis", "In career growth path": "Im Karrierewachstumspfad", "In treatment or recovery": "In Behandlung oder Genesung", "Income is variable": "Das Einkommen ist variabel", @@ -465,7 +465,7 @@ "Planner": "Planer", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "Bitte erläutern Sie kurz den Sorgerechtsstatus, den Besuchsplan des Kindes, mögliche Einschränkungen bei Umzug oder Auswanderung sowie die damit verbundenen finanziellen Verpflichtungen. Vermeiden Sie die Nennung des Namens des Kindes, des anderen Elternteils oder unnötiger persönlicher Daten.", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "Bitte erläutern Sie kurz die Art der Verantwortung, ihre Dauer, das Ausmaß der finanziellen Unterstützung oder Pflege sowie deren potenzielle Auswirkungen auf Ihren Wohnort, einen Umzug oder die Bedingungen des zukünftigen Ehelebens.", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "Bitte füllen Sie die erforderlichen Informationen aus, damit wir passende Vorschläge für Sie finden können", "Please mention during the call that you were introduced by the Habib Marriage app.": "Bitte erwähnen Sie während des Telefonats, dass Sie über die Habib Marriage-App vermittelt wurden.", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "Bitte beachten Sie, dass die Ablehnung dieses Falles zu einer Verzögerung bei der Empfehlung des nächsten Spiels führen kann, es besteht jedoch absolut keine Verpflichtung zur Annahme und Sie können völlig frei entscheiden.", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "Berufszertifikat", "Profile Picture": "Profilbild", "Profile is locked": "Profil ist gesperrt", - "Profile registration": "Profile registration", + "Profile registration": "Profilregistrierung", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "Wohlhabend", "Provide more details if you have any health conditions or limitations.": "Geben Sie weitere Einzelheiten an, wenn Sie gesundheitliche Probleme oder Einschränkungen haben.", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "Erforderliche Schritte", "Residence Preference after Marriage": "Wohnsitzpräferenz nach der Heirat", "Residence Status": "Aufenthaltsstatus", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "Respektvoll und konventionell (keine Intimität) – Höflicher Umgang mit klaren persönlichen Grenzen.", @@ -603,7 +603,7 @@ "Student": "Student", "Student Visa": "Studentenvisum", "Student and Job Seeking": "Studenten- und Jobsuche", - "Submit": "Submit", + "Submit": "Absenden", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "Endergebnis einreichen", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "Abonnement", "Subscription Status": "Abonnementstatus", - "Support": "Support", + "Support": "Berater", "Supporter of the current government, but a difference in view is not a red line.": "Unterstützer der aktuellen Regierung, aber eine Meinungsverschiedenheit ist keine rote Linie.", "Supporter of the current government; serious opposition from my spouse is a red line.": "Unterstützer der aktuellen Regierung; Ernsthafter Widerstand meines Ehepartners ist eine rote Linie.", "Sweden": "Schweden", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "Ja, sie leben dauerhaft bei mir.", "Yes, they live with me temporarily or periodically.": "Ja, sie wohnen vorübergehend oder zeitweise bei mir.", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "Sie können Ihre Anfrage jetzt absenden, damit wir mit der Suche nach der passenden Person für Sie beginnen können", "You can now view their family's contact details and arrange further steps.": "Sie können nun die Kontaktdaten der Familie einsehen und die weiteren Schritte vereinbaren.", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "Allgemeine Geschäftsbedingungen", "user profiles": "user profiles", "user@example.com": "user@example.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "{completed} von {total} erforderlichen Schritten abgeschlossen", "{days} days remaining of your subscription.": "{days} Tage verbleibende Laufzeit Ihres Abonnements.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "Vielen Dank für Ihre Rückmeldung. Um den Vorgang abzuschließen, teilen Sie bitte das endgültige Ergebnis dieses Kennenlernens/Kontakts mit, damit der finale Status festgelegt werden kann. Falls das Ergebnis noch offen ist, können Sie in diesem Status verbleiben, bis eine Entscheidung feststeht.", @@ -2099,5 +2099,22 @@ "Contact Advisors": "Berater kontaktieren", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Bei Problemen kontaktieren Sie bitte unsere Berater über WhatsApp", "Steps": "Schritte", - "Previous": "Zurück" + "Previous": "Zurück", + "min": "Min.", + "1 minute": "1 Minute", + "10 minutes": "10 Minuten", + "15 minutes": "15 Minuten", + "16 minutes": "16 Minuten", + "20 minutes": "20 Minuten", + "30 minutes": "30 Minuten", + "1 min": "1 Min.", + "2 min": "2 Min.", + "3 min": "3 Min.", + "4 min": "4 Min.", + "5 min": "5 Min.", + "6 min": "6 Min.", + "8 min": "8 Min.", + "10 min": "10 Min.", + "15 min": "15 Min.", + "16 min": "16 Min." } diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 9cef4b7..403858a 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -2090,5 +2090,22 @@ "commercial_activity": "Commercial, promotional, or non-marriage activities", "admin_discretion": "Administrative and supervisory team discretion", "Steps": "Steps", - "Previous": "Previous" + "Previous": "Previous", + "min": "min", + "1 minute": "1 minute", + "10 minutes": "10 minutes", + "15 minutes": "15 minutes", + "16 minutes": "16 minutes", + "20 minutes": "20 minutes", + "30 minutes": "30 minutes", + "1 min": "1 min", + "2 min": "2 min", + "3 min": "3 min", + "4 min": "4 min", + "5 min": "5 min", + "6 min": "6 min", + "8 min": "8 min", + "10 min": "10 min", + "15 min": "15 min", + "16 min": "16 min" } diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index c97839c..b0c46cd 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "Ciudad, región o barrio", "Close and active": "Cercano y activo", - "Close questions list": "Close questions list", + "Close questions list": "Cerrar lista de preguntas", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "Recopila detalles sobre su apariencia física, estado de salud y bienestar mental.", "Collects information about your educational background, employment status, and financial situation.": "Recopila información sobre su formación académica, situación laboral y situación financiera.", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "Buscar coincidencias", "Finish": "Finalizar", "Fit/Average": "Ajuste/Promedio", "Flexible": "Flexibles", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "Si tiene algún problema, no dude en ponerse en contacto con nuestros especialistas de soporte en WhatsApp", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "Si tiene alguna condición especial en cuanto a trabajo, ingresos, alquiler, compra de vivienda, migración o futuro lugar de residencia, explique brevemente.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "Si continúa, notificaremos a la otra parte y, tras su aprobación, podrán ver la información de contacto de cada uno.", - "Important Note": "Important Note", + "Important Note": "Nota importante", "In career growth path": "En el camino del crecimiento profesional", "In treatment or recovery": "En tratamiento o recuperación", "Income is variable": "El ingreso es variable.", @@ -465,7 +465,7 @@ "Planner": "Planificador", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "Por favor, explique brevemente el estado de la custodia, el horario de visitas o presencia del niño, las posibles restricciones de traslado o inmigración y las obligaciones financieras relacionadas. Evite mencionar el nombre del niño, el del otro progenitor o detalles personales innecesarios.", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "Por favor, explique brevemente el tipo de responsabilidad, su duración, el nivel de apoyo financiero o de cuidado, y su posible impacto en el lugar de residencia, la reubicación o las condiciones de la futura vida matrimonial.", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "Por favor complete la información requerida para que podamos encontrar las parejas adecuadas para usted", "Please mention during the call that you were introduced by the Habib Marriage app.": "Por favor, mencione durante la llamada que fue presentado a través de la aplicación Habib Marriage.", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "Tenga en cuenta que rechazar este caso podría provocar un retraso en la recomendación del próximo partido, pero no existe ninguna obligación de aceptarlo y usted es totalmente libre de elegir.", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "Certificado Profesional", "Profile Picture": "Foto de perfil", "Profile is locked": "El perfil está bloqueado", - "Profile registration": "Profile registration", + "Profile registration": "Registro del perfil", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "próspero", "Provide more details if you have any health conditions or limitations.": "Proporcione más detalles si tiene alguna condición o limitación de salud.", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "Pasos obligatorios", "Residence Preference after Marriage": "Preferencia de residencia después del matrimonio", "Residence Status": "Estado de residencia", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "Respetuoso y convencional (sin intimidad): interacciones educadas con límites personales claros.", @@ -603,7 +603,7 @@ "Student": "estudiante", "Student Visa": "Visa de estudiante", "Student and Job Seeking": "Estudiante y búsqueda de empleo", - "Submit": "Submit", + "Submit": "Enviar", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "Enviar resultado final", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "Suscripción", "Subscription Status": "Estado de suscripción", - "Support": "Support", + "Support": "Asesores", "Supporter of the current government, but a difference in view is not a red line.": "Partidario del actual gobierno, pero una diferencia de opinión no es una línea roja.", "Supporter of the current government; serious opposition from my spouse is a red line.": "Partidario del actual gobierno; La oposición seria de mi cónyuge es una línea roja.", "Sweden": "Suecia", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "Sí, viven conmigo permanentemente.", "Yes, they live with me temporarily or periodically.": "Sí, viven conmigo de forma temporal o periódica.", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "Ahora puede enviar su solicitud para que podamos comenzar a buscar la pareja adecuada para usted", "You can now view their family's contact details and arrange further steps.": "Ahora puede ver los datos de contacto de su familia y coordinar los siguientes pasos.", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "Términos y condiciones", "user profiles": "user profiles", "user@example.com": "usuario@ejemplo.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "{completed} de {total} pasos obligatorios completados", "{days} days remaining of your subscription.": "{days} días restantes de tu suscripción.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "Gracias por sus comentarios. Para completar el proceso, envíe el resultado final de esta presentación/contacto para que se pueda determinar el estado definitivo. Si el resultado aún no se ha definido, puede permanecer en este estado hasta que se concrete.", @@ -2099,5 +2099,22 @@ "Contact Advisors": "Contactar consejeros", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si tiene algún problema, no dude en ponerse en contacto con nuestros consejeros en WhatsApp", "Steps": "Pasos", - "Previous": "Anterior" + "Previous": "Anterior", + "min": "min", + "1 minute": "1 minuto", + "10 minutes": "10 minutos", + "15 minutes": "15 minutos", + "16 minutes": "16 minutos", + "20 minutes": "20 minutos", + "30 minutes": "30 minutos", + "1 min": "1 min", + "2 min": "2 min", + "3 min": "3 min", + "4 min": "4 min", + "5 min": "5 min", + "6 min": "6 min", + "8 min": "8 min", + "10 min": "10 min", + "15 min": "15 min", + "16 min": "16 min" } diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 172bd70..eb1f215 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -2111,5 +2111,22 @@ "commercial_activity": "فعالیت تجاری، تبلیغاتی یا مقاصد غیرمرتبط", "admin_discretion": "صلاحدید و تصمیم کارشناسی مدیریت سامانه", "Steps": "مراحل", - "Previous": "قبلی" + "Previous": "قبلی", + "min": "دقیقه", + "1 minute": "۱ دقیقه", + "10 minutes": "۱۰ دقیقه", + "15 minutes": "۱۵ دقیقه", + "16 minutes": "۱۶ دقیقه", + "20 minutes": "۲۰ دقیقه", + "30 minutes": "۳۰ دقیقه", + "1 min": "۱ دقیقه", + "2 min": "۲ دقیقه", + "3 min": "۳ دقیقه", + "4 min": "۴ دقیقه", + "5 min": "۵ دقیقه", + "6 min": "۶ دقیقه", + "8 min": "۸ دقیقه", + "10 min": "۱۰ دقیقه", + "15 min": "۱۵ دقیقه", + "16 min": "۱۶ دقیقه" } diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index 3af31e3..b6034f4 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "Ville, région ou quartier", "Close and active": "Proche et actif", - "Close questions list": "Close questions list", + "Close questions list": "Fermer la liste des questions", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "Recueille des détails sur votre apparence physique, votre état de santé et votre bien-être mental.", "Collects information about your educational background, employment status, and financial situation.": "Recueille des informations sur votre formation, votre situation professionnelle et votre situation financière.", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "Trouver des correspondances", "Finish": "Terminer", "Fit/Average": "Ajustement/Moyenne", "Flexible": "Flexible", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "Si vous rencontrez des problèmes, n'hésitez pas à contacter nos spécialistes du support sur WhatsApp", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "Si vous avez des conditions particulières concernant le travail, les revenus, la location, l'achat d'un logement, la migration ou le futur lieu de résidence, veuillez les expliquer brièvement.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "Si vous continuez, nous en informerons l'autre partie et, après son approbation, vous pourrez consulter les coordonnées de chacun.", - "Important Note": "Important Note", + "Important Note": "Remarque importante", "In career growth path": "En cheminement de carrière", "In treatment or recovery": "En traitement ou en convalescence", "Income is variable": "Le revenu est variable", @@ -465,7 +465,7 @@ "Planner": "Planificateur", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "Veuillez expliquer brièvement les modalités de garde, le calendrier de présence de l'enfant, les restrictions éventuelles de déménagement ou d'émigration, ainsi que les obligations financières associées. Évitez de mentionner le nom de l'enfant, celui de l'autre parent ou des détails personnels inutiles.", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "Veuillez expliquer brièvement le type de responsabilité, sa durée, l'étendue du soutien financier ou des soins, et son impact potentiel sur votre lieu de résidence, votre déménagement ou les conditions de votre future vie commune.", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "Veuillez compléter les informations requises afin que nous puissions trouver les profils qui vous correspondent", "Please mention during the call that you were introduced by the Habib Marriage app.": "Veuillez mentionner lors de l'appel que vous avez été présenté par l'application Habib Marriage.", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "Veuillez noter que décliner ce cas pourrait entraîner un retard dans la recommandation de la prochaine correspondance, mais vous n'avez aucune obligation d'accepter et vous êtes totalement libre de choisir.", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "Certificat Professionnel", "Profile Picture": "Photo de profil", "Profile is locked": "Le profil est verrouillé", - "Profile registration": "Profile registration", + "Profile registration": "Enregistrement du profil", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "Prospère", "Provide more details if you have any health conditions or limitations.": "Fournissez plus de détails si vous avez des problèmes de santé ou des limitations.", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "Étapes obligatoires", "Residence Preference after Marriage": "Préférence de résidence après le mariage", "Residence Status": "Statut de résidence", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "Respectueux et conventionnel (Pas d'intimité) - Interactions polies avec des limites personnelles claires.", @@ -603,7 +603,7 @@ "Student": "Étudiant", "Student Visa": "Visa étudiant", "Student and Job Seeking": "Étudiant et recherche d'emploi", - "Submit": "Submit", + "Submit": "Envoyer", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "Soumettre le résultat final", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "Abonnement", "Subscription Status": "Statut de l'abonnement", - "Support": "Support", + "Support": "Conseillers", "Supporter of the current government, but a difference in view is not a red line.": "Partisan du gouvernement actuel, mais une divergence de vues ne constitue pas une ligne rouge.", "Supporter of the current government; serious opposition from my spouse is a red line.": "Partisan du gouvernement actuel; une opposition sérieuse de la part de mon conjoint est une ligne rouge.", "Sweden": "Suède", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "Oui, ils vivent avec moi en permanence.", "Yes, they live with me temporarily or periodically.": "Oui, ils vivent avec moi temporairement ou périodiquement.", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "Vous pouvez maintenant envoyer votre demande afin que nous commencions à chercher le profil qui vous convient", "You can now view their family's contact details and arrange further steps.": "Vous pouvez désormais consulter les coordonnées de sa famille et organiser les prochaines étapes.", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "Conditions générales", "user profiles": "user profiles", "user@example.com": "utilisateur@exemple.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "{completed} sur {total} étapes obligatoires complétées", "{days} days remaining of your subscription.": "{days} jours restants de votre abonnement.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "Merci pour vos commentaires. Pour finaliser la démarche, veuillez indiquer le résultat final de cette prise de contact afin d'en déterminer le statut définitif. Si la situation n'est pas encore arrêtée, vous pouvez rester dans cet état jusqu'à sa conclusion.", @@ -2099,5 +2099,22 @@ "Contact Advisors": "Contacter les conseillers", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Si vous rencontrez des problèmes, n'hésitez pas à contacter nos conseillers sur WhatsApp", "Steps": "Étapes", - "Previous": "Précédent" + "Previous": "Précédent", + "min": "min", + "1 minute": "1 minute", + "10 minutes": "10 minutes", + "15 minutes": "15 minutes", + "16 minutes": "16 minutes", + "20 minutes": "20 minutes", + "30 minutes": "30 minutes", + "1 min": "1 min", + "2 min": "2 min", + "3 min": "3 min", + "4 min": "4 min", + "5 min": "5 min", + "6 min": "6 min", + "8 min": "8 min", + "10 min": "10 min", + "15 min": "15 min", + "16 min": "16 min" } diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index 9e3fd9a..b4e6c8f 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "શહેર, પ્રદેશ અથવા પડોશ", "Close and active": "બંધ અને સક્રિય", - "Close questions list": "Close questions list", + "Close questions list": "પ્રશ્ન યાદી બંધ કરો", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "તમારા શારીરિક દેખાવ, સ્વાસ્થ્યની સ્થિતિ અને માનસિક સુખાકારી વિશે વિગતો એકત્રિત કરે છે.", "Collects information about your educational background, employment status, and financial situation.": "તમારી શૈક્ષણિક પૃષ્ઠભૂમિ, રોજગાર સ્થિતિ અને નાણાકીય પરિસ્થિતિ વિશેની માહિતી એકત્રિત કરે છે.", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "મેચ શોધો", "Finish": "સમાપ્ત", "Fit/Average": "ફિટ/સરેરાશ", "Flexible": "લવચીક", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "જો તમને કોઈ સમસ્યા આવે, તો કૃપા કરીને WhatsApp પર અમારા સપોર્ટ નિષ્ણાતોનો સંપર્ક કરો", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "જો તમારી પાસે કામ, આવક, ભાડા, ઘર ખરીદવા, સ્થળાંતર અથવા ભાવિ રહેઠાણના સ્થળને લગતી કોઈ ખાસ શરતો હોય, તો કૃપા કરીને ટૂંકમાં સમજાવો.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "જો તમે આગળ વધો છો, તો અમે અન્ય પક્ષને સૂચિત કરીશું, અને તેમની મંજૂરી પર, તમે એકબીજાની સંપર્ક માહિતી જોઈ શકો છો.", - "Important Note": "Important Note", + "Important Note": "મહત્વપૂર્ણ નોંધ", "In career growth path": "કારકિર્દી વૃદ્ધિ પાથ માં", "In treatment or recovery": "સારવાર અથવા પુનઃપ્રાપ્તિમાં", "Income is variable": "આવક ચલ છે", @@ -465,7 +465,7 @@ "Planner": "પ્લાનર", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "કૃપા કરીને બાળકની કસ્ટડીની સ્થિતિ, બાળકની હાજરીનું સમયપત્રક, સ્થાનાંતરણ અથવા વિદેશ પ્રવાસ પરના સંભવિત નિયંત્રણો અને સંબંધિત નાણાકીય જવાબદારીઓ ટૂંકમાં સમજાવો. બાળકના નામ, અન્ય માતા કે પિતાના નામ અથવા બિનજરૂری અંગત વિગતોનો ઉલ્લેખ કરવાનું ટાળો.", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "કૃપા કરીને જવાબદારીનો પ્રકાર, તેનો સમયગાળો, નાણાકીય કે સંભાળ સહાયની મર્યાદા અને તમારા રહેઠાણના સ્થળ, સ્થાનાંતરણ અથવા ભવિષ્યના લગ્ન જીવનની પરિસ્થિતિઓ પર તેની સંભવيت અસર ટૂંકમાં સમજાવો.", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "કૃપા કરીને જરૂરી માહિતી પૂર્ણ કરો જેથી અમે તમારા માટે યોગ્ય જોડી શોધી શકીએ", "Please mention during the call that you were introduced by the Habib Marriage app.": "કૃપા કરીને કૉલ દરમિયાન ઉલ્લેખ કરો કે તમને હબીબ મેરેજ એપ્લિકેશન દ્વારા પરિચય કરાવવામાં આવ્યો હતો.", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "મહેરબાની કરીને નોંધ કરો કે આ કેસને નકારવાથી આગામી મેચની ભલામણ કરવામાં વિલંબ થઈ શકે છે, પરંતુ સ્વીકારવાની કોઈ જવાબદારી નથી અને તમે પસંદ કરવા માટે સંપૂર્ણપણે સ્વતંત્ર છો.", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "વ્યવસાયિક પ્રમાણપત્ર", "Profile Picture": "પ્રોફાઇલ ચિત્ર", "Profile is locked": "પ્રોફાઇલ લૉક કરેલ છે", - "Profile registration": "Profile registration", + "Profile registration": "પ્રોફાઇલ નોંધણી", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "સમૃદ્ધ", "Provide more details if you have any health conditions or limitations.": "જો તમારી પાસે કોઈ સ્વાસ્થ્ય સ્થિતિ અથવા મર્યાદાઓ હોય તો વધુ વિગતો આપો.", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "જરૂરી પગલાં", "Residence Preference after Marriage": "લગ્ન પછી રહેઠાણની પસંદગી", "Residence Status": "રહેઠાણની સ્થિતિ", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "આદરપૂર્ણ અને પરંપરાગત (કોઈ આત્મીયતા નથી) - સ્પષ્ટ વ્યક્તિગત સીમાઓ સાથે નમ્ર ક્રિયાપ્રતિક્રિયાઓ.", @@ -603,7 +603,7 @@ "Student": "વિદ્યાર્થી", "Student Visa": "વિદ્યાર્થી વિઝા", "Student and Job Seeking": "વિદ્યાર્થી અને જોબ સીકિંગ", - "Submit": "Submit", + "Submit": "સબમિટ કરો", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "અંતિમ પરિણામ સબમિટ કરો", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "સબ્સ્ક્રિપ્શન", "Subscription Status": "સબ્સ્ક્રિપ્શન સ્થિતિ", - "Support": "Support", + "Support": "સલાહકારો", "Supporter of the current government, but a difference in view is not a red line.": "વર્તમાન સરકારના સમર્થક, પરંતુ દૃષ્ટિએ તફાવત એ લાલ લાઇન નથી.", "Supporter of the current government; serious opposition from my spouse is a red line.": "વર્તમાન સરકારના સમર્થક; મારા જીવનસાથી તરફથી ગંભીર વિરોધ એ લાલ રેખા છે.", "Sweden": "સ્વીડન", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "હા, તેઓ કાયમ મારી સાથે રહે છે.", "Yes, they live with me temporarily or periodically.": "હા, તેઓ મારી સાથે અસ્થાયી અથવા સમયાંતરે રહે છે.", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "હવે તમે તમારી વિનંતી સબમિટ કરી શકો છો જેથી અમે તમારા માટે યોગ્ય જોડી શોધવાનું શરૂ કરી શકીએ", "You can now view their family's contact details and arrange further steps.": "હવે તમે તેમના પરિવારની સંપર્ક વિગતો જોઈ શકો છો અને આગળનાં પગલાં ગોઠવી શકો છો.", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "નિયમો અને શરતો", "user profiles": "user profiles", "user@example.com": "user@example.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "{total} માંથી {completed} જરૂરી પગલાં પૂર્ણ થયા", "{days} days remaining of your subscription.": "તમારા સબ્સ્ક્રિપ્શનના {days} દિવસ બાકી છે.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "તમારા પ્રતિસાદ બદલ આભાર. પ્રક્રિયા પૂર્ણ કરવા માટે, કૃપા કરીને આ ઓળખાણ/સંપર્કનું અંતિમ પરિણામ સબમિટ કરો જેથી અંતિમ સ્થિતિ નક્કી થઈ શકે. જો હજી અંતિમ સ્થિતિ નક્કી ન થઈ હોય, તો તે અંતિમ ન થાય ત્યાં સુધી તમે આ સ્થિતિમાં રહી શકો છો.", @@ -2101,5 +2101,22 @@ "Contact Advisors": "સલાહકારોનો સંપર્ક કરો", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "જો તમને કોઈ સમસ્યા આવે, તો કૃપા કરીને WhatsApp પર અમારા સલાહકારોનો સંપર્ક કરો", "Steps": "પગલાં", - "Previous": "પાછલું" + "Previous": "પાછલું", + "min": "મિનિટ", + "1 minute": "1 મિનિટ", + "10 minutes": "10 મિનિટ", + "15 minutes": "15 મિનિટ", + "16 minutes": "16 મિનિટ", + "20 minutes": "20 મિનિટ", + "30 minutes": "30 મિનિટ", + "1 min": "1 મિનિટ", + "2 min": "2 મિનિટ", + "3 min": "3 મિનિટ", + "4 min": "4 મિનિટ", + "5 min": "5 મિનિટ", + "6 min": "6 મિનિટ", + "8 min": "8 મિનિટ", + "10 min": "10 મિનિટ", + "15 min": "15 મિનિટ", + "16 min": "16 મિનિટ" } diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 54da21c..9de1401 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "Birni, yanki, ko unguwa", "Close and active": "Kusa da aiki", - "Close questions list": "Close questions list", + "Close questions list": "Rufe jerin tambayoyi", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "Yana tattara cikakkun bayanai game da kamannin jikin ku, yanayin lafiyar ku, da jin daɗin tunanin ku.", "Collects information about your educational background, employment status, and financial situation.": "Yana tattara bayanai game da asalin ilimin ku, matsayin aiki, da yanayin kuɗi.", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "Nemo masu dacewa", "Finish": "Kammala", "Fit/Average": "Fit/Matsakaici", "Flexible": "M", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "Idan kun ci karo da matsala, ku tuntuɓi ƙwararrun tallafi a WhatsApp", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "Idan kuna da kowane yanayi na musamman game da aiki, samun kuɗi, haya, siyan gida, ƙaura, ko wurin zama na gaba, da fatan za a yi bayani a taƙaice.", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "Idan kun ci gaba, za mu sanar da ɗayan, kuma bayan amincewarsu, kuna iya duba bayanan tuntuɓar juna.", - "Important Note": "Important Note", + "Important Note": "Muhimmin bayani", "In career growth path": "A cikin hanyar haɓaka aiki", "In treatment or recovery": "A cikin magani ko farfadowa", "Income is variable": "Kudin shiga yana canzawa", @@ -465,7 +465,7 @@ "Planner": "Mai tsarawa", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "Da fatan za a taƙaita bayanin yanayin riƙon yaro, tsarin lokacin kasancewar yaro, yiwuwar hana ƙaura ko tafiya wata ƙasa, da kuma wajibai na kuɗi masu alaƙa. Guji shigar da sunan yaron, sunan ɗayan iyayen, ko bayanan sirri da ba su da mahimmanci.", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "Da fatan za a taƙaita bayanin nau'in alhakin, tsawonsa, gwargwadon tallafin kuɗi ko kulawa, da yuwuwar tasirinsa ga wurin zama, ƙaura, ko yanayin rayuwar aure na gaba.", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "Da fatan za a cika bayanan da ake buƙata domin mu nemo muku mutumin da ya dace", "Please mention during the call that you were introduced by the Habib Marriage app.": "Da fatan za a ambata lokacin kiran cewa an gabatar da ku ta hanyar aikace-aikacen Habib Marriage.", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "Lura cewa raguwar wannan shari'ar na iya haifar da jinkiri wajen ba da shawarar wasa na gaba, amma kwata-kwata babu wajibcin karɓa kuma kuna da cikakken 'yancin zaɓar.", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "Takaddar Kwarewa", "Profile Picture": "Hoton Bayani", "Profile is locked": "An kulle bayanin martaba", - "Profile registration": "Profile registration", + "Profile registration": "Rijistar bayanan martaba", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "Mai wadata", "Provide more details if you have any health conditions or limitations.": "Bada ƙarin cikakkun bayanai idan kuna da kowane yanayi ko gazawa.", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "Matakan da ake buƙata", "Residence Preference after Marriage": "Zabar Mazauna Bayan Aure", "Residence Status": "Matsayin Mazauni", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "Girmamawa da na al'ada (Babu kusanci) - hulɗa mai kyau tare da fayyace iyakoki na sirri.", @@ -603,7 +603,7 @@ "Student": "dalibi", "Student Visa": "Visa dalibi", "Student and Job Seeking": "Dalibi da Neman Aiki", - "Submit": "Submit", + "Submit": "Ƙaddamar", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "Gabatar da Sakamakon Karshe", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "Biyan kuɗi", "Subscription Status": "Matsayin Biyan Kuɗi", - "Support": "Support", + "Support": "Masu ba da shawara", "Supporter of the current government, but a difference in view is not a red line.": "Mai goyan bayan gwamnati mai ci, amma bambancin ra'ayi ba jan layi ba ne.", "Supporter of the current government; serious opposition from my spouse is a red line.": "Mai goyon bayan gwamnati mai ci; tsananin adawa daga mijina jajayen layi ne.", "Sweden": "Suwidin", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "Ee, suna rayuwa tare da ni har abada.", "Yes, they live with me temporarily or periodically.": "Ee, suna rayuwa tare da ni na ɗan lokaci ko lokaci-lokaci.", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "Yanzu za ku iya ƙaddamar da buƙatarku domin mu fara neman wanda ya dace da ku", "You can now view their family's contact details and arrange further steps.": "Yanzu zaka iya ganin lambobin tuntuɓar iyalinsu kuma ka shirya matakai na gaba.", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "Sharudda da ƙa'idoji", "user profiles": "user profiles", "user@example.com": "user@example.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "An kammala matakai {completed} cikin {total} da ake buƙata", "{days} days remaining of your subscription.": "{days} days remaining of your subscription.", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "Mungode da ra'ayoyinku. Don kammala aikin, da fatan za a gabatar da sakamakon ƙarshe na wannan gabatarwa/tuntuɓar don a iya tabbatar da matsayin ƙarshe. Idan ba a riga an yanke hukunci na ƙarshe ba, za ku iya zama a wannan matsayin har sai an kammala.", @@ -2099,5 +2099,22 @@ "Contact Advisors": "Tuntuɓi masu ba da shawara", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Idan kun ci karo da matsala, ku tuntuɓi masu ba da shawara a WhatsApp", "Steps": "Matakai", - "Previous": "Baya" + "Previous": "Baya", + "min": "minti", + "1 minute": "Minti 1", + "10 minutes": "Minti 10", + "15 minutes": "Minti 15", + "16 minutes": "Minti 16", + "20 minutes": "Minti 20", + "30 minutes": "Minti 30", + "1 min": "Minti 1", + "2 min": "Minti 2", + "3 min": "Minti 3", + "4 min": "Minti 4", + "5 min": "Minti 5", + "6 min": "Minti 6", + "8 min": "Minti 8", + "10 min": "Minti 10", + "15 min": "Minti 15", + "16 min": "Minti 16" } diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index 1eeea6c..f676a2a 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "פנייה ליועצים", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "אם נתקלת בבעיה כלשהי, ניתן לפנות ליועצים שלנו בוואטסאפ.", "Steps": "שלבים", - "Previous": "הקודם" + "Previous": "הקודם", + "min": "דק׳", + "1 minute": "דקה אחת", + "10 minutes": "10 דקות", + "15 minutes": "15 דקות", + "16 minutes": "16 דקות", + "20 minutes": "20 דקות", + "30 minutes": "30 דקות", + "1 min": "דקה 1", + "2 min": "2 דק׳", + "3 min": "3 דק׳", + "4 min": "4 דק׳", + "5 min": "5 דק׳", + "6 min": "6 דק׳", + "8 min": "8 דק׳", + "10 min": "10 דק׳", + "15 min": "15 דק׳", + "16 min": "16 דק׳" } diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index 0e458b2..d5d6b72 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "शहर, क्षेत्र, या पड़ोस", "Close and active": "बंद और सक्रिय", - "Close questions list": "Close questions list", + "Close questions list": "प्रश्न सूची बंद करें", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "आपकी शारीरिक बनावट, स्वास्थ्य स्थिति और मानसिक कल्याण के बारे में विवरण एकत्र करता है।", "Collects information about your educational background, employment status, and financial situation.": "आपकी शैक्षिक पृष्ठभूमि, रोजगार की स्थिति और वित्तीय स्थिति के बारे में जानकारी एकत्र करता है।", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "साथी खोजें", "Finish": "समाप्त", "Fit/Average": "फ़िट/औसत", "Flexible": "लचीला", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "किसी भी समस्या के लिए, कृपया व्हाट्सएप पर हमारे सहायता विशेषज्ञों से संपर्क करें", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "यदि आपके पास काम, आय, किराये, घर खरीदने, प्रवासन या भविष्य के निवास स्थान के संबंध में कोई विशेष शर्तें हैं, तो कृपया संक्षेप में बताएं।", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "यदि आप आगे बढ़ते हैं, तो हम दूसरे पक्ष को सूचित करेंगे, और उनकी मंजूरी पर, आप एक-दूसरे की संपर्क जानकारी देख सकते हैं।", - "Important Note": "Important Note", + "Important Note": "महत्वपूर्ण नोट", "In career growth path": "कैरियर विकास पथ में", "In treatment or recovery": "उपचार या पुनर्प्राप्ति में", "Income is variable": "आय परिवर्तनशील है", @@ -465,7 +465,7 @@ "Planner": "योजनाकार", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "कृपया बच्चे की कस्टडी की स्थिति, बच्चे की उपस्थिति की समय सारिणी, स्थानांतरण या प्रवास पर संभावित प्रतिबंधों और संबंधित वित्तीय दायित्वों को संक्षेप में स्पष्ट करें। बच्चे का नाम, दूसरे अभिभावक का नाम या अनावश्यक व्यक्तिगत विवरण शामिल करने से बचें।", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "कृपया जिम्मेदारी के प्रकार, उसकी अवधि, वित्तीय या देखभाल सहायता की सीमा, और आपके निवास स्थान, स्थानांतरण या भविष्य के वैवाहिक जीवन की स्थितियों पर इसके संभावित प्रभाव को संक्षेप में स्पष्ट करें।", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "कृपया आवश्यक जानकारी पूरी करें ताकि हम आपके लिए उपयुक्त साथी ढूंढ सकें", "Please mention during the call that you were introduced by the Habib Marriage app.": "कृपया कॉल के दौरान उल्लेख करें कि आपका परिचय हबीब मैरिज ऐप के माध्यम से कराया गया था।", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "कृपया ध्यान दें कि इस मामले को अस्वीकार करने से अगले मिलान की सिफारिश में देरी हो सकती है, लेकिन स्वीकार करने का कोई दायित्व नहीं है और आप चुनने के लिए पूरी तरह स्वतंत्र हैं।", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "व्यावसायिक प्रमाणपत्र", "Profile Picture": "प्रोफ़ाइल चित्र", "Profile is locked": "प्रोफ़ाइल लॉक है", - "Profile registration": "Profile registration", + "Profile registration": "प्रोफ़ाइल पंजीकरण", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "समृद्ध", "Provide more details if you have any health conditions or limitations.": "यदि आपकी कोई स्वास्थ्य स्थितियाँ या सीमाएँ हैं तो अधिक विवरण प्रदान करें।", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "आवश्यक चरण", "Residence Preference after Marriage": "विवाह के बाद निवास को प्राथमिकता", "Residence Status": "निवास स्थिति", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "सम्मानजनक और पारंपरिक (कोई अंतरंगता नहीं) - स्पष्ट व्यक्तिगत सीमाओं के साथ विनम्र बातचीत।", @@ -603,7 +603,7 @@ "Student": "छात्र", "Student Visa": "छात्र वीज़ा", "Student and Job Seeking": "छात्र और नौकरी की तलाश", - "Submit": "Submit", + "Submit": "सबमिट करें", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "अंतिम परिणाम जमा करें", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "सदस्यता", "Subscription Status": "सदस्यता स्थिति", - "Support": "Support", + "Support": "सलाहकार", "Supporter of the current government, but a difference in view is not a red line.": "वर्तमान सरकार के समर्थक, लेकिन दृष्टिकोण में अंतर कोई लाल रेखा नहीं है।", "Supporter of the current government; serious opposition from my spouse is a red line.": "वर्तमान सरकार के समर्थक; मेरे जीवनसाथी का गंभीर विरोध एक लाल रेखा है।", "Sweden": "स्वीडन", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "हाँ, वे स्थायी रूप से मेरे साथ रहते हैं।", "Yes, they live with me temporarily or periodically.": "हाँ, वे अस्थायी रूप से या समय-समय पर मेरे साथ रहते हैं।", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "अब आप अपना अनुरोध सबमिट कर सकते हैं ताकि हम आपके लिए सही साथी की तलाश शुरू कर सकें", "You can now view their family's contact details and arrange further steps.": "अब आप उनके परिवार के संपर्क विवरण देख सकते हैं और आगे के कदमों की व्यवस्था कर सकते हैं।", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "नियम और शर्तें", "user profiles": "user profiles", "user@example.com": "user@example.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "{total} में से {completed} आवश्यक चरण पूर्ण हुए", "{days} days remaining of your subscription.": "आपकी सदस्यता के {days} दिन शेष हैं।", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "आपकी प्रतिक्रिया के लिए धन्यवाद। प्रक्रिया पूरी करने के लिए, कृपया इस परिचय/संपर्क का अंतिम परिणाम सबमिट करें ताकि अंतिम स्थिति निर्धारित की जा सके। यदि अंतिम स्थिति अभी तय नहीं हुई है, तो आप इसके अंतिम होने तक इसी स्थिति में रह सकते हैं।", @@ -2099,5 +2099,22 @@ "Contact Advisors": "सलाहकारों से संपर्क करें", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "किसी भी समस्या के लिए, कृपया व्हाट्सएप पर हमारे सलाहकारों से संपर्क करें", "Steps": "चरण", - "Previous": "पिछला" + "Previous": "पिछला", + "min": "मिनट", + "1 minute": "1 मिनट", + "10 minutes": "10 मिनट", + "15 minutes": "15 मिनट", + "16 minutes": "16 मिनट", + "20 minutes": "20 मिनट", + "30 minutes": "30 मिनट", + "1 min": "1 मिनट", + "2 min": "2 मिनट", + "3 min": "3 मिनट", + "4 min": "4 मिनट", + "5 min": "5 मिनट", + "6 min": "6 मिनट", + "8 min": "8 मिनट", + "10 min": "10 मिनट", + "15 min": "15 मिनट", + "16 min": "16 मिनट" } diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index ebfb45c..6137ad1 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Hubungi Penasihat", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Jika Anda mengalami kendala, jangan ragu untuk menghubungi penasihat kami di WhatsApp.", "Steps": "Langkah", - "Previous": "Sebelumnya" + "Previous": "Sebelumnya", + "min": "mnt", + "1 minute": "1 menit", + "10 minutes": "10 menit", + "15 minutes": "15 menit", + "16 minutes": "16 menit", + "20 minutes": "20 menit", + "30 minutes": "30 menit", + "1 min": "1 mnt", + "2 min": "2 mnt", + "3 min": "3 mnt", + "4 min": "4 mnt", + "5 min": "5 mnt", + "6 min": "6 mnt", + "8 min": "8 mnt", + "10 min": "10 mnt", + "15 min": "15 mnt", + "16 min": "16 mnt" } diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index 4e0266f..6433b44 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "مشیرن سٟتؠ رابطہ کٔریو", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "کانہہ دشواری پیش یِنہٕ وزِ ہیکیو واٹس ایپ پؠٹھ مشیرن سٟتؠ رابطہ کٔرِتھ۔", "Steps": "مراحل", - "Previous": "پچھلا" + "Previous": "پچھلا", + "min": "منٹ", + "1 minute": "۱ منٹ", + "10 minutes": "۱۰ منٹ", + "15 minutes": "۱۵ منٹ", + "16 minutes": "۱۶ منٹ", + "20 minutes": "۲۰ منٹ", + "30 minutes": "۳۰ منٹ", + "1 min": "۱ منٹ", + "2 min": "۲ منٹ", + "3 min": "۳ منٹ", + "4 min": "۴ منٹ", + "5 min": "۵ منٹ", + "6 min": "۶ منٹ", + "8 min": "۸ منٹ", + "10 min": "۱۰ منٹ", + "15 min": "۱۵ منٹ", + "16 min": "۱۶ منٹ" } diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index 4cfe83d..8477e90 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Contatar consultores", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Se encontrar algum problema, entre em contato com nossos consultores no WhatsApp.", "Steps": "Etapas", - "Previous": "Anterior" + "Previous": "Anterior", + "min": "min", + "1 minute": "1 minuto", + "10 minutes": "10 minutos", + "15 minutes": "15 minutos", + "16 minutes": "16 minutos", + "20 minutes": "20 minutos", + "30 minutes": "30 minutos", + "1 min": "1 min", + "2 min": "2 min", + "3 min": "3 min", + "4 min": "4 min", + "5 min": "5 min", + "6 min": "6 min", + "8 min": "8 min", + "10 min": "10 min", + "15 min": "15 min", + "16 min": "16 min" } diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 0992525..1a88bbc 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -2095,5 +2095,22 @@ "Contact Advisors": "Связаться с консультантами", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Если у вас возникнут какие-либо проблемы, пожалуйста, свяжитесь с нашими консультантами в WhatsApp.", "Steps": "Шаги", - "Previous": "Предыдущий" + "Previous": "Предыдущий", + "min": "мин", + "1 minute": "1 минута", + "10 minutes": "10 минут", + "15 minutes": "15 минут", + "16 minutes": "16 минут", + "20 minutes": "20 минут", + "30 minutes": "30 минут", + "1 min": "1 мин", + "2 min": "2 мин", + "3 min": "3 мин", + "4 min": "4 мин", + "5 min": "5 мин", + "6 min": "6 мин", + "8 min": "8 мин", + "10 min": "10 мин", + "15 min": "15 мин", + "16 min": "16 мин" } diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 61e5ccb..dc9349b 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Wasiliana na Washauri", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Ukikumbana na changamoto yoyote, jisikie huru kuwasiliana na washauri wetu kupitia WhatsApp.", "Steps": "Hatua", - "Previous": "Iliyopita" + "Previous": "Iliyopita", + "min": "dak", + "1 minute": "dakika 1", + "10 minutes": "dakika 10", + "15 minutes": "dakika 15", + "16 minutes": "dakika 16", + "20 minutes": "dakika 20", + "30 minutes": "dakika 30", + "1 min": "dak 1", + "2 min": "dak 2", + "3 min": "dak 3", + "4 min": "dak 4", + "5 min": "dak 5", + "6 min": "dak 6", + "8 min": "dak 8", + "10 min": "dak 10", + "15 min": "dak 15", + "16 min": "dak 16" } diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index 99c83c1..2b3e14c 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Тамос бо мушовирон", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Агар мушкиле пеш ояд, лутфан ба мушовирони мо дар WhatsApp муроҷиат кунед.", "Steps": "Марҳилаҳо", - "Previous": "Қаблӣ" + "Previous": "Қаблӣ", + "min": "дақ", + "1 minute": "1 дақиқа", + "10 minutes": "10 дақиқа", + "15 minutes": "15 дақиқа", + "16 minutes": "16 дақиқа", + "20 minutes": "20 дақиқа", + "30 minutes": "30 дақиқа", + "1 min": "1 дақ", + "2 min": "2 дақ", + "3 min": "3 дақ", + "4 min": "4 дақ", + "5 min": "5 дақ", + "6 min": "6 дақ", + "8 min": "8 дақ", + "10 min": "10 дақ", + "15 min": "15 дақ", + "16 min": "16 дақ" } diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index c8fc44d..0b32261 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Danışmanlarla İletişime Geçin", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Herhangi bir sorunla karşılaşırsanız WhatsApp üzerinden danışmanlarımızla iletişime geçebilirsiniz.", "Steps": "Adımlar", - "Previous": "Önceki" + "Previous": "Önceki", + "min": "dk", + "1 minute": "1 dakika", + "10 minutes": "10 dakika", + "15 minutes": "15 dakika", + "16 minutes": "16 dakika", + "20 minutes": "20 dakika", + "30 minutes": "30 dakika", + "1 min": "1 dk", + "2 min": "2 dk", + "3 min": "3 dk", + "4 min": "4 dk", + "5 min": "5 dk", + "6 min": "6 dk", + "8 min": "8 dk", + "10 min": "10 dk", + "15 min": "15 dk", + "16 min": "16 dk" } diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index b32340a..9e85b64 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Musheeran se Rabta Karein", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar koi masla darpesh ho to WhatsApp par hamare musheeran se bila-jhijhak rabta karein.", "Steps": "Marahil", - "Previous": "Pichla" + "Previous": "Pichla", + "min": "min", + "1 minute": "1 minute", + "10 minutes": "10 minute", + "15 minutes": "15 minute", + "16 minutes": "16 minute", + "20 minutes": "20 minute", + "30 minutes": "30 minute", + "1 min": "1 min", + "2 min": "2 min", + "3 min": "3 min", + "4 min": "4 min", + "5 min": "5 min", + "6 min": "6 min", + "8 min": "8 min", + "10 min": "10 min", + "15 min": "15 min", + "16 min": "16 min" } diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 410ac70..ba15cfd 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "مشیروں سے رابطہ کریں", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "اگر کوئی مسئلہ درپیش ہو تو واٹس ایپ پر ہمارے مشیروں سے بلا جھجھک رابطہ کریں۔", "Steps": "مراحل", - "Previous": "پچھلا" + "Previous": "پچھلا", + "min": "منٹ", + "1 minute": "1 منٹ", + "10 minutes": "10 منٹ", + "15 minutes": "15 منٹ", + "16 minutes": "16 منٹ", + "20 minutes": "20 منٹ", + "30 minutes": "30 منٹ", + "1 min": "1 منٹ", + "2 min": "2 منٹ", + "3 min": "3 منٹ", + "4 min": "4 منٹ", + "5 min": "5 منٹ", + "6 min": "6 منٹ", + "8 min": "8 منٹ", + "10 min": "10 منٹ", + "15 min": "15 منٹ", + "16 min": "16 منٹ" } diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index 5c60a91..6501186 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -2364,5 +2364,22 @@ "Contact Advisors": "Maslahatchilar bilan bog'lanish", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "Agar biron bir muammo yuzaga kelsa, WhatsApp orqali maslahatchilarimiz bilan bog'laning.", "Steps": "Bosqichlar", - "Previous": "Oldingi" + "Previous": "Oldingi", + "min": "daq", + "1 minute": "1 daqiqa", + "10 minutes": "10 daqiqa", + "15 minutes": "15 daqiqa", + "16 minutes": "16 daqiqa", + "20 minutes": "20 daqiqa", + "30 minutes": "30 daqiqa", + "1 min": "1 daq", + "2 min": "2 daq", + "3 min": "3 daq", + "4 min": "4 daq", + "5 min": "5 daq", + "6 min": "6 daq", + "8 min": "8 daq", + "10 min": "10 daq", + "15 min": "15 daq", + "16 min": "16 daq" } diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index 9a2b0e1..2139ae2 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -104,7 +104,7 @@ "City and country of residence": "City and country of residence", "City, region, or neighborhood": "城市、地区或社区", "Close and active": "关闭且活跃", - "Close questions list": "Close questions list", + "Close questions list": "关闭问题列表", "Close slider": "Close slider", "Collects details about your physical appearance, health status, and mental well-being.": "收集有关您的外貌、健康状况和心理健康的详细信息。", "Collects information about your educational background, employment status, and financial situation.": "收集有关您的教育背景、就业状况和财务状况的信息。", @@ -224,7 +224,7 @@ "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", - "Find Matches": "Find Matches", + "Find Matches": "寻找匹配", "Finish": "完成", "Fit/Average": "适合/平均", "Flexible": "灵活", @@ -292,7 +292,7 @@ "If you encounter any issues, please feel free to contact our support specialists in WhatsApp": "如果遇到任何问题,请随时通过 WhatsApp 联系我们的支持专家", "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly.": "如果您在工作、收入、租房、购房、移民或未来居住地等方面有任何特殊情况,请简要说明。", "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.": "如果您继续,我们将通知对方,经其批准后,您可以查看对方的联系信息。", - "Important Note": "Important Note", + "Important Note": "重要提示", "In career growth path": "在职业成长道路上", "In treatment or recovery": "治疗或康复中", "Income is variable": "收入是可变的", @@ -465,7 +465,7 @@ "Planner": "规划师", "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.": "请简要说明抚养权状况、子女共同生活的时间安排、对居住地变更或移民的潜在限制,以及相关的财务义务。请避免提供子女姓名、另一方家长的姓名或其他不必要的个人隐私细节。", "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.": "请简要说明责任类型、持续时间、资金支持或照顾的程度,以及其对您的居住地、搬迁或未来已婚生活条件的潜在影响。", - "Please complete the required information so we can find suitable matches for you": "Please complete the required information so we can find suitable matches for you", + "Please complete the required information so we can find suitable matches for you": "请填写必要的信息,以便我们为您寻找合适的对象", "Please mention during the call that you were introduced by the Habib Marriage app.": "请在通话中说明您是通过 Habib Marriage 应用程序介绍的。", "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.": "请注意,婉拒此推荐可能会延迟下一次匹配推荐,但您完全没有必须接受的义务,您可以完全自由选择。", "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.", @@ -489,7 +489,7 @@ "Professional Certificate": "专业证书", "Profile Picture": "个人资料图片", "Profile is locked": "资料已锁定", - "Profile registration": "Profile registration", + "Profile registration": "个人资料注册", "Progressive Disclosure:": "Progressive Disclosure:", "Prosperous": "繁荣", "Provide more details if you have any health conditions or limitations.": "如果您有任何健康状况或限制,请提供更多详细信息。", @@ -542,7 +542,7 @@ "Request approved!": "Request approved!", "Request to Proceed": "Request to Proceed", "Required": "Required", - "Required Steps": "Required Steps", + "Required Steps": "必填步骤", "Residence Preference after Marriage": "婚后居住偏好", "Residence Status": "居留身份", "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.": "尊重和传统(没有亲密行为)-有礼貌的互动,有明确的个人界限。", @@ -603,7 +603,7 @@ "Student": "学生", "Student Visa": "学生签证", "Student and Job Seeking": "学生和求职", - "Submit": "Submit for Finding Match", + "Submit": "提交", "Submit Call Result": "Submit Call Result", "Submit Final Outcome": "提交最终结果", "Submit Man": "Submit Man", @@ -611,7 +611,7 @@ "Submit Woman": "Submit Woman", "Subscription": "订阅", "Subscription Status": "订阅状态", - "Support": "Support", + "Support": "顾问", "Supporter of the current government, but a difference in view is not a red line.": "现任政府的支持者,但观点分歧并非红线。", "Supporter of the current government; serious opposition from my spouse is a red line.": "现任政府的支持者;我配偶的严重反对是一条红线。", "Sweden": "瑞典", @@ -717,7 +717,7 @@ "Yes, they live with me permanently.": "是的,他们永远和我住在一起。", "Yes, they live with me temporarily or periodically.": "是的,他们暂时或定期与我住在一起。", "You are always in control of what happens next.": "You are always in control of what happens next.", - "You can now submit your request so we can start finding the right match for you": "You can now submit your request so we can start finding the right match for you", + "You can now submit your request so we can start finding the right match for you": "您现在可以提交申请,以便我们开始为您寻找合适的对象", "You can now view their family's contact details and arrange further steps.": "您现在可以查看对方家人的联系方式并安排后续步骤。", "You can pause the survey anytime and resume later. Your progress is saved automatically.": "You can pause the survey anytime and resume later. Your progress is saved automatically.", "You can't edit your profile while we're searching for matches": "You can't edit your profile while we're searching for matches", @@ -740,11 +740,11 @@ "marriages": "marriages", "matches": "matches", "play": "play", - "terms & conditions": "terms & conditions", + "terms & conditions": "条款与条件", "user profiles": "user profiles", "user@example.com": "用户@example.com", "video": "video", - "{completed} of {total} required steps completed": "{completed} of {total} required steps completed", + "{completed} of {total} required steps completed": "已完成 {total} 个必填步骤中的 {completed} 个", "{days} days remaining of your subscription.": "您的订阅还剩 {days} 天。", "⚠️ This section is completely confidential and is only used for matching and review by advisors.": "⚠️ This section is completely confidential and is only used for matching and review by advisors.", "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized.": "感谢您的反馈。为了完成该流程,请提交此次介绍/联系的最终结果,以便确定最终状态。如果最终结果尚未确定,您可以保持此状态直至明确。", @@ -2099,5 +2099,22 @@ "Contact Advisors": "联系顾问", "If you encounter any issues, please feel free to contact our advisors in WhatsApp": "如果遇到任何问题,请随时通过 WhatsApp 联系我们的顾问", "Steps": "步骤", - "Previous": "上一步" + "Previous": "上一步", + "min": "分钟", + "1 minute": "1分钟", + "10 minutes": "10分钟", + "15 minutes": "15分钟", + "16 minutes": "16分钟", + "20 minutes": "20分钟", + "30 minutes": "30分钟", + "1 min": "1分钟", + "2 min": "2分钟", + "3 min": "3分钟", + "4 min": "4分钟", + "5 min": "5分钟", + "6 min": "6分钟", + "8 min": "8分钟", + "10 min": "10分钟", + "15 min": "15分钟", + "16 min": "16分钟" } diff --git a/src/translations/path_to_english.json b/src/translations/path_to_english.json index bb741ca..fa05f63 100644 --- a/src/translations/path_to_english.json +++ b/src/translations/path_to_english.json @@ -244,5 +244,6 @@ "onboarding.back": "Back", "onboarding.accept": "Accept", "rejectionNotice.title": "Your request was rejected", - "rejectionNotice.message": "Your request was rejected by the lady. You will be introduced to other candidates in the future." + "rejectionNotice.message": "Your request was rejected by the lady. You will be introduced to other candidates in the future.", + "common.min": "min" } From 8ea69d39f23e9f618e57d9aee1e89f21cd3a3198 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 12 Sep 2026 15:13:45 +0330 Subject: [PATCH 18/27] feat: add question components, media upload hook, and localization support --- src/components/Componentes/question-file.tsx | 126 +++++++---- .../Componentes/question-photo.test.tsx | 213 ++++++++++++++++++ src/components/Componentes/question-photo.tsx | 190 ++++++++++++---- .../Componentes/question-section-flow.tsx | 2 +- src/hooks/marriage/use-upload-tmp-media.ts | 1 + src/lib/http.ts | 71 ++++++ src/translations/locales/ar.json | 7 +- src/translations/locales/az.json | 7 +- src/translations/locales/bn.json | 7 +- src/translations/locales/da.json | 7 +- src/translations/locales/de.json | 7 +- src/translations/locales/en.json | 7 +- src/translations/locales/es.json | 7 +- src/translations/locales/fa.json | 7 +- src/translations/locales/fr.json | 7 +- src/translations/locales/gu.json | 7 +- src/translations/locales/ha.json | 7 +- src/translations/locales/he.json | 7 +- src/translations/locales/hi.json | 7 +- src/translations/locales/id.json | 7 +- src/translations/locales/ks.json | 7 +- src/translations/locales/pt.json | 7 +- src/translations/locales/ru.json | 7 +- src/translations/locales/sw.json | 7 +- src/translations/locales/tg.json | 7 +- src/translations/locales/tr.json | 7 +- src/translations/locales/ul.json | 7 +- src/translations/locales/ur.json | 7 +- src/translations/locales/uz.json | 7 +- src/translations/locales/zh.json | 7 +- 30 files changed, 643 insertions(+), 128 deletions(-) create mode 100644 src/components/Componentes/question-photo.test.tsx diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index af9a9a8..7bce970 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -7,9 +7,10 @@ import { uploadTmpMedia, useUploadTmpMediaMutation, } from "@/hooks/marriage/use-upload-tmp-media"; -import { getApiRequestUrl } from "@/lib/http"; +import { getApiRequestUrl, resolveMediaUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; import { useI18n } from "@/translations/provider"; +import ErrorToast from "./error-toast"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; import { LoadingSkeleton } from "./loading-skeleton"; @@ -191,12 +192,7 @@ export function QuestionFile({ .filter((item): item is string | Record => Boolean(item)) .map((item) => { if (typeof item === "string") { - const resolvedUrl = - item.startsWith("http") || - item.startsWith("blob:") || - item.startsWith("data:") - ? item - : getApiRequestUrl(item); + const resolvedUrl = resolveMediaUrl(item) ?? item; return { url: item, previewUrl: resolvedUrl, @@ -206,22 +202,12 @@ export function QuestionFile({ const url = (item.url || item.path || "") as string; const name = (item.name || url.split("/").pop() || t.defaultDoc) as string; - const resolvedUrl = - url.startsWith("http") || - url.startsWith("blob:") || - url.startsWith("data:") - ? url - : getApiRequestUrl(url); + const resolvedUrl = resolveMediaUrl(url) ?? url; return { url, name, previewUrl: resolvedUrl }; }); } if (typeof stored === "string" && stored.trim().length > 0) { - const resolvedUrl = - stored.startsWith("http") || - stored.startsWith("blob:") || - stored.startsWith("data:") - ? stored - : getApiRequestUrl(stored); + const resolvedUrl = resolveMediaUrl(stored) ?? stored; return [ { url: stored, @@ -242,6 +228,9 @@ export function QuestionFile({ const [uploadProgress, setUploadProgress] = useState( debugProgress ?? null, ); + const [toastMessage, setToastMessage] = useState(null); + const [imgErrorSet, setImgErrorSet] = useState>(() => new Set()); + const activeBlobUrlsRef = useRef>(new Map()); const isInitiatorRef = useRef(false); const pendingDeferredRef = useRef<{ resolve: (val?: any) => void; @@ -249,6 +238,16 @@ export function QuestionFile({ } | null>(null); const fileInputRef = useRef(null); + // Revoke blob URLs on unmount + useEffect(() => { + return () => { + for (const blobUrl of activeBlobUrlsRef.current.values()) { + URL.revokeObjectURL(blobUrl); + } + activeBlobUrlsRef.current.clear(); + }; + }, []); + // Sync state when answers load asynchronously from server/cache useEffect(() => { if (storedValue !== undefined && storedValue !== null) { @@ -263,9 +262,7 @@ export function QuestionFile({ const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { if (response.path) { - const resolved = response.path.startsWith("http") - ? response.path - : getApiRequestUrl(response.path); + const resolved = resolveMediaUrl(response.path) ?? response.path; const newDoc: UploadedDoc = { url: response.path, name: response.name ?? response.path.split("/").pop() ?? t.defaultDoc, @@ -287,6 +284,7 @@ export function QuestionFile({ console.error("File upload error:", error); setIsUploading(false); setUploadProgress(null); + setToastMessage(t.uploadFailed); }, }); @@ -330,12 +328,7 @@ export function QuestionFile({ (f?.data?.path as string | undefined) || (f?.data?.url as string | undefined); if (remoteUrl) { - const resolved = - remoteUrl.startsWith("http") || - remoteUrl.startsWith("blob:") || - remoteUrl.startsWith("data:") - ? remoteUrl - : getApiRequestUrl(remoteUrl); + const resolved = resolveMediaUrl(remoteUrl) ?? remoteUrl; newDocs.push({ url: remoteUrl, name: f?.name || remoteUrl.split("/").pop() || t.defaultDoc, @@ -439,32 +432,59 @@ export function QuestionFile({ setIsUploading(true); setUploadProgress(0); + setToastMessage(null); + + // Create instant blob previews for image files + const blobPreviews: { file: File; blobUrl: string | null }[] = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const isImg = file.type.startsWith("image/"); + if (isImg) { + const blobUrl = URL.createObjectURL(file); + blobPreviews.push({ file, blobUrl }); + } else { + blobPreviews.push({ file, blobUrl: null }); + } + } const uploadTask = (async () => { const newDocs: UploadedDoc[] = []; + let hasError = false; - for (let i = 0; i < files.length; i++) { - const file = files[i]; + for (let i = 0; i < blobPreviews.length; i++) { + const { file, blobUrl } = blobPreviews[i]; try { const res = await uploadTmpMedia(file, (p) => setUploadProgress(p)); if (res.path) { - const resolved = res.path.startsWith("http") - ? res.path - : getApiRequestUrl(res.path); + const resolved = resolveMediaUrl(res.path) ?? res.path; + // Use blob preview URL if available (instant), otherwise use resolved backend URL + const previewUrl = blobUrl ?? resolved; + if (blobUrl) { + activeBlobUrlsRef.current.set(res.path, blobUrl); + } newDocs.push({ url: res.path, name: res.name || file.name, - previewUrl: resolved, + previewUrl, }); } } catch (err) { console.error("Failed to upload tmp file:", file.name, err); + // Revoke blob URL on failure + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } + hasError = true; } } setIsUploading(false); setUploadProgress(null); + if (hasError && newDocs.length === 0) { + setToastMessage(t.uploadFailed); + } + if (newDocs.length > 0) { setUploadedDocs((prev) => { const nextDocs = [...prev, ...newDocs]; @@ -488,7 +508,25 @@ export function QuestionFile({ e.stopPropagation(); e.preventDefault(); setUploadedDocs((prev) => { + const removedDoc = prev[indexToRemove]; + // Revoke blob URL if exists + if (removedDoc) { + const blobUrl = activeBlobUrlsRef.current.get(removedDoc.url); + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + activeBlobUrlsRef.current.delete(removedDoc.url); + } + } const nextDocs = prev.filter((_, idx) => idx !== indexToRemove); + // Clear image error tracking for removed index + setImgErrorSet((prevSet) => { + const newSet = new Set(); + for (const errIdx of prevSet) { + if (errIdx < indexToRemove) newSet.add(errIdx); + else if (errIdx > indexToRemove) newSet.add(errIdx - 1); + } + return newSet; + }); if (nextDocs.length === 0) { setAnswerValue(question, null); } else if (nextDocs.length === 1) { @@ -522,6 +560,13 @@ export function QuestionFile({ disabled ? "pointer-events-none opacity-30" : "", ].join(" ")} > + {toastMessage && ( + setToastMessage(null)} + /> + )} + {/* Hidden browser input for fallback */} @@ -561,17 +606,22 @@ export function QuestionFile({
{uploadedDocs.map((doc, index) => { const isImg = isImageFile(doc.name, doc.previewUrl ?? doc.url); + const hasThumbError = imgErrorSet.has(index); + const ext = doc.name?.split(".").pop()?.toUpperCase() || "FILE"; return (
- {/* Left: Thumbnail or PDF Icon */} + {/* Left: Thumbnail or File Icon */}
- {isImg && doc.previewUrl ? ( + {isImg && doc.previewUrl && !hasThumbError ? ( {doc.name} { + setImgErrorSet((prev) => new Set(prev).add(index)); + }} className="h-12 w-12 shrink-0 rounded-[10px] object-cover border border-[#E5E7EB]" /> ) : ( @@ -590,7 +640,7 @@ export function QuestionFile({ - PDF + {ext.length <= 4 ? ext : "FILE"}
)} diff --git a/src/components/Componentes/question-photo.test.tsx b/src/components/Componentes/question-photo.test.tsx new file mode 100644 index 0000000..bdc8c96 --- /dev/null +++ b/src/components/Componentes/question-photo.test.tsx @@ -0,0 +1,213 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { QuestionField } from "@/lib/schema-adapter"; +import { QuestionPhoto } from "./question-photo"; + +let answerMap: Record = {}; +const mockSetAnswerValue = vi.fn((q: QuestionField, val: unknown) => { + answerMap[q.id] = val; +}); +const mockRegisterPendingUpload = vi.fn(); + +const mockUploadTmpMedia = vi.fn(async (file: File) => ({ + path: `/media/tmp/${file.name}`, + name: file.name, +})); + +vi.mock("@/hooks/marriage/use-upload-tmp-media", () => ({ + useUploadTmpMediaMutation: ({ + onSuccess, + onError, + }: { + onSuccess?: (data: any) => void; + onError?: (err: any) => void; + }) => ({ + mutate: (file: File) => { + mockUploadTmpMedia(file) + .then((res) => onSuccess?.(res)) + .catch((err) => onError?.(err)); + }, + mutateAsync: async (file: File) => { + try { + const res = await mockUploadTmpMedia(file); + onSuccess?.(res); + return res; + } catch (err) { + onError?.(err); + throw err; + } + }, + isPending: false, + isError: false, + }), +})); + +vi.mock("./question-answer-storage", () => ({ + useQuestionAnswers: () => ({ + getAnswerValue: (q: QuestionField) => answerMap[q.id] ?? null, + setAnswerValue: mockSetAnswerValue, + registerPendingUpload: mockRegisterPendingUpload, + isLoading: false, + }), +})); + +vi.mock("@/translations/provider", () => ({ + useI18n: () => ({ + locale: "en", + dictionary: { + "Photo upload failed. Please try again.": "Photo upload failed. Please try again.", + "Failed to load image. Please try uploading again.": "Failed to load image. Please try uploading again.", + }, + }), +})); + +const mockPhotoQuestion: QuestionField = { + id: "documents_verification.face_photo", + title: "Face Photo", + type: "photo", + order: 1, + required: true, + baseRequired: true, + isVisible: true, + description: "Recent clear face photo", + tooltip: "", + extras: { + options: [".jpg", ".jpeg", ".png"], + }, + options: [], +}; + +describe("QuestionPhoto Component", () => { + beforeEach(() => { + answerMap = {}; + mockSetAnswerValue.mockClear(); + mockUploadTmpMedia.mockClear(); + mockRegisterPendingUpload.mockClear(); + vi.clearAllMocks(); + }); + + afterEach(() => { + cleanup(); + }); + + it("renders default avatar placeholder when no photo uploaded", () => { + const { container } = render(); + + expect(screen.getByText(/Face/)).toBeInTheDocument(); + expect(screen.getByText(/Photo/)).toBeInTheDocument(); + expect(screen.getByText("Recent clear face photo")).toBeInTheDocument(); + // Default placeholder SVG is rendered + expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); + }); + + it("renders uploaded image and camera badge when answer is provided", () => { + answerMap[mockPhotoQuestion.id] = "https://example.com/avatar.jpg"; + const { container } = render(); + + const img = container.querySelector("img[src='https://example.com/avatar.jpg']"); + expect(img).toBeInTheDocument(); + expect(img).toHaveClass("rounded-full"); + // Camera badge rendered + expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeInTheDocument(); + }); + + it("triggers upload and renders circular spinner without black background overlay during file selection", async () => { + let resolveUpload!: (val: any) => void; + mockUploadTmpMedia.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveUpload = resolve; + }), + ); + + const { container } = render(); + + const fileInput = container.querySelector("input[type='file']") as HTMLInputElement; + const testFile = new File(["dummy content"], "my-photo.jpg", { type: "image/jpeg" }); + + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = vi.fn(() => "blob:http://localhost/test-blob"); + + try { + fireEvent.change(fileInput, { target: { files: [testFile] } }); + + await waitFor(() => { + const spinner = container.querySelector(".animate-spin"); + expect(spinner).toBeInTheDocument(); + expect(spinner).toHaveClass("border-[#F0445B]/25"); + }); + + expect(container.querySelector(".bg-black\\/40")).toBeNull(); + expect(container.querySelector(".bg-black")).toBeNull(); + + resolveUpload({ path: "/media/uploaded.jpg" }); + + await waitFor(() => { + expect(container.querySelector(".animate-spin")).toBeNull(); + }); + } finally { + URL.createObjectURL = originalCreateObjectURL; + } + }); + + it("shows error toast, reverts to default avatar placeholder, and clears answer when upload fails", async () => { + mockUploadTmpMedia.mockImplementationOnce( + () => Promise.reject(new Error("Network Error")), + ); + + const { container } = render(); + const fileInput = container.querySelector("input[type='file']") as HTMLInputElement; + const testFile = new File(["dummy content"], "bad-photo.jpg", { type: "image/jpeg" }); + + const originalCreateObjectURL = URL.createObjectURL; + const originalRevokeObjectURL = URL.revokeObjectURL; + URL.createObjectURL = vi.fn(() => "blob:http://localhost/bad-blob"); + URL.revokeObjectURL = vi.fn(); + + try { + fireEvent.change(fileInput, { target: { files: [testFile] } }); + + // Error toast should appear + await waitFor(() => { + expect(screen.getByText("Photo upload failed. Please try again.")).toBeInTheDocument(); + }); + + // Answer value is cleared + expect(mockSetAnswerValue).toHaveBeenCalledWith(mockPhotoQuestion, null); + + // Default avatar SVG placeholder is restored + expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); + + // Camera badge is not visible + expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeNull(); + + // File input is reset allowing re-upload + expect(fileInput.value).toBe(""); + } finally { + URL.createObjectURL = originalCreateObjectURL; + URL.revokeObjectURL = originalRevokeObjectURL; + } + }); + + it("shows error toast and reverts to default avatar when rendered image fails to load", async () => { + answerMap[mockPhotoQuestion.id] = "https://example.com/broken-avatar.jpg"; + const { container } = render(); + + const img = container.querySelector("img[src='https://example.com/broken-avatar.jpg']") as HTMLImageElement; + expect(img).toBeInTheDocument(); + + // Trigger image error (e.g. 404 / ERR_CONNECTION_REFUSED) + fireEvent.error(img); + + await waitFor(() => { + expect(screen.getByText("Failed to load image. Please try uploading again.")).toBeInTheDocument(); + }); + + // Answer value cleared + expect(mockSetAnswerValue).toHaveBeenCalledWith(mockPhotoQuestion, null); + + // Default avatar placeholder rendered instead of broken image + expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); + expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeNull(); + }); +}); diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 582452b..bad1619 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -4,11 +4,12 @@ import Image from "next/image"; import { type ReactNode, useCallback, useEffect, useId, useRef, useState } from "react"; import type { QuestionField } from "@/lib/schema-adapter"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; -import { getApiRequestUrl } from "@/lib/http"; +import { resolveMediaUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; +import { useI18n } from "@/translations/provider"; +import ErrorToast from "./error-toast"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; -import { LoadingSkeleton } from "./loading-skeleton"; type QuestionPhotoProps = { question: QuestionField; @@ -22,15 +23,21 @@ export function QuestionPhoto({ disabled, }: QuestionPhotoProps) { const inputId = useId(); + const fileInputRef = useRef(null); + const activeBlobUrlRef = useRef(null); + + const { dictionary: t } = useI18n(); const [localPreviewUrl, setLocalPreviewUrl] = useState(null); const [isUploading, setIsUploading] = useState(false); + const [toastMessage, setToastMessage] = useState(null); + const [hasImageError, setHasImageError] = useState(false); const acceptedFiles = question.extras?.options && question.extras.options.length > 0 ? question.extras.options.join(",") : "image/*"; - const descriptionContent = description ?? question.description; + const { getAnswerValue, setAnswerValue, registerPendingUpload } = useQuestionAnswers(); const storedValue = getAnswerValue(question); @@ -41,17 +48,72 @@ export function QuestionPhoto({ reject: (err?: any) => void; } | null>(null); + // Revoke blob URL on unmount + useEffect(() => { + return () => { + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + activeBlobUrlRef.current = null; + } + }; + }, []); + + const handleUploadFailure = useCallback( + (errorMessage?: string) => { + setIsUploading(false); + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + activeBlobUrlRef.current = null; + } + setLocalPreviewUrl(null); + setAnswerValue(question, null); + setHasImageError(true); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setToastMessage( + errorMessage ?? + t["Photo upload failed. Please try again."] ?? + "Photo upload failed. Please try again.", + ); + }, + [question, setAnswerValue, t], + ); + + const handleImageError = useCallback(() => { + console.error("Photo preview failed to load"); + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + activeBlobUrlRef.current = null; + } + setLocalPreviewUrl(null); + setAnswerValue(question, null); + setHasImageError(true); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setToastMessage( + t["Failed to load image. Please try uploading again."] ?? + "Failed to load image. Please try uploading again.", + ); + }, [question, setAnswerValue, t]); + const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { - if (response.path) { + if (response?.path) { setAnswerValue(question, response.path); - setLocalPreviewUrl(response.path); + setHasImageError(false); + if (!activeBlobUrlRef.current) { + setLocalPreviewUrl(response.path); + } + } else { + handleUploadFailure(); } setIsUploading(false); }, onError: (error) => { console.error("Photo upload error:", error); - setIsUploading(false); + handleUploadFailure(); }, }); @@ -72,6 +134,8 @@ export function QuestionPhoto({ case "picked": case "progress": setIsUploading(true); + setHasImageError(false); + setToastMessage(null); if (event.data?.files?.[0]?.base64) { const b64 = event.data.files[0].base64; setLocalPreviewUrl(b64); @@ -88,7 +152,8 @@ export function QuestionPhoto({ (file?.data?.url as string | undefined); if (remoteUrl) { setAnswerValue(question, remoteUrl); - setLocalPreviewUrl(remoteUrl); + setLocalPreviewUrl((prev) => prev || remoteUrl); + setHasImageError(false); pendingDeferredRef.current?.resolve(remoteUrl); pendingDeferredRef.current = null; } else if (file?.base64) { @@ -105,14 +170,18 @@ export function QuestionPhoto({ .then((res) => { if (res?.path) { setAnswerValue(question, res.path); - setLocalPreviewUrl(res.path); + setLocalPreviewUrl((prev) => prev || res.path); + setHasImageError(false); pendingDeferredRef.current?.resolve(res.path); } else { + handleUploadFailure(); pendingDeferredRef.current?.resolve(); } pendingDeferredRef.current = null; }) .catch((err) => { + console.error("Flutter photo upload error:", err); + handleUploadFailure(); pendingDeferredRef.current?.reject(err); pendingDeferredRef.current = null; }); @@ -124,10 +193,19 @@ export function QuestionPhoto({ break; } case "cancelled": + setIsUploading(false); + isInitiatorRef.current = false; + if (!storedValue) { + setLocalPreviewUrl(null); + } + pendingDeferredRef.current?.reject(new Error("Upload cancelled")); + pendingDeferredRef.current = null; + break; case "failed": setIsUploading(false); isInitiatorRef.current = false; - pendingDeferredRef.current?.reject(new Error("Upload failed or cancelled")); + handleUploadFailure(); + pendingDeferredRef.current?.reject(new Error("Upload failed")); pendingDeferredRef.current = null; break; } @@ -136,7 +214,14 @@ export function QuestionPhoto({ return () => { unsubscribe?.(); }; - }, [question, setAnswerValue, registerPendingUpload, uploadTmpMediaMutation]); + }, [ + question, + setAnswerValue, + registerPendingUpload, + uploadTmpMediaMutation, + handleUploadFailure, + storedValue, + ]); const handleFlutterPick = useCallback(() => { const extensions = (question.extras?.options ?? []).map((o) => @@ -145,6 +230,8 @@ export function QuestionPhoto({ isInitiatorRef.current = true; setIsUploading(true); + setHasImageError(false); + setToastMessage(null); let resolveFn!: (val?: any) => void; let rejectFn!: (err?: any) => void; @@ -184,8 +271,28 @@ export function QuestionPhoto({ const file = files?.[0]; if (!file) return; - // Create synchronous object URL for instant UI preview only + // Validate size (10 MB limit) + if (file.size > 10_485_760) { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setToastMessage( + t["File size exceeds the 10 MB limit."] ?? + "File size exceeds the 10 MB limit.", + ); + return; + } + + // Reset previous errors + setHasImageError(false); + setToastMessage(null); + + // Create synchronous object URL for instant UI preview + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + } const objectUrl = URL.createObjectURL(file); + activeBlobUrlRef.current = objectUrl; setLocalPreviewUrl(objectUrl); // Trigger background upload and register promise with central storage @@ -195,13 +302,18 @@ export function QuestionPhoto({ .then((res) => { if (res?.path) { setAnswerValue(question, res.path); - setLocalPreviewUrl(res.path); + setHasImageError(false); + if (!activeBlobUrlRef.current) { + setLocalPreviewUrl(res.path); + } + } else { + handleUploadFailure(); } setIsUploading(false); }) .catch((err) => { console.error("Photo upload error:", err); - setIsUploading(false); + handleUploadFailure(); }); registerPendingUpload?.(question.id, uploadPromise); @@ -215,27 +327,19 @@ export function QuestionPhoto({ }; // Determine photo URL to display - let displayUrl: string | null = localPreviewUrl; + let candidateUrl: string | null = localPreviewUrl; if ( - !displayUrl && + !candidateUrl && + !hasImageError && typeof storedValue === "string" && storedValue.trim().length > 0 ) { - const val = storedValue.trim(); - if ( - val.startsWith("http://") || - val.startsWith("https://") || - val.startsWith("blob:") || - val.startsWith("data:") - ) { - displayUrl = val; - } else { - displayUrl = getApiRequestUrl(val); - } + candidateUrl = storedValue.trim(); } - const hasAnswer = Boolean(displayUrl); + const displayUrl = hasImageError ? null : resolveMediaUrl(candidateUrl); + const hasAnswer = Boolean(displayUrl && !hasImageError); return (
+ {toastMessage && ( + setToastMessage(null)} + /> + )} + -
- {displayUrl ? ( +
+ {hasAnswer && displayUrl ? ( {typeof ) : ( @@ -283,8 +396,8 @@ export function QuestionPhoto({ )} {/* Red camera badge when image is uploaded */} - {displayUrl && ( - + {hasAnswer && !isPending && ( + )} - {/* Loading spinner during upload matching Flutter ProfileAvatarWidget */} + {/* Circular loading spinner during upload without black overlay */} {isPending && ( -
-
+
+
)}
- {descriptionContent ? ( - - {descriptionContent} - - ) : null}
diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index 4319497..327be2b 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -298,7 +298,7 @@ function SectionFlowContent({ -
+
, + document.body, ); } diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index 7bce970..b662781 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -417,7 +417,7 @@ export function QuestionFile({ source: pickerSource as any, picker_type: pickerType as any, returnAs: "upload", - uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`, + uploadUrl: `${window.location.origin}/api/proxy?__proxyPath=/upload-tmp-media/`, fieldName: "file", allowedExtensions: extensions, maxBytes: 10_485_760, // 10 MB diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index bad1619..97f6b9a 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -257,7 +257,7 @@ export function QuestionPhoto({ source: pickerSource as any, picker_type: pickerType as any, returnAs: "upload", - uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`, + uploadUrl: `${window.location.origin}/api/proxy?__proxyPath=/upload-tmp-media/`, fieldName: "file", allowedExtensions: extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], From 4468e832bfca974358d47ef3a1acca074fb98e4e Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 12 Sep 2026 16:24:01 +0330 Subject: [PATCH 20/27] feat: add QuestionPhoto component for image uploads and previews --- src/components/Componentes/question-photo.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 97f6b9a..1d84377 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -92,11 +92,15 @@ export function QuestionPhoto({ if (fileInputRef.current) { fileInputRef.current.value = ""; } - setToastMessage( - t["Failed to load image. Please try uploading again."] ?? - "Failed to load image. Please try uploading again.", - ); - }, [question, setAnswerValue, t]); + // Only show the toast if the user was actively uploading; + // stale stored URLs from previous sessions fail silently. + if (isUploading || isInitiatorRef.current) { + setToastMessage( + t["Failed to load image. Please try uploading again."] ?? + "Failed to load image. Please try uploading again.", + ); + } + }, [question, setAnswerValue, t, isUploading]); const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { @@ -256,8 +260,7 @@ export function QuestionPhoto({ mediaType: "image", source: pickerSource as any, picker_type: pickerType as any, - returnAs: "upload", - uploadUrl: `${window.location.origin}/api/proxy?__proxyPath=/upload-tmp-media/`, + returnAs: "base64", fieldName: "file", allowedExtensions: extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], From c90f068a1af8ed787ab8bed39d65efaf3bb17420 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sat, 12 Sep 2026 16:41:28 +0330 Subject: [PATCH 21/27] feat: add question-file and question-photo components for media uploads --- src/components/Componentes/question-file.tsx | 31 +++-- src/components/Componentes/question-photo.tsx | 119 +++++++++++------- 2 files changed, 94 insertions(+), 56 deletions(-) diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index b662781..a089c80 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -164,6 +164,20 @@ function isImageFile( return /\.(jpg|jpeg|png|webp|gif|svg|bmp|avif)$/i.test(nameToCheck); } +function base64ToFile(dataUrl: string, filename = "document"): File { + const parts = dataUrl.split(","); + const mimeMatch = parts[0]?.match(/:(.*?);/); + const mime = mimeMatch ? mimeMatch[1] : "application/octet-stream"; + const base64Data = parts[1] || parts[0]; + const binaryString = atob(base64Data); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new File([bytes], filename, { type: mime }); +} + export type UploadedDoc = { url: string; name: string; @@ -335,15 +349,12 @@ export function QuestionFile({ previewUrl: resolved, }); } else if (f?.base64) { - const b64 = f.base64; - fetch(b64) - .then((res) => res.blob()) - .then((blob) => { - const fileObj = new File([blob], f.name ?? "document", { - type: blob.type, - }); - uploadTmpMediaMutation.mutate(fileObj); - }); + try { + const fileObj = base64ToFile(f.base64, f.name ?? "document"); + uploadTmpMediaMutation.mutate(fileObj); + } catch (e) { + console.error("Failed to decode base64 file:", e); + } } } @@ -618,7 +629,7 @@ export function QuestionFile({ {isImg && doc.previewUrl && !hasThumbError ? ( { setImgErrorSet((prev) => new Set(prev).add(index)); }} diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 1d84377..8292006 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -17,6 +17,20 @@ type QuestionPhotoProps = { disabled?: boolean; }; +function base64ToFile(dataUrl: string, filename = "photo.jpg"): File { + const parts = dataUrl.split(","); + const mimeMatch = parts[0]?.match(/:(.*?);/); + const mime = mimeMatch ? mimeMatch[1] : "image/jpeg"; + const base64Data = parts[1] || parts[0]; + const binaryString = atob(base64Data); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new File([bytes], filename, { type: mime }); +} + export function QuestionPhoto({ question, description, @@ -37,7 +51,6 @@ export function QuestionPhoto({ ? question.extras.options.join(",") : "image/*"; - const { getAnswerValue, setAnswerValue, registerPendingUpload } = useQuestionAnswers(); const storedValue = getAnswerValue(question); @@ -61,6 +74,7 @@ export function QuestionPhoto({ const handleUploadFailure = useCallback( (errorMessage?: string) => { setIsUploading(false); + isInitiatorRef.current = false; if (activeBlobUrlRef.current) { URL.revokeObjectURL(activeBlobUrlRef.current); activeBlobUrlRef.current = null; @@ -92,15 +106,11 @@ export function QuestionPhoto({ if (fileInputRef.current) { fileInputRef.current.value = ""; } - // Only show the toast if the user was actively uploading; - // stale stored URLs from previous sessions fail silently. - if (isUploading || isInitiatorRef.current) { - setToastMessage( - t["Failed to load image. Please try uploading again."] ?? - "Failed to load image. Please try uploading again.", - ); - } - }, [question, setAnswerValue, t, isUploading]); + setToastMessage( + t["Failed to load image. Please try uploading again."] ?? + "Failed to load image. Please try uploading again.", + ); + }, [question, setAnswerValue, t]); const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { @@ -114,6 +124,7 @@ export function QuestionPhoto({ handleUploadFailure(); } setIsUploading(false); + isInitiatorRef.current = false; }, onError: (error) => { console.error("Photo upload error:", error); @@ -140,57 +151,70 @@ export function QuestionPhoto({ setIsUploading(true); setHasImageError(false); setToastMessage(null); - if (event.data?.files?.[0]?.base64) { - const b64 = event.data.files[0].base64; - setLocalPreviewUrl(b64); - } break; case "completed": { - setIsUploading(false); - isInitiatorRef.current = false; const file = event.data?.files?.[0]; const remoteUrl = file?.path || file?.url || (file?.data?.path as string | undefined) || (file?.data?.url as string | undefined); + if (remoteUrl) { + setIsUploading(false); + isInitiatorRef.current = false; setAnswerValue(question, remoteUrl); setLocalPreviewUrl((prev) => prev || remoteUrl); setHasImageError(false); pendingDeferredRef.current?.resolve(remoteUrl); pendingDeferredRef.current = null; } else if (file?.base64) { - const b64 = file.base64; - setLocalPreviewUrl(b64); - const uploadPromise = fetch(b64) - .then((res) => res.blob()) - .then((blob) => { - const f = new File([blob], file.name ?? "photo.jpg", { - type: blob.type || "image/jpeg", - }); - return uploadTmpMediaMutation.mutateAsync(f); - }) - .then((res) => { - if (res?.path) { - setAnswerValue(question, res.path); - setLocalPreviewUrl((prev) => prev || res.path); - setHasImageError(false); - pendingDeferredRef.current?.resolve(res.path); - } else { + setIsUploading(true); + setHasImageError(false); + setToastMessage(null); + try { + const f = base64ToFile(file.base64, file.name ?? "photo.jpg"); + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + } + const objectUrl = URL.createObjectURL(f); + activeBlobUrlRef.current = objectUrl; + setLocalPreviewUrl(objectUrl); + + const uploadPromise = uploadTmpMediaMutation + .mutateAsync(f) + .then((res) => { + if (res?.path) { + setAnswerValue(question, res.path); + setHasImageError(false); + if (!activeBlobUrlRef.current) { + setLocalPreviewUrl(res.path); + } + pendingDeferredRef.current?.resolve(res.path); + } else { + handleUploadFailure(); + pendingDeferredRef.current?.resolve(); + } + setIsUploading(false); + isInitiatorRef.current = false; + pendingDeferredRef.current = null; + }) + .catch((err) => { + console.error("Flutter photo upload to server error:", err); handleUploadFailure(); - pendingDeferredRef.current?.resolve(); - } - pendingDeferredRef.current = null; - }) - .catch((err) => { - console.error("Flutter photo upload error:", err); - handleUploadFailure(); - pendingDeferredRef.current?.reject(err); - pendingDeferredRef.current = null; - }); - registerPendingUpload?.(question.id, uploadPromise); + pendingDeferredRef.current?.reject(err); + pendingDeferredRef.current = null; + }); + registerPendingUpload?.(question.id, uploadPromise); + } catch (convErr) { + console.error("Failed to decode base64 photo:", convErr); + handleUploadFailure(); + pendingDeferredRef.current?.reject(convErr); + pendingDeferredRef.current = null; + } } else { + setIsUploading(false); + isInitiatorRef.current = false; pendingDeferredRef.current?.resolve(); pendingDeferredRef.current = null; } @@ -208,7 +232,7 @@ export function QuestionPhoto({ case "failed": setIsUploading(false); isInitiatorRef.current = false; - handleUploadFailure(); + handleUploadFailure(event.message || event.data?.message); pendingDeferredRef.current?.reject(new Error("Upload failed")); pendingDeferredRef.current = null; break; @@ -265,6 +289,9 @@ export function QuestionPhoto({ allowedExtensions: extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], maxBytes: 10_485_760, // 10 MB + maxWidth: 1024, + maxHeight: 1024, + imageQuality: 85, title: typeof question.title === "string" ? question.title : "Upload Photo", }); @@ -374,7 +401,7 @@ export function QuestionPhoto({ className="sr-only" />
-
@@ -463,3 +333,4 @@ export function QuestionPhoto({ } export default QuestionPhoto; +