search localization

This commit is contained in:
v.lagerev 2026-04-14 00:09:24 +03:00
parent 335c82e2c0
commit 1d3a6b5f7c
10 changed files with 122 additions and 35 deletions

View file

@ -279,5 +279,36 @@
"events": "Events",
"total": "Total: {{count}}",
"add_new": "Add New"
},
"Search": {
"search": "Search",
"no_match_found": "No Match Found",
"no_rooms": "No Rooms",
"no_match_for_query": "No match found for \"{{query}}\".",
"no_rooms_to_display": "You do not have any rooms to display yet.",
"help_text": "Type <bold>#</bold> for rooms, <bold>@</bold> for DMs and <bold>*</bold> for spaces. Hotkey: <bold>{{hotkey}}</bold>",
"message_search": "Message Search",
"message_search_subtitle": "Find helpful messages in your community by searching with related keywords.",
"no_results_for": "No results found for <bold>\"{{term}}\"</bold>",
"results_for": "Results for \"{{term}}\"",
"search_for_keyword": "Search for keyword",
"clear": "Clear",
"enter": "Enter",
"sort_by": "Sort by",
"recent": "Recent",
"relevance": "Relevance",
"rooms": "Rooms",
"rooms_for_query": "Rooms for \"{{query}}\"",
"no_match_found_excl": "No match found!",
"save": "Save",
"save_count": "Save ({{count}})",
"deselect_all": "Deselect All",
"select_rooms": "Select Rooms",
"filter": "Filter",
"global": "Global",
"room_tombstone": "This room has been replaced.",
"event": "event",
"open": "Open",
"home": "Home"
}
}

View file

@ -279,5 +279,36 @@
"events": "События",
"total": "Всего: {{count}}",
"add_new": "Добавить"
},
"Search": {
"search": "Поиск",
"no_match_found": "Совпадений не найдено",
"no_rooms": "Нет комнат",
"no_match_for_query": "Совпадений для «{{query}}» не найдено.",
"no_rooms_to_display": "У вас пока нет комнат для отображения.",
"help_text": "Введите <bold>#</bold> для комнат, <bold>@</bold> для личных чатов и <bold>*</bold> для пространств. Горячая клавиша: <bold>{{hotkey}}</bold>",
"message_search": "Поиск сообщений",
"message_search_subtitle": "Находите нужные сообщения в сообществе по ключевым словам.",
"no_results_for": "Ничего не найдено по запросу <bold>«{{term}}»</bold>",
"results_for": "Результаты для «{{term}}»",
"search_for_keyword": "Поиск по ключевому слову",
"clear": "Очистить",
"enter": "Найти",
"sort_by": "Сортировка",
"recent": "По дате",
"relevance": "По релевантности",
"rooms": "Комнаты",
"rooms_for_query": "Комнаты для «{{query}}»",
"no_match_found_excl": "Совпадений не найдено!",
"save": "Сохранить",
"save_count": "Сохранить ({{count}})",
"deselect_all": "Снять выбор",
"select_rooms": "Выбрать комнаты",
"filter": "Фильтр",
"global": "Глобальный",
"room_tombstone": "Комната перенесена.",
"event": "событие",
"open": "Открыть",
"home": "Главная"
}
}

View file

