Translate saved searches

This commit is contained in:
MartinBraquet
2026-02-19 12:19:35 +01:00
parent cf843f66c4
commit 4b894363af
20 changed files with 268 additions and 66 deletions

View File

@@ -1,7 +1,4 @@
import {invert} from "lodash";
import {FaHeart, FaUsers} from "react-icons/fa";
import {FiUser} from "react-icons/fi";
import {GiRing} from "react-icons/gi";
export const RELATIONSHIP_CHOICES = {
// Other: 'other',
@@ -18,14 +15,6 @@ export const RELATIONSHIP_STATUS_CHOICES = {
'In open relationship': 'open',
};
export const RELATIONSHIP_ICONS = {
single: FiUser,
married: GiRing,
casual: FaHeart,
long_term: FaHeart,
open: FaUsers,
} as const;
export const ROMANTIC_CHOICES = {
Monogamous: 'mono',
Polyamorous: 'poly',

View File

@@ -2,28 +2,40 @@
import {FilterFields, initialFilters} from 'common/filters'
import {wantsKidsNames} from 'common/wants-kids'
import {hasKidsNames} from 'common/has-kids'
import {milesToKm} from "common/measurement-utils";
import {milesToKm} from 'common/measurement-utils'
import {
INVERTED_DIET_CHOICES,
INVERTED_EDUCATION_CHOICES,
INVERTED_GENDERS,
INVERTED_LANGUAGE_CHOICES,
INVERTED_MBTI_CHOICES,
INVERTED_POLITICAL_CHOICES,
INVERTED_RELATIONSHIP_CHOICES,
INVERTED_RELATIONSHIP_STATUS_CHOICES,
INVERTED_RELIGION_CHOICES,
INVERTED_ROMANTIC_CHOICES,
} from 'common/choices'
import {capitalize} from 'lodash'
const filterLabels: Record<string, string> = {
geodbCityIds: '',
location: '',
name: 'Searching',
genders: '',
pref_gender: 'Gender they seek',
education_levels: 'Education',
pref_age_max: 'Max age',
pref_age_min: 'Min age',
drinks_max: 'Max drinks',
drinks_min: 'Min drinks',
relationship_status: '',
has_kids: '',
wants_kids_strength: 'Kids',
wants_kids_strength: '',
is_smoker: '',
pref_relation_styles: 'Seeking',
pref_romantic_styles: '',
interests: '',
causes: '',
work: '',
religion: '',
pref_gender: '',
orderBy: '',
diet: 'Diet',
political_beliefs: 'Political views',
@@ -48,29 +60,49 @@ const skippedKeys = [
'lat',
'lon',
'radius',
// Big Five min/max keys are handled separately
'big5_openness_min',
'big5_openness_max',
'big5_conscientiousness_min',
'big5_conscientiousness_max',
'big5_extraversion_min',
'big5_extraversion_max',
'big5_agreeableness_min',
'big5_agreeableness_max',
'big5_neuroticism_min',
'big5_neuroticism_max',
// Drinks min/max keys are handled separately
'drinks_min',
'drinks_max',
]
export function formatFilters(
filters: Partial<FilterFields>,
location: locationType | null,
choicesIdsToLabels: Record<string, any>,
measurementSystem?: 'metric' | 'imperial'
measurementSystem?: 'metric' | 'imperial',
t?: (key: string, fallback: string) => string
): String[] | null {
const entries: String[] = []
// Helper function to translate UI text
const translate = (key: string, fallback: string): string => {
return t ? t(key, fallback) : fallback
}
let ageEntry = null
let ageMin: number | undefined | null = filters.pref_age_min
if (ageMin == 18) ageMin = undefined
let ageMax = filters.pref_age_max
if (ageMax == 100) ageMax = undefined
if (ageMin || ageMax) {
let text: string = 'Age: '
let text: string = translate('filter.age.label', 'Age') + ': '
if (ageMin) text = `${text}${ageMin}`
if (ageMax) {
if (ageMin) {
text = `${text}-${ageMax}`
} else {
text = `${text}up to ${ageMax}`
text = `${text}${translate('filter.age.up_to', 'up to')} ${ageMax}`
}
} else {
text = `${text}+`
@@ -88,13 +120,81 @@ export function formatFilters(
const label = filterLabels[typedKey] ?? key
// Translate the label if it exists and we have a translation function
let translatedLabel = label
if (label && t) {
const labelKey = `filter.label.${typedKey}`
translatedLabel = t(labelKey, label)
}
console.log(key, value)
let stringValue = value
if (key === 'has_kids') stringValue = hasKidsNames[value as number]
if (key === 'wants_kids_strength')
stringValue = wantsKidsNames[value as number]
if (key === 'has_kids')
stringValue = translate(
`profile.has_kids.${value}`,
hasKidsNames[value as number]
)
else if (key === 'wants_kids_strength')
stringValue = translate(
`profile.wants_kids_${value}`,
wantsKidsNames[value as number]
)
else if (key === 'is_smoker')
stringValue = translate(
`profile.smoker.${value ? 'yes' : 'no'}`,
value ? 'Smoker' : 'Non-smoker'
)
if (Array.isArray(value)) {
if (choicesIdsToLabels[key]) {
value = value.map((id) => choicesIdsToLabels[key][id])
} else if (key === 'mbti') {
value = value.map((s) => INVERTED_MBTI_CHOICES[s])
} else if (key === 'pref_romantic_styles') {
value = value.map((s) =>
translate(`profile.romantic.${s}`, INVERTED_ROMANTIC_CHOICES[s])
)
} else if (key === 'pref_relation_styles') {
value = value.map((s) =>
translate(
`profile.relationship.${s}`,
INVERTED_RELATIONSHIP_CHOICES[s]
)
)
} else if (key === 'relationship_status') {
value = value.map((s) =>
translate(
`profile.relationship_status.${s}`,
INVERTED_RELATIONSHIP_STATUS_CHOICES[s]
)
)
} else if (key === 'political_beliefs') {
value = value.map((s) =>
translate(`profile.political.${s}`, INVERTED_POLITICAL_CHOICES[s])
)
} else if (key === 'diet') {
value = value.map((s) =>
translate(`profile.diet.${s}`, INVERTED_DIET_CHOICES[s])
)
} else if (key === 'education_levels') {
value = value.map((s) =>
translate(`profile.education.${s}`, INVERTED_EDUCATION_CHOICES[s])
)
} else if (key === 'religion') {
value = value.map((s) =>
translate(`profile.religion.${s}`, INVERTED_RELIGION_CHOICES[s])
)
} else if (key === 'languages') {
value = value.map((s) =>
translate(`profile.language.${s}`, INVERTED_LANGUAGE_CHOICES[s])
)
} else if (key === 'pref_gender') {
value = value.map((s) =>
translate(`profile.gender.${s}`, INVERTED_GENDERS[s])
)
} else if (key === 'genders') {
value = value.map((s) =>
translate(`profile.gender.${s}`, INVERTED_GENDERS[s])
)
}
stringValue = value.join(', ')
}
@@ -106,11 +206,81 @@ export function formatFilters(
const display = stringValue
entries.push(`${label}${label ? ': ' : ''}${display}`)
entries.push(`${translatedLabel}${translatedLabel ? ': ' : ''}${display}`)
})
if (ageEntry) entries.push(ageEntry)
// Process Big Five personality traits as ranges
const big5Traits = [
{name: 'openness', min: 'big5_openness_min', max: 'big5_openness_max'},
{
name: 'conscientiousness',
min: 'big5_conscientiousness_min',
max: 'big5_conscientiousness_max',
},
{
name: 'extraversion',
min: 'big5_extraversion_min',
max: 'big5_extraversion_max',
},
{
name: 'agreeableness',
min: 'big5_agreeableness_min',
max: 'big5_agreeableness_max',
},
{
name: 'neuroticism',
min: 'big5_neuroticism_min',
max: 'big5_neuroticism_max',
},
] as const
big5Traits.forEach(({name, min, max}) => {
const minValue = filters[min as keyof FilterFields] as number | undefined
const maxValue = filters[max as keyof FilterFields] as number | undefined
if (minValue !== undefined || maxValue !== undefined) {
const traitName = translate(`profile.big5_${name}`, capitalize(name))
let rangeText: string
if (minValue !== undefined && maxValue !== undefined) {
// Both min and max: "12-78"
rangeText = `${minValue}-${maxValue}`
} else if (minValue !== undefined) {
// Only min: "12+"
rangeText = `${minValue}+`
} else {
// Only max: "up to 82"
rangeText = `${translate('filter.age.up_to', 'up to')} ${maxValue}`
}
entries.push(`${traitName}: ${rangeText}`)
}
})
// Process drinks as range
const drinksMin = filters.drinks_min
const drinksMax = filters.drinks_max
if (drinksMin !== undefined || drinksMax !== undefined) {
const drinksLabel = translate('filter.label.drinks', 'Drinks')
const perMonth = translate('filter.drinks.per_month', 'per month')
let drinksText: string
if (drinksMin !== undefined && drinksMax !== undefined) {
// Both min and max: "12-78"
drinksText = `${drinksMin}-${drinksMax}`
} else if (drinksMin !== undefined) {
// Only min: "12+"
drinksText = `${drinksMin}+`
} else {
// Only max: "up to 82"
drinksText = `${translate('filter.age.up_to', 'up to')} ${drinksMax}`
}
entries.push(`${drinksLabel}: ${drinksText} ${perMonth}`)
}
if (location?.location?.name) {
const radius = location?.radius || 0
let formattedRadius: string
@@ -123,7 +293,8 @@ export function formatFilters(
entries.push(locString)
}
if (entries.length === 0) return ['Anyone']
if (entries.length === 0)
return [translate('filter.any_new_users', 'Any new user')]
return entries
}

View File

@@ -3,7 +3,7 @@ import {convertDietTypes, DietType,} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {DIET_CHOICES} from 'web/components/filters/choices'
import {DIET_CHOICES} from 'common/choices'
import {FilterFields} from 'common/filters'
import {useT} from 'web/lib/locale'

View File

@@ -2,7 +2,7 @@ import clsx from 'clsx'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {FilterFields} from "common/filters";
import {EDUCATION_CHOICES} from "web/components/filters/choices";
import {EDUCATION_CHOICES} from "common/choices";
import {convertEducationTypes} from "web/lib/util/convert-types";
import stringOrStringArrayToText from "web/lib/util/string-or-string-array-to-text";
import {getSortedOptions} from 'common/util/sorting'

View File

@@ -5,7 +5,7 @@ import {Row} from 'web/components/layout/row'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {useT} from 'web/lib/locale'
import {FilterFields} from "common/filters";
import {GENDERS_PLURAL} from "web/components/filters/choices";
import {GENDERS_PLURAL} from "common/choices";
export function GenderFilterText(props: {
gender: Gender[] | undefined

View File

@@ -3,7 +3,7 @@ import {convertLanguageTypes,} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {LANGUAGE_CHOICES} from 'web/components/filters/choices'
import {LANGUAGE_CHOICES} from 'common/choices'
import {FilterFields} from 'common/filters'
import {getSortedOptions} from 'common/util/sorting'
import {useT} from 'web/lib/locale'

View File

@@ -1,5 +1,5 @@
import clsx from 'clsx'
import {MBTI_CHOICES} from 'web/components/filters/choices'
import {MBTI_CHOICES} from 'common/choices'
import {FilterFields} from 'common/filters'
import {getSortedOptions} from 'common/util/sorting'
import {MultiCheckbox} from 'web/components/multi-checkbox'

View File

@@ -3,7 +3,7 @@ import {convertPoliticalTypes,} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {POLITICAL_CHOICES} from 'web/components/filters/choices'
import {POLITICAL_CHOICES} from 'common/choices'
import {FilterFields} from 'common/filters'
import {getSortedOptions} from 'common/util/sorting'
import {useT} from 'web/lib/locale'

View File

@@ -5,7 +5,7 @@ import {Row} from 'web/components/layout/row'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {FilterFields} from 'common/filters'
import {useT} from 'web/lib/locale'
import {GENDERS_PLURAL} from "web/components/filters/choices";
import {GENDERS_PLURAL} from "common/choices";
export function PrefGenderFilterText(props: {
pref_gender: Gender[] | undefined

View File

@@ -4,7 +4,7 @@ import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-te
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {useT} from 'web/lib/locale'
import {RELATIONSHIP_CHOICES} from "web/components/filters/choices";
import {RELATIONSHIP_CHOICES} from "common/choices";
import {FilterFields} from "common/filters";
export function RelationshipFilterText(props: {

View File

@@ -3,7 +3,7 @@ import {convertRelationshipStatusTypes,} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {RELATIONSHIP_STATUS_CHOICES} from "web/components/filters/choices"
import {RELATIONSHIP_STATUS_CHOICES} from "common/choices"
import {FilterFields} from "common/filters"
import {getSortedOptions} from "common/util/sorting"
import {useT} from 'web/lib/locale'

View File

@@ -2,7 +2,7 @@ import clsx from 'clsx'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {FilterFields} from 'common/filters'
import {RELIGION_CHOICES} from 'web/components/filters/choices'
import {RELIGION_CHOICES} from 'common/choices'
import {convertReligionTypes} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {getSortedOptions} from 'common/util/sorting'

View File

@@ -3,7 +3,7 @@ import {convertRomanticTypes, RomanticType,} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
import {MultiCheckbox} from 'web/components/multi-checkbox'
import {ROMANTIC_CHOICES} from 'web/components/filters/choices'
import {ROMANTIC_CHOICES} from 'common/choices'
import {FilterFields} from 'common/filters'
import {useT} from 'web/lib/locale'
import {toKey} from "common/parsing";

View File

@@ -40,7 +40,7 @@ import {
RELATIONSHIP_STATUS_CHOICES,
RELIGION_CHOICES,
ROMANTIC_CHOICES,
} from 'web/components/filters/choices'
} from 'common/choices'
import toast from 'react-hot-toast'
import {db} from 'web/lib/supabase/db'
import {fetchChoices} from 'web/hooks/use-choices'

View File

@@ -10,8 +10,7 @@ import {
INVERTED_POLITICAL_CHOICES,
INVERTED_RELATIONSHIP_STATUS_CHOICES,
INVERTED_RELIGION_CHOICES,
RELATIONSHIP_ICONS,
} from 'web/components/filters/choices'
} from 'common/choices'
import {BiSolidDrink} from 'react-icons/bi'
import {BsPersonHeart, BsPersonVcard} from 'react-icons/bs'
import {FaChild} from 'react-icons/fa6'
@@ -30,12 +29,13 @@ import {UserActivity} from 'common/user'
import {ClockIcon} from '@heroicons/react/solid'
import {formatHeight, MeasurementSystem} from 'common/measurement-utils'
import {MAX_INT, MIN_INT} from 'common/constants'
import {GiFruitBowl} from 'react-icons/gi'
import {FaBriefcase, FaHandsHelping, FaHeart, FaStar} from 'react-icons/fa'
import {GiFruitBowl, GiRing} from 'react-icons/gi'
import {FaBriefcase, FaHandsHelping, FaHeart, FaStar, FaUsers} from 'react-icons/fa'
import {useLocale, useT} from 'web/lib/locale'
import {useChoices} from 'web/hooks/use-choices'
import {getSeekingGenderText} from 'web/lib/profile/seeking'
import {TbBulb, TbCheck, TbMoodSad, TbUsers} from 'react-icons/tb'
import {FiUser} from "react-icons/fi"
export function AboutRow(props: {
icon: ReactNode
@@ -538,3 +538,12 @@ const capitalizeAndRemoveUnderscores = (str: string) => {
const withSpaces = str.replace(/_/g, ' ')
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1)
}
export const RELATIONSHIP_ICONS = {
single: FiUser,
married: GiRing,
casual: FaHeart,
long_term: FaHeart,
open: FaUsers,
} as const

View File

@@ -77,6 +77,7 @@ function ButtonModal(props: {
onClose={() => {
refreshBookmarkedSearches()
}}
size={'lg'}
>
<Col className={MODAL_CLASS}>
<h3>{t('saved_searches.title', 'Saved Searches')}</h3>
@@ -89,34 +90,36 @@ function ButtonModal(props: {
)}
</p>
<Col
className={
'border-ink-300bg-canvas-0 inline-flex flex-col gap-2 rounded-md border p-1 shadow-sm'
}
className={clsx(
'divide-y divide-canvas-300 w-full pr-4',
SCROLLABLE_MODAL_CLASS
)}
>
<ol className="list-decimal list-inside space-y-2">
{(bookmarkedSearches || []).map((search) => (
<li
key={search.id}
className="items-center justify-between gap-2 list-item marker:text-ink-500 marker:font-bold"
>
{(bookmarkedSearches || []).map((search) => (
<Row
key={search.id}
className="items-center justify-between py-2 gap-2"
>
<div className="w-full rounded-md p-2">
{formatFilters(
search.search_filters as Partial<FilterFields>,
search.location as locationType,
choicesIdsToLabels,
measurementSystem
measurementSystem,
t
)?.join(' • ')}
<button
onClick={async () => {
await deleteBookmarkedSearch(search.id)
refreshBookmarkedSearches()
}}
className="inline-flex text-xl h-5 w-5 items-center justify-center rounded-full text-red-600 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400"
>
×
</button>
</li>
))}
</ol>
</div>
<button
onClick={async () => {
await deleteBookmarkedSearch(search.id)
refreshBookmarkedSearches()
}}
className="inline-flex items-center justify-center h-8 w-8 rounded-full text-red-600 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-400"
>
<XIcon className="h-6 w-6"/>
</button>
</Row>
))}
</Col>
</>
) : (

View File

@@ -1,7 +1,7 @@
import {convertRelationshipType, RelationshipType} from "web/lib/util/convert-types";
import stringOrStringArrayToText from "web/lib/util/string-or-string-array-to-text";
import {Profile} from "common/profiles/profile";
import {INVERTED_ROMANTIC_CHOICES} from "web/components/filters/choices";
import {INVERTED_ROMANTIC_CHOICES} from "common/choices";
export function getSeekingGenderText(profile: Profile, t: any) {
const relationshipTypes = profile.pref_relation_styles

View File

@@ -8,7 +8,7 @@ import {
INVERTED_RELATIONSHIP_STATUS_CHOICES,
INVERTED_RELIGION_CHOICES,
INVERTED_ROMANTIC_CHOICES
} from "web/components/filters/choices";
} from "common/choices";
export type RelationshipType = keyof typeof INVERTED_RELATIONSHIP_CHOICES

View File

@@ -1121,5 +1121,20 @@
"settings.measurement.metric": "Metrisch",
"font.atkinson": "Atkinson Hyperlegible",
"font.system-sans": "System Sans",
"font.classic-serif": "Klassische Serifenschrift"
"font.classic-serif": "Klassische Serifenschrift",
"filter.age.label": "Alter",
"filter.age.up_to": "bis zu",
"filter.any_new_users": "Jegliche neue Nutzer",
"filter.label.name": "Suche",
"filter.label.education_levels": "Bildung",
"filter.label.pref_age_max": "Max. Alter",
"filter.label.pref_age_min": "Min. Alter",
"filter.label.drinks": "Getränke",
"filter.label.wants_kids_strength": "Kinder",
"filter.label.pref_relation_styles": "Suche",
"filter.label.pref_gender": "Gesuchtes Geschlecht",
"filter.label.diet": "Ernährung",
"filter.label.political_beliefs": "Politische Ansichten",
"filter.label.mbti": "MBTI",
"filter.drinks.per_month": "pro Monat"
}

View File

@@ -1121,5 +1121,20 @@
"settings.measurement.metric": "Métrique",
"font.atkinson": "Atkinson Hyperlegible",
"font.system-sans": "Sans-serif système",
"font.classic-serif": "Serif classique"
"font.classic-serif": "Serif classique",
"filter.age.label": "Âge",
"filter.age.up_to": "jusqu'à",
"filter.any_new_users": "Tout nouvel utilisateur",
"filter.label.name": "Recherche",
"filter.label.education_levels": "Éducation",
"filter.label.pref_age_max": "Âge max",
"filter.label.pref_age_min": "Âge min",
"filter.label.drinks": "Boissons",
"filter.label.wants_kids_strength": "Enfants",
"filter.label.pref_relation_styles": "Recherche",
"filter.label.pref_gender": "Genre recherché",
"filter.label.diet": "Régime",
"filter.label.political_beliefs": "Opinions politiques",
"filter.label.mbti": "MBTI",
"filter.drinks.per_month": "par mois"
}