feat(front): add search to the language picker (#22095)

## What

Adds a search bar to the **Settings → Experience → Language** picker,
and makes languages searchable across languages.

Each option is matched against:
- its displayed label (the name in the current UI language)
- its name **in English** — typing `chinese` finds "Chinois — Simplifié"
- its **native name** — typing `中文` finds the same option

Matching is also accent-insensitive (`francais` finds "Français").

## How

- The shared `Select` already supports search via `withSearchInput`
(used by the currency/country pickers) — the picker just opts in.
- Cross-language matching uses the platform `Intl.DisplayNames` API to
derive each language's English and native names — no hardcoded
translation tables, no extra requests.
- A generic optional `searchKeywords` field on `SelectOption` lets the
`Select` filter match synonyms on top of the label; the filter now runs
through the existing `normalizeSearchText`, hence the
accent-insensitivity. Behavior is unchanged for every existing `Select`
(strict superset for ASCII labels).

## Test

- `nx typecheck` / `nx lint` pass for `twenty-front` and `twenty-ui`.
- Open the Language dropdown and try `chinese`, `中文`, or `francais`.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22095?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Félix Malfait
2026-06-24 16:58:24 +02:00
committed by GitHub
parent dd9ad876a4
commit 7d3cd5ed00
3 changed files with 49 additions and 12 deletions
@@ -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 = <Value extends SelectValue>({
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 ||