diff --git a/packages/twenty-front/src/modules/ui/input/components/Select.tsx b/packages/twenty-front/src/modules/ui/input/components/Select.tsx index ed07fbc416..04d95249f6 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Select.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Select.tsx @@ -23,6 +23,7 @@ import { type IconComponent } from 'twenty-ui/icon'; import { type SelectOption } from 'twenty-ui/input'; import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation'; import { themeCssVariables } from 'twenty-ui/theme-constants'; +import { normalizeSearchText } from '~/utils/normalizeSearchText'; export type SelectSizeVariant = 'small' | 'default'; @@ -124,15 +125,20 @@ export const Select = ({ return null; }, [emptyOption, options, pinnedOption, value]); - const filteredOptions = useMemo( - () => - searchInputValue - ? options.filter(({ label }) => - label.toLowerCase().includes(searchInputValue.toLowerCase()), - ) - : options, - [options, searchInputValue], - ); + const filteredOptions = useMemo(() => { + if (!isNonEmptyString(searchInputValue)) { + return options; + } + + const normalizedSearch = normalizeSearchText(searchInputValue); + + return options.filter( + ({ label, searchKeywords }) => + normalizeSearchText(label).includes(normalizedSearch) || + (isDefined(searchKeywords) && + normalizeSearchText(searchKeywords).includes(normalizedSearch)), + ); + }, [options, searchInputValue]); const isDisabled = disabledFromProps || diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx index fa7cb26b73..9fa837ad69 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/LocalePicker.tsx @@ -10,6 +10,7 @@ import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidate import { useStore } from 'jotai'; import { useLingui } from '@lingui/react/macro'; import { enUS } from 'date-fns/locale'; +import { isNonEmptyString } from '@sniptt/guards'; import { APP_LOCALES } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import { dateLocaleState } from '~/localization/states/dateLocaleState'; @@ -17,6 +18,31 @@ import { themeCssVariables } from 'twenty-ui/theme-constants'; import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; import { logError } from '~/utils/logError'; +// Language name in English and in its own language, so it is searchable +// regardless of the UI language — e.g. typing "chinese" or "中文". +const getLocaleSearchKeywords = ( + locale: (typeof APP_LOCALES)[keyof typeof APP_LOCALES], +): string => { + const displayNames = [locale, APP_LOCALES.en].map((displayLocale) => { + try { + return new Intl.DisplayNames([displayLocale], { type: 'language' }).of( + locale, + ); + } catch { + return undefined; + } + }); + + return [...new Set(displayNames.filter(isNonEmptyString))].join(' '); +}; + +const LOCALE_SEARCH_KEYWORDS: Record = Object.fromEntries( + Object.values(APP_LOCALES).map((locale) => [ + locale, + getLocaleSearchKeywords(locale), + ]), +); + const StyledContainer = styled.div` display: flex; flex-direction: column; @@ -207,9 +233,12 @@ export const LocalePicker = () => { }); } - const localeOptions = [...unsortedLocaleOptions].sort((a, b) => - a.label.localeCompare(b.label), - ); + const localeOptions = [...unsortedLocaleOptions] + .sort((a, b) => a.label.localeCompare(b.label)) + .map((option) => ({ + ...option, + searchKeywords: LOCALE_SEARCH_KEYWORDS[option.value], + })); return ( @@ -217,6 +246,7 @@ export const LocalePicker = () => { dropdownId="preferred-locale" dropdownWidthAuto fullWidth + withSearchInput value={currentWorkspaceMember.locale} options={localeOptions} onChange={(value) => diff --git a/packages/twenty-ui/src/input/types/SelectOption.ts b/packages/twenty-ui/src/input/types/SelectOption.ts index 21b80a01a8..d43434868c 100644 --- a/packages/twenty-ui/src/input/types/SelectOption.ts +++ b/packages/twenty-ui/src/input/types/SelectOption.ts @@ -12,4 +12,5 @@ export type SelectOption< disabled?: boolean; color?: ThemeColor | 'transparent'; contextualText?: string; + searchKeywords?: string; };