@ -1,5 +1,6 @@
import React, { RefObject, useEffect, useMemo, useRef } from 'react';
import { Text, Box, Icon, Icons, config, Spinner, IconButton, Line, toRem } from 'folds';
import { Trans, useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useInfiniteQuery } from '@tanstack/react-query';
@ -50,6 +51,7 @@ export function MessageSearch({
senders,
scrollRef,
}: MessageSearchProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const mDirects = useAtomValue(mDirectAtom);
const allRooms = useRooms(mx, allRoomsAtom, mDirects);
@ -229,8 +231,8 @@ export function MessageSearch({
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Message} />}
title="Search Messages"
subTitle="Find helpful messages in your community by searching with related keywords."
title={t('Search.message_search')}
subTitle={t('Search.message_search_subtitle')}
/>
</PageHeroSection>
</PageHeroEmpty>
@ -245,7 +247,11 @@ export function MessageSearch({
>
<Icon size="200" src={Icons.Info} />
<Text>
No results found for <b>{`"${msgSearchParams.term}"`}</b>
<Trans
i18nKey="Search.no_results_for"
values={{ term: msgSearchParams.term }}
components={{ bold: <b /> }}
/>
</Text>
</Box>
)}
@ -262,7 +268,7 @@ export function MessageSearch({
{vItems.length > 0 && (
<Box direction="Column" gap="300">
<Box direction="Column" gap="200">
<Text size="H5">{`Results for "${msgSearchParams.term}"`}</Text>
<Text size="H5">{t('Search.results_for', { term: msgSearchParams.term })}</Text>
<Line size="300" variant="Surface" />
</Box>
<div

View file

@ -26,6 +26,7 @@ import {
RectCords,
} from 'folds';
import { SearchOrderBy } from 'matrix-js-sdk';
import { useTranslation } from 'react-i18next';
import FocusTrap from 'focus-trap-react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useMatrixClient } from '../../hooks/useMatrixClient';
@ -45,6 +46,7 @@ type OrderButtonProps = {
onChange: (order?: string) => void;
};
function OrderButton({ order, onChange }: OrderButtonProps) {
const { t } = useTranslation();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const rankOrder = order === SearchOrderBy.Rank;
@ -72,7 +74,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
>
<Menu variant="Surface">
<Header size="300" variant="Surface" style={{ padding: `0 ${config.space.S300}` }}>
<Text size="L400">Sort by</Text>
<Text size="L400">{t('Search.sort_by')}</Text>
</Header>
<Line variant="Surface" size="300" />
<div style={{ padding: config.space.S100 }}>
@ -83,7 +85,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
radii="300"
aria-pressed={!rankOrder}
>
<Text size="T300">Recent</Text>
<Text size="T300">{t('Search.recent')}</Text>
</MenuItem>
<MenuItem
onClick={() => setOrder(SearchOrderBy.Rank)}
@ -92,7 +94,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
radii="300"
aria-pressed={rankOrder}
>
<Text size="T300">Relevance</Text>
<Text size="T300">{t('Search.relevance')}</Text>
</MenuItem>
</div>
</Menu>
@ -105,7 +107,7 @@ function OrderButton({ order, onChange }: OrderButtonProps) {
after={<Icon size="50" src={Icons.Sort} />}
onClick={handleOpenMenu}
>
{rankOrder ? <Text size="T200">Relevance</Text> : <Text size="T200">Recent</Text>}
{rankOrder ? <Text size="T200">{t('Search.relevance')}</Text> : <Text size="T200">{t('Search.recent')}</Text>}
</Chip>
</PopOut>
);
@ -127,6 +129,7 @@ type SelectRoomButtonProps = {
onChange: (rooms?: string[]) => void;
};
function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButtonProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const scrollRef = useRef<HTMLDivElement>(null);
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
@ -215,7 +218,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
gap="100"
style={{ padding: config.space.S200, paddingBottom: 0 }}
>
<Text size="L400">Search</Text>
<Text size="L400">{t('Search.search')}</Text>
<Input
onChange={handleSearchChange}
size="300"
@ -238,11 +241,11 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
paddingRight: 0,
}}
>
{!searchResult && <Text size="L400">Rooms</Text>}
{searchResult && <Text size="L400">{`Rooms for "${searchResult.query}"`}</Text>}
{!searchResult && <Text size="L400">{t('Search.rooms')}</Text>}
{searchResult && <Text size="L400">{t('Search.rooms_for_query', { query: searchResult.query })}</Text>}
{searchResult && searchResult.items.length === 0 && (
<Text style={{ padding: config.space.S400 }} size="T300" align="Center">
No match found!
{t('Search.no_match_found_excl')}
</Text>
)}
<div
@ -292,9 +295,9 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
<Box shrink="No" direction="Column" gap="100" style={{ padding: config.space.S200 }}>
<Button size="300" variant="Secondary" radii="300" onClick={handleSave}>
{localSelected && localSelected.length > 0 ? (
<Text size="B300">Save ({localSelected.length})</Text>
<Text size="B300">{t('Search.save_count', { count: localSelected.length })}</Text>
) : (
<Text size="B300">Save</Text>
<Text size="B300">{t('Search.save')}</Text>
)}
</Button>
<Button
@ -305,7 +308,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
onClick={handleDeselectAll}
disabled={!localSelected || localSelected.length === 0}
>
<Text size="B300">Deselect All</Text>
<Text size="B300">{t('Search.deselect_all')}</Text>
</Button>
</Box>
</Box>
@ -319,7 +322,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
radii="Pill"
before={<Icon size="100" src={Icons.PlusCircle} />}
>
<Text size="T200">Select Rooms</Text>
<Text size="T200">{t('Search.select_rooms')}</Text>
</Chip>
</PopOut>
);
@ -347,11 +350,12 @@ export function SearchFilters({
onGlobalChange,
onOrderChange,
}: SearchFiltersProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
return (
<Box direction="Column" gap="100">
<Text size="L400">Filter</Text>
<Text size="L400">{t('Search.filter')}</Text>
<Box gap="200" wrap="Wrap">
<Chip
variant={!global ? 'Success' : 'Surface'}
@ -370,7 +374,7 @@ export function SearchFilters({
outlined
onClick={() => onGlobalChange(true)}
>
<Text size="T200">Global</Text>
<Text size="T200">{t('Search.global')}</Text>
</Chip>
)}
<Line

View file

@ -1,5 +1,6 @@
import React, { FormEventHandler, RefObject } from 'react';
import { Box, Text, Input, Icon, Icons, Spinner, Chip, config } from 'folds';
import { useTranslation } from 'react-i18next';
type SearchProps = {
active?: boolean;
@ -9,6 +10,7 @@ type SearchProps = {
onReset: () => void;
};
export function SearchInput({ active, loading, searchInputRef, onSearch, onReset }: SearchProps) {
const { t } = useTranslation();
const handleSearchSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const { searchInput } = evt.target as HTMLFormElement & {
@ -24,7 +26,7 @@ export function SearchInput({ active, loading, searchInputRef, onSearch, onReset
return (
<Box as="form" direction="Column" gap="100" onSubmit={handleSearchSubmit}>
<span data-spacing-node />
<Text size="L400">Search</Text>
<Text size="L400">{t('Search.search')}</Text>
<Input
ref={searchInputRef}
style={{ paddingRight: config.space.S300 }}
@ -32,7 +34,7 @@ export function SearchInput({ active, loading, searchInputRef, onSearch, onReset
autoFocus
size="500"
variant="Background"
placeholder="Search for keyword"
placeholder={t('Search.search_for_keyword')}
autoComplete="off"
before={
active && loading ? (
@ -53,11 +55,11 @@ export function SearchInput({ active, loading, searchInputRef, onSearch, onReset
after={<Icon size="50" src={Icons.Cross} />}
onClick={onReset}
>
<Text size="B300">Clear</Text>
<Text size="B300">{t('Search.clear')}</Text>
</Chip>
) : (
<Chip type="submit" variant="Primary" size="400" radii="Pill" outlined>
<Text size="B300">Enter</Text>
<Text size="B300">{t('Search.enter')}</Text>
</Chip>
)
}

View file

@ -3,6 +3,7 @@ import React, { MouseEventHandler, useMemo } from 'react';
import { IEventWithRoomId, JoinRule, RelationType, Room } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser';
import { Avatar, Box, Chip, Header, Icon, Icons, Text, config } from 'folds';
import { useTranslation } from 'react-i18next';
import { Opts as LinkifyOpts } from 'linkifyjs';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import {
@ -74,6 +75,7 @@ export function SearchResultGroup({
hour24Clock,
dateFormatString,
}: SearchResultGroupProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const highlightRegex = useMemo(() => makeHighlightRegex(highlights), [highlights]);
@ -165,7 +167,7 @@ export function SearchResultGroup({
return (
<Box grow="Yes" direction="Column">
<Text size="T400" priority="300">
Room Tombstone. {content.body}
{t('Search.room_tombstone')} {content.body}
</Text>
</Box>
);
@ -180,7 +182,7 @@ export function SearchResultGroup({
<Box grow="Yes" direction="Column">
<Text size="T400" priority="300">
<code className={customHtmlCss.Code}>{event.type}</code>
{' event'}
{` ${t('Search.event')}`}
</Text>
</Box>
);
@ -303,7 +305,7 @@ export function SearchResultGroup({
variant="Secondary"
radii="400"
>
<Text size="T200">Open</Text>
<Text size="T200">{t('Search.open')}</Text>
</Chip>
</Box>
</Box>

View file

@ -25,6 +25,7 @@ import React, {
useRef,
useState,
} from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { isKeyHotkey } from 'is-hotkey';
import { useAtom, useAtomValue } from 'jotai';
import { Room } from 'matrix-js-sdk';
@ -135,6 +136,7 @@ type SearchProps = {
requestClose: () => void;
};
export function Search({ requestClose }: SearchProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const scrollRef = useRef<HTMLDivElement>(null);
@ -269,7 +271,7 @@ export function Search({ requestClose }: SearchProps) {
variant="Background"
radii="400"
outlined
placeholder="Search"
placeholder={t('Search.search')}
before={<Icon size="200" src={Icons.Search} />}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
@ -286,12 +288,12 @@ export function Search({ requestClose }: SearchProps) {
gap="100"
>
<Text size="H6" align="Center">
{result ? 'No Match Found' : 'No Rooms'}
{result ? t('Search.no_match_found') : t('Search.no_rooms')}
</Text>
<Text size="T200" align="Center">
{result
? `No match found for "${result.query}".`
: `You do not have any Rooms to display yet.`}
? t('Search.no_match_for_query', { query: result.query })
: t('Search.no_rooms_to_display')}
</Text>
</Box>
)}
@ -409,8 +411,11 @@ export function Search({ requestClose }: SearchProps) {
<Line size="300" />
<Box shrink="No" justifyContent="Center" style={{ padding: config.space.S200 }}>
<Text size="T200" priority="300">
Type <b>#</b> for rooms, <b>@</b> for DMs and <b>*</b> for spaces. Hotkey:{' '}
<b>{isMacOS() ? KeySymbol.Command : 'Ctrl'} + k</b>
<Trans
i18nKey="Search.help_text"
values={{ hotkey: `${isMacOS() ? KeySymbol.Command : 'Ctrl'} + k` }}
components={{ bold: <b /> }}
/>
</Text>
</Box>
</Modal>

View file

@ -1,5 +1,6 @@
import React, { useRef } from 'react';
import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds';
import { useTranslation } from 'react-i18next';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { MessageSearch } from '../../../features/message-search';
import { useHomeRooms } from './useHomeRooms';
@ -7,6 +8,7 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function HomeSearch() {
const { t } = useTranslation();
const scrollRef = useRef<HTMLDivElement>(null);
const rooms = useHomeRooms();
const screenSize = useScreenSizeContext();
@ -29,7 +31,7 @@ export function HomeSearch() {
<Box justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
<Text size="H3" truncate>
Message Search
{t('Search.message_search')}
</Text>
</Box>
<Box grow="Yes" basis="No" />
@ -40,7 +42,7 @@ export function HomeSearch() {
<PageContent>
<PageContentCenter>
<MessageSearch
defaultRoomsFilterName="Home"
defaultRoomsFilterName={t('Search.home')}
allowGlobal
rooms={rooms}
scrollRef={scrollRef}

View file

@ -1,17 +1,19 @@
import React from 'react';
import { Icon, Icons } from 'folds';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
import { searchModalAtom } from '../../../state/searchModal';
export function SearchTab() {
const { t } = useTranslation();
const [opened, setOpen] = useAtom(searchModalAtom);
const open = () => setOpen(true);
return (
<SidebarItem active={opened}>
<SidebarItemTooltip tooltip="Search">
<SidebarItemTooltip tooltip={t('Search.search')}>
{(triggerRef) => (
<SidebarAvatar as="button" ref={triggerRef} outlined onClick={open}>
<Icon src={Icons.Search} filled={opened} />

View file

@ -1,5 +1,6 @@
import React, { useRef } from 'react';
import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page';
import { MessageSearch } from '../../../features/message-search';
@ -13,6 +14,7 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function SpaceSearch() {
const { t } = useTranslation();
const mx = useMatrixClient();
const scrollRef = useRef<HTMLDivElement>(null);
const space = useSpace();
@ -44,7 +46,7 @@ export function SpaceSearch() {
<Box justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
<Text size="H3" truncate>
Message Search
{t('Search.message_search')}
</Text>
</Box>
<Box grow="Yes" basis="No" />