localize Explore Community

This commit is contained in:
v.lagerev 2026-04-14 01:59:41 +03:00
parent 3f4e07ce16
commit 456526315a
7 changed files with 157 additions and 46 deletions

View file

@ -507,5 +507,52 @@
"unverified_device": "Unverified Device",
"unverified_devices": "Unverified Devices"
},
"Explore": {
"explore_community": "Explore Community",
"add_server": "Add Server",
"add_server_desc": "Add server name to explore public communities.",
"server_name": "Server Name",
"failed_load_public_rooms": "Failed to load public rooms. Please try again.",
"view": "View",
"featured": "Featured",
"servers": "Servers",
"featured_by_client": "Featured by Client",
"featured_by_client_desc": "Public rooms and spaces hand-picked by this client.",
"featured_spaces": "Featured Spaces",
"featured_rooms": "Featured Rooms",
"no_featured": "No featured rooms or spaces yet.",
"search": "Search",
"search_placeholder": "Search for keyword",
"clear": "Clear",
"enter": "Enter",
"protocols": "Protocols",
"presets": "Presets",
"custom_limit": "Custom Limit",
"per_page_limit": "Per Page Item Limit",
"change_limit": "Change Limit",
"page_limit": "Page Limit: {{limit}}",
"results_for": "Results for \"{{term}}\"",
"popular_communities": "Popular Communities",
"filter_all": "All",
"filter_spaces": "Spaces",
"filter_rooms": "Rooms",
"previous_page": "Previous Page",
"next_page": "Next Page",
"no_communities": "No communities found!",
"space_badge": "Space",
"members_count": "{{count}} Members",
"join": "Join",
"joining": "Joining",
"retry": "Retry",
"join_error": "Join Error",
"join_error_unknown": "Failed to join. Unknown Error.",
"view_error": "View Error",
"cancel": "Cancel"
}
}

View file

@ -509,5 +509,52 @@
"unverified_device": "Неподтверждённое устройство",
"unverified_devices": "Неподтверждённые устройства"
},
"Explore": {
"explore_community": "Обзор сообществ",
"add_server": "Добавить сервер",
"add_server_desc": "Укажите имя сервера для обзора публичных сообществ.",
"server_name": "Имя сервера",
"failed_load_public_rooms": "Не удалось загрузить публичные комнаты. Попробуйте ещё раз.",
"view": "Открыть",
"featured": "Рекомендуемые",
"servers": "Серверы",
"featured_by_client": "Рекомендации клиента",
"featured_by_client_desc": "Подборка публичных комнат и пространств от этого клиента.",
"featured_spaces": "Рекомендуемые пространства",
"featured_rooms": "Рекомендуемые комнаты",
"no_featured": "Рекомендуемых комнат и пространств пока нет.",
"search": "Поиск",
"search_placeholder": "Поиск по ключевому слову",
"clear": "Очистить",
"enter": "Найти",
"protocols": "Протоколы",
"presets": "Готовые значения",
"custom_limit": "Своё значение",
"per_page_limit": "Элементов на странице",
"change_limit": "Применить",
"page_limit": "На странице: {{limit}}",
"results_for": "Результаты для «{{term}}»",
"popular_communities": "Популярные сообщества",
"filter_all": "Все",
"filter_spaces": "Пространства",
"filter_rooms": "Комнаты",
"previous_page": "Предыдущая",
"next_page": "Следующая",
"no_communities": "Сообщества не найдены!",
"space_badge": "Пространство",
"members_count": "{{count}} участников",
"join": "Присоединиться",
"joining": "Вступление…",
"retry": "Повторить",
"join_error": "Не удалось вступить",
"join_error_unknown": "Не удалось присоединиться. Неизвестная ошибка.",
"view_error": "Подробности",
"cancel": "Отмена"
}
}

View file

@ -19,6 +19,7 @@ import {
} from 'folds';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import * as css from './style.css';
import { RoomAvatar } from '../room-avatar';
import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix';
@ -94,6 +95,7 @@ function ErrorDialog({
message: string;
children: (openError: () => void) => ReactNode;
}) {
const { t } = useTranslation();
const [viewError, setViewError] = useState(false);
const closeError = () => setViewError(false);
const openError = () => setViewError(true);
@ -120,7 +122,7 @@ function ErrorDialog({
</Text>
</Box>
<Button size="400" variant="Secondary" fill="Soft" onClick={closeError}>
<Text size="B400">Cancel</Text>
<Text size="B400">{t('Explore.cancel')}</Text>
</Button>
</Box>
</Dialog>
@ -161,6 +163,7 @@ export const RoomCard = as<'div', RoomCardProps>(
},
ref
) => {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const joinedRoomId = useJoinedRoomId(allRooms, roomIdOrAlias);
@ -224,7 +227,7 @@ export const RoomCard = as<'div', RoomCardProps>(
</Avatar>
{(roomType === RoomType.Space || joinedRoom?.isSpaceRoom()) && (
<Badge variant="Secondary" fill="Soft" outlined>
<Text size="L400">Space</Text>
<Text size="L400">{t('Explore.space_badge')}</Text>
</Badge>
)}
</Box>
@ -252,7 +255,7 @@ export const RoomCard = as<'div', RoomCardProps>(
{typeof joinedMemberCount === 'number' && (
<Box gap="100">
<Icon size="50" src={Icons.User} />
<Text size="T200">{`${millify(joinedMemberCount)} Members`}</Text>
<Text size="T200">{t('Explore.members_count', { count: millify(joinedMemberCount) })}</Text>
</Box>
)}
{typeof joinedRoomId === 'string' && (
@ -263,7 +266,7 @@ export const RoomCard = as<'div', RoomCardProps>(
size="300"
>
<Text size="B300" truncate>
View
{t('Explore.view')}
</Text>
</Button>
)}
@ -276,7 +279,7 @@ export const RoomCard = as<'div', RoomCardProps>(
before={joining && <Spinner size="50" variant="Secondary" fill="Soft" />}
>
<Text size="B300" truncate>
{joining ? 'Joining' : 'Join'}
{joining ? t('Explore.joining') : t('Explore.join')}
</Text>
</Button>
)}
@ -290,12 +293,12 @@ export const RoomCard = as<'div', RoomCardProps>(
size="300"
>
<Text size="B300" truncate>
Retry
{t('Explore.retry')}
</Text>
</Button>
<ErrorDialog
title="Join Error"
message={joinState.error.message || 'Failed to join. Unknown Error.'}
title={t('Explore.join_error')}
message={joinState.error.message || t('Explore.join_error_unknown')}
>
{(openError) => (
<Button
@ -307,7 +310,7 @@ export const RoomCard = as<'div', RoomCardProps>(
size="300"
>
<Text size="B300" truncate>
View Error
{t('Explore.view_error')}
</Text>
</Button>
)}

View file

@ -1,6 +1,7 @@
import React, { FormEventHandler, useCallback, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import {
Avatar,
Box,
@ -39,6 +40,7 @@ import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page
import { stopPropagation } from '../../../utils/keyboard';
export function AddServer() {
const { t } = useTranslation();
const mx = useMatrixClient();
const navigate = useNavigate();
const [dialog, setDialog] = useState(false);
@ -94,7 +96,7 @@ export function AddServer() {
size="500"
>
<Box grow="Yes">
<Text size="H4">Add Server</Text>
<Text size="H4">{t('Explore.add_server')}</Text>
</Box>
<IconButton size="300" onClick={() => setDialog(false)} radii="300">
<Icon src={Icons.Cross} />
@ -107,13 +109,13 @@ export function AddServer() {
direction="Column"
gap="400"
>
<Text priority="400">Add server name to explore public communities.</Text>
<Text priority="400">{t('Explore.add_server_desc')}</Text>
<Box direction="Column" gap="100">
<Text size="L400">Server Name</Text>
<Text size="L400">{t('Explore.server_name')}</Text>
<Input ref={serverInputRef} name="serverInput" variant="Background" required />
{exploreState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to load public rooms. Please try again.
{t('Explore.failed_load_public_rooms')}
</Text>
)}
</Box>
@ -132,7 +134,7 @@ export function AddServer() {
</Button> */}
<Button type="submit" onClick={handleView} variant="Secondary" fill="Soft">
<Text size="B400">View</Text>
<Text size="B400">{t('Explore.view')}</Text>
</Button>
</Box>
</Box>
@ -148,7 +150,7 @@ export function AddServer() {
onClick={() => setDialog(true)}
>
<Text size="B300" truncate>
Add Server
{t('Explore.add_server')}
</Text>
</Button>
</>
@ -156,6 +158,7 @@ export function AddServer() {
}
export function Explore() {
const { t } = useTranslation();
const mx = useMatrixClient();
useNavToActivePathMapper('explore');
const userId = mx.getUserId();
@ -173,7 +176,7 @@ export function Explore() {
<Box grow="Yes" gap="300">
<Box grow="Yes">
<Text size="H4" truncate>
Explore Community
{t('Explore.explore_community')}
</Text>
</Box>
</Box>
@ -191,7 +194,7 @@ export function Explore() {
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Featured
{t('Explore.featured')}
</Text>
</Box>
</Box>
@ -229,7 +232,7 @@ export function Explore() {
<NavCategory>
<NavCategoryHeader>
<Text size="O400" style={{ paddingLeft: config.space.S200 }}>
Servers
{t('Explore.servers')}
</Text>
</NavCategoryHeader>
{servers.map((server) => (

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { RoomCard, RoomCardGrid } from '../../../components/room-card';
@ -20,6 +21,7 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
export function FeaturedRooms() {
const { t } = useTranslation();
const { featuredCommunities } = useClientConfig();
const { rooms, spaces } = featuredCommunities ?? {};
const allRooms = useAtomValue(allRoomsAtom);
@ -49,14 +51,14 @@ export function FeaturedRooms() {
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Bulb} />}
title="Featured by Client"
subTitle="Find and explore public rooms and spaces featured by client provider."
title={t('Explore.featured_by_client')}
subTitle={t('Explore.featured_by_client_desc')}
/>
</PageHeroSection>
<Box direction="Column" gap="700">
{spaces && spaces.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Spaces</Text>
<Text size="H4">{t('Explore.featured_spaces')}</Text>
<RoomCardGrid>
{spaces.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
@ -85,7 +87,7 @@ export function FeaturedRooms() {
)}
{rooms && rooms.length > 0 && (
<Box direction="Column" gap="400">
<Text size="H4">Featured Rooms</Text>
<Text size="H4">{t('Explore.featured_rooms')}</Text>
<RoomCardGrid>
{rooms.map((roomIdOrAlias) => (
<RoomSummaryLoader key={roomIdOrAlias} roomIdOrAlias={roomIdOrAlias}>
@ -123,7 +125,7 @@ export function FeaturedRooms() {
>
<Icon size="400" src={Icons.Info} />
<Text size="T300" align="Center">
No rooms or spaces featured by client provider.
{t('Explore.no_featured')}
</Text>
</Box>
)}

View file

@ -29,6 +29,7 @@ import {
} from 'folds';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { useQuery } from '@tanstack/react-query';
import { MatrixClient, Method, RoomType } from 'matrix-js-sdk';
@ -62,24 +63,26 @@ type RoomTypeFilter = {
title: string;
value: string | undefined;
};
const useRoomTypeFilters = (): RoomTypeFilter[] =>
useMemo(
const useRoomTypeFilters = (): RoomTypeFilter[] => {
const { t } = useTranslation();
return useMemo(
() => [
{
title: 'All',
title: t('Explore.filter_all'),
value: undefined,
},
{
title: 'Spaces',
title: t('Explore.filter_spaces'),
value: RoomType.Space,
},
{
title: 'Rooms',
title: t('Explore.filter_rooms'),
value: 'null',
},
],
[]
[t]
);
};
const FALLBACK_ROOMS_LIMIT = 24;
@ -91,6 +94,7 @@ type SearchProps = {
onReset: () => void;
};
function Search({ active, loading, searchInputRef, onSearch, onReset }: SearchProps) {
const { t } = useTranslation();
const handleSearchSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const { searchInput } = evt.target as HTMLFormElement & {
@ -106,14 +110,14 @@ function Search({ active, loading, searchInputRef, onSearch, onReset }: SearchPr
return (
<Box as="form" direction="Column" gap="100" onSubmit={handleSearchSubmit}>
<span data-spacing-node />
<Text size="L400">Search</Text>
<Text size="L400">{t('Explore.search')}</Text>
<Input
ref={searchInputRef}
style={{ paddingRight: config.space.S300 }}
name="searchInput"
size="500"
variant="Background"
placeholder="Search for keyword"
placeholder={t('Explore.search_placeholder')}
before={
active && loading ? (
<Spinner variant="Secondary" size="200" />
@ -132,11 +136,11 @@ function Search({ active, loading, searchInputRef, onSearch, onReset }: SearchPr
after={<Icon size="50" src={Icons.Cross} />}
onClick={onReset}
>
<Text size="B300">Clear</Text>
<Text size="B300">{t('Explore.clear')}</Text>
</Chip>
) : (
<Chip type="submit" variant="Primary" size="400" radii="Pill" outlined>
<Text size="B300">Enter</Text>
<Text size="B300">{t('Explore.enter')}</Text>
</Chip>
)
}
@ -153,6 +157,7 @@ function ThirdPartyProtocolsSelector({
instanceId?: string;
onChange: (instanceId?: string) => void;
}) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
@ -196,7 +201,7 @@ function ThirdPartyProtocolsSelector({
style={{ padding: config.space.S100, minWidth: toRem(100) }}
>
<Text style={{ padding: config.space.S100 }} size="L400" truncate>
Protocols
{t('Explore.protocols')}
</Text>
<Box direction="Column">
<MenuItem
@ -252,6 +257,7 @@ type LimitButtonProps = {
onLimitChange: (limit: string) => void;
};
function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
const { t } = useTranslation();
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const handleLimitSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
@ -288,7 +294,7 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
<Menu variant="Surface">
<Box direction="Column" gap="400" style={{ padding: config.space.S300 }}>
<Box direction="Column" gap="100">
<Text size="L400">Presets</Text>
<Text size="L400">{t('Explore.presets')}</Text>
<Box gap="100" wrap="Wrap">
<Chip variant="SurfaceVariant" onClick={() => setLimit('24')} radii="Pill">
<Text size="T200">24</Text>
@ -303,7 +309,7 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
</Box>
<Box as="form" onSubmit={handleLimitSubmit} direction="Column" gap="300">
<Box direction="Column" gap="100">
<Text size="L400">Custom Limit</Text>
<Text size="L400">{t('Explore.custom_limit')}</Text>
<Input
name="limitInput"
size="300"
@ -314,11 +320,11 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
outlined
type="number"
radii="400"
aria-label="Per Page Item Limit"
aria-label={t('Explore.per_page_limit')}
/>
</Box>
<Button type="submit" size="300" variant="Primary" radii="400">
<Text size="B300">Change Limit</Text>
<Text size="B300">{t('Explore.change_limit')}</Text>
</Button>
</Box>
</Box>
@ -334,13 +340,14 @@ function LimitButton({ limit, onLimitChange }: LimitButtonProps) {
variant="SurfaceVariant"
after={<Icon size="100" src={Icons.ChevronBottom} />}
>
<Text size="T200" truncate>{`Page Limit: ${limit}`}</Text>
<Text size="T200" truncate>{t('Explore.page_limit', { limit })}</Text>
</Chip>
</PopOut>
);
}
export function PublicRooms() {
const { t } = useTranslation();
const { server } = useParams();
const mx = useMatrixClient();
const userId = mx.getUserId();
@ -488,7 +495,7 @@ export function PublicRooms() {
<Box grow="No" justifyContent="Center" alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />}
<Text size="H3" truncate>
Search
{t('Explore.search')}
</Text>
</Box>
<Box grow="Yes" basis="No" />
@ -532,9 +539,9 @@ export function PublicRooms() {
<Box direction="Column" gap="400">
<Box direction="Column" gap="300">
{isSearch ? (
<Text size="H4">{`Results for "${serverSearchParams.term}"`}</Text>
<Text size="H4">{t('Explore.results_for', { term: serverSearchParams.term })}</Text>
) : (
<Text size="H4">Popular Communities</Text>
<Text size="H4">{t('Explore.popular_communities')}</Text>
)}
<Box gap="200">
{roomTypeFilters.map((filter) => (
@ -624,7 +631,7 @@ export function PublicRooms() {
disabled={!data.prev_batch}
>
<Text size="B300" truncate>
Previous Page
{t('Explore.previous_page')}
</Text>
</Button>
<Box data-spacing-node grow="Yes" />
@ -635,7 +642,7 @@ export function PublicRooms() {
disabled={!data.next_batch}
>
<Text size="B300" truncate>
Next Page
{t('Explore.next_page')}
</Text>
</Button>
</Box>
@ -651,7 +658,7 @@ export function PublicRooms() {
>
<Icon size="400" src={Icons.Info} />
<Text size="T300" align="Center">
No communities found!
{t('Explore.no_communities')}
</Text>
</Box>
))}

View file

@ -1,6 +1,7 @@
import React from 'react';
import { Icon, Icons } from 'folds';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
import { useExploreSelected } from '../../../hooks/router/useExploreSelected';
@ -17,6 +18,7 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
export function ExploreTab() {
const { t } = useTranslation();
const mx = useMatrixClient();
const screenSize = useScreenSizeContext();
const clientConfig = useClientConfig();
@ -52,7 +54,7 @@ export function ExploreTab() {
return (
<SidebarItem active={exploreSelected}>
<SidebarItemTooltip tooltip="Explore Community">
<SidebarItemTooltip tooltip={t('Explore.explore_community')}>
{(triggerRef) => (
<SidebarAvatar as="button" ref={triggerRef} outlined onClick={handleExploreClick}>
<Icon src={Icons.Explore} filled={exploreSelected} />