localize Inbox/Notifications

This commit is contained in:
v.lagerev 2026-04-14 01:41:22 +03:00
parent 1495a4b01e
commit 3f4e07ce16
9 changed files with 190 additions and 54 deletions

View file

@ -451,5 +451,61 @@
"broken_message": "Broken message",
"empty_message": "Empty message",
"edited": " (edited)"
},
"Inbox": {
"inbox": "Inbox",
"invites": "Invites",
"notifications": "Notifications",
"notification_messages": "Notifications",
"filter": "Filter",
"all_notifications": "All Notifications",
"highlighted": "Highlighted",
"mark_as_read": "Mark as Read",
"open": "Open",
"no_notifications": "No Notifications",
"no_notifications_desc": "You don't have any new notifications to display yet.",
"scroll_to_top": "Scroll to Top",
"encrypted": "Encrypted",
"direct_message": "Direct Message",
"space": "Space",
"decline": "Decline",
"accept": "Accept",
"from": "From: ",
"reason_label": "Reason: ",
"primary": "Primary",
"public": "Public",
"spam": "Spam",
"no_invites": "No Invites",
"no_invites_known_desc": "When someone you share a room with sends you an invite, it'll show up here.",
"no_invites_unknown_desc": "Invites from people outside your rooms will appear here.",
"decline_all": "Decline All",
"spam_invites_count_one": "{{count}} Spam Invite",
"spam_invites_count_other": "{{count}} Spam Invites",
"spam_invites_desc": "Some of the following invites may contain harmful content or have been sent by banned users.",
"report_all": "Report All",
"block_all": "Block All",
"hide_all": "Hide All",
"view_all": "View All",
"no_spam_invites": "No Spam Invites",
"no_spam_invites_desc": "Invites detected as spam appear here.",
"invite_title": "Invite",
"user_id": "User ID",
"user_id_placeholder": "@username:server",
"reason_optional": "Reason (Optional)",
"invite_button": "Invite",
"notif_default": "Default",
"notif_all_messages": "All Messages",
"notif_mentions_keywords": "Mention & Keywords",
"notif_mute": "Mute",
"unverified_device": "Unverified Device",
"unverified_devices": "Unverified Devices"
}
}

View file

@ -451,5 +451,63 @@
"broken_message": "Повреждённое сообщение",
"empty_message": "Пустое сообщение",
"edited": " (изменено)"
},
"Inbox": {
"inbox": "Входящие",
"invites": "Приглашения",
"notifications": "Уведомления",
"notification_messages": "Уведомления",
"filter": "Фильтр",
"all_notifications": "Все уведомления",
"highlighted": "Выделенные",
"mark_as_read": "Отметить прочитанным",
"open": "Открыть",
"no_notifications": "Нет уведомлений",
"no_notifications_desc": "У вас пока нет новых уведомлений.",
"scroll_to_top": "Наверх",
"encrypted": "Зашифровано",
"direct_message": "Личное сообщение",
"space": "Пространство",
"decline": "Отклонить",
"accept": "Принять",
"from": "От: ",
"reason_label": "Причина: ",
"primary": "Основные",
"public": "Публичные",
"spam": "Спам",
"no_invites": "Нет приглашений",
"no_invites_known_desc": "Когда кто-то, с кем вы состоите в одной комнате, отправит вам приглашение, оно появится здесь.",
"no_invites_unknown_desc": "Приглашения от людей за пределами ваших комнат появятся здесь.",
"decline_all": "Отклонить все",
"spam_invites_count_one": "{{count}} спам-приглашение",
"spam_invites_count_few": "{{count}} спам-приглашения",
"spam_invites_count_many": "{{count}} спам-приглашений",
"spam_invites_count_other": "{{count}} спам-приглашений",
"spam_invites_desc": "Некоторые из этих приглашений могут содержать вредоносный контент или были отправлены забаненными пользователями.",
"report_all": "Пожаловаться на все",
"block_all": "Заблокировать всех",
"hide_all": "Скрыть все",
"view_all": "Показать все",
"no_spam_invites": "Нет спам-приглашений",
"no_spam_invites_desc": "Приглашения, распознанные как спам, появятся здесь.",
"invite_title": "Пригласить",
"user_id": "ID пользователя",
"user_id_placeholder": "@username:server",
"reason_optional": "Причина (необязательно)",
"invite_button": "Пригласить",
"notif_default": "По умолчанию",
"notif_all_messages": "Все сообщения",
"notif_mentions_keywords": "Упоминания и ключевые слова",
"notif_mute": "Без уведомлений",
"unverified_device": "Неподтверждённое устройство",
"unverified_devices": "Неподтверждённые устройства"
}
}

View file

@ -1,6 +1,7 @@
import { Box, config, Icon, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
import React, { MouseEventHandler, ReactNode, useMemo, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { stopPropagation } from '../utils/keyboard';
import {
getRoomNotificationModeIcon,
@ -20,16 +21,18 @@ const useRoomNotificationModes = (): RoomNotificationMode[] =>
[]
);
const useRoomNotificationModeStr = (): Record<RoomNotificationMode, string> =>
useMemo(
const useRoomNotificationModeStr = (): Record<RoomNotificationMode, string> => {
const { t } = useTranslation();
return useMemo(
() => ({
[RoomNotificationMode.Unset]: 'Default',
[RoomNotificationMode.AllMessages]: 'All Messages',
[RoomNotificationMode.SpecialMessages]: 'Mention & Keywords',
[RoomNotificationMode.Mute]: 'Mute',
[RoomNotificationMode.Unset]: t('Inbox.notif_default'),
[RoomNotificationMode.AllMessages]: t('Inbox.notif_all_messages'),
[RoomNotificationMode.SpecialMessages]: t('Inbox.notif_mentions_keywords'),
[RoomNotificationMode.Mute]: t('Inbox.notif_mute'),
}),
[]
[t]
);
};
type NotificationModeSwitcherProps = {
roomId: string;

View file

@ -32,6 +32,7 @@ import {
import { Room } from 'matrix-js-sdk';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { stopPropagation } from '../../utils/keyboard';
import { useDirectUsers } from '../../hooks/useDirectUsers';
import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix';
@ -56,6 +57,7 @@ type InviteUserProps = {
requestClose: () => void;
};
export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const alive = useAlive();
@ -169,7 +171,7 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
>
<Box grow="Yes">
<Text size="H4" truncate>
Invite
{t('Inbox.invite_title')}
</Text>
</Box>
<Box shrink="No">
@ -187,14 +189,14 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
gap="400"
>
<Box direction="Column" gap="100">
<Text size="L400">User ID</Text>
<Text size="L400">{t('Inbox.user_id')}</Text>
<div>
<Input
size="500"
ref={inputRef}
onChange={handleSearchChange}
onKeyDown={handleKeyDown}
placeholder="@username:server"
placeholder={t('Inbox.user_id_placeholder')}
name="userIdInput"
variant="Background"
disabled={inviting}
@ -260,7 +262,7 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
</div>
</Box>
<Box direction="Column" gap="100">
<Text size="L400">Reason (Optional)</Text>
<Text size="L400">{t('Inbox.reason_optional')}</Text>
<TextArea
size="500"
name="reasonInput"
@ -279,7 +281,7 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
disabled={!validUserId || inviting}
before={inviting && <Spinner size="200" variant="Primary" fill="Solid" />}
>
<Text size="B400">Invite</Text>
<Text size="B400">{t('Inbox.invite_button')}</Text>
</Button>
</Box>
</Box>

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Avatar, Box, Icon, Icons, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { NavCategory, NavItem, NavItemContent, NavLink } from '../../../components/nav';
import { getInboxInvitesPath, getInboxNotificationsPath } from '../../pathUtils';
@ -13,6 +14,7 @@ import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMappe
import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page';
function InvitesNavItem() {
const { t } = useTranslation();
const invitesSelected = useInboxInvitesSelected();
const allInvites = useAtomValue(allInvitesAtom);
const inviteCount = allInvites.length;
@ -32,7 +34,7 @@ function InvitesNavItem() {
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Invites
{t('Inbox.invites')}
</Text>
</Box>
{inviteCount > 0 && <UnreadBadge highlight count={inviteCount} />}
@ -44,6 +46,7 @@ function InvitesNavItem() {
}
export function Inbox() {
const { t } = useTranslation();
useNavToActivePathMapper('inbox');
const notificationsSelected = useInboxNotificationsSelected();
@ -53,7 +56,7 @@ export function Inbox() {
<Box grow="Yes" gap="300">
<Box grow="Yes">
<Text size="H4" truncate>
Inbox
{t('Inbox.inbox')}
</Text>
</Box>
</Box>
@ -71,7 +74,7 @@ export function Inbox() {
</Avatar>
<Box as="span" grow="Yes">
<Text as="span" size="Inherit" truncate>
Notifications
{t('Inbox.notifications')}
</Text>
</Box>
</Box>

View file

@ -18,6 +18,7 @@ import {
config,
} from 'folds';
import { useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
import { RoomTopicEventContent } from 'matrix-js-sdk/lib/types';
import FocusTrap from 'focus-trap-react';
import { MatrixClient, MatrixError, Room } from 'matrix-js-sdk';
@ -159,6 +160,7 @@ function InviteCard({
onNavigate,
hideAvatar,
}: InviteCardProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const userId = mx.getSafeUserId();
@ -200,21 +202,21 @@ function InviteCard({
{invite.isEncrypted && (
<Box shrink="No" alignItems="Center" justifyContent="Center">
<Badge variant="Success" fill="Solid" size="400" radii="300">
<Text size="L400">Encrypted</Text>
<Text size="L400">{t('Inbox.encrypted')}</Text>
</Badge>
</Box>
)}
{invite.isDirect && (
<Box shrink="No" alignItems="Center" justifyContent="Center">
<Badge variant="Primary" fill="Solid" size="400" radii="300">
<Text size="L400">Direct Message</Text>
<Text size="L400">{t('Inbox.direct_message')}</Text>
</Badge>
</Box>
)}
{invite.isSpace && (
<Box shrink="No" alignItems="Center" justifyContent="Center">
<Badge variant="Secondary" fill="Soft" size="400" radii="300">
<Text size="L400">Space</Text>
<Text size="L400">{t('Inbox.space')}</Text>
</Badge>
</Box>
)}
@ -290,7 +292,7 @@ function InviteCard({
disabled={joining || leaving}
before={leaving ? <Spinner variant="Secondary" size="100" /> : undefined}
>
<Text size="B300">Decline</Text>
<Text size="B300">{t('Inbox.decline')}</Text>
</Button>
<Button
onClick={join}
@ -302,7 +304,7 @@ function InviteCard({
disabled={joining || leaving}
before={joining ? <Spinner variant="Success" fill="Soft" size="100" /> : undefined}
>
<Text size="B300">Accept</Text>
<Text size="B300">{t('Inbox.accept')}</Text>
</Button>
</Box>
</Box>
@ -311,7 +313,7 @@ function InviteCard({
<Box gap="200" alignItems="Baseline">
<Box grow="Yes">
<Text size="T200" priority="300">
From: <b>{invite.senderId}</b>
{t('Inbox.from')}<b>{invite.senderId}</b>
</Text>
</Box>
{typeof invite.inviteTs === 'number' && invite.inviteTs !== 0 && (
@ -328,7 +330,7 @@ function InviteCard({
</Box>
{invite.reason && (
<Text size="T200" priority="300">
Reason: {invite.reason}
{t('Inbox.reason_label')}{invite.reason}
</Text>
)}
</Box>
@ -355,6 +357,7 @@ function InviteFilters({
unknownInvites,
spamInvites,
}: InviteFiltersProps) {
const { t } = useTranslation();
const isKnown = filter === InviteFilter.Known;
const isUnknown = filter === InviteFilter.Unknown;
const isSpam = filter === InviteFilter.Spam;
@ -375,7 +378,7 @@ function InviteFilters({
)
}
>
<Text size="T200">Primary</Text>
<Text size="T200">{t('Inbox.primary')}</Text>
</Chip>
<Chip
variant={isUnknown ? 'Warning' : 'Surface'}
@ -391,7 +394,7 @@ function InviteFilters({
)
}
>
<Text size="T200">Public</Text>
<Text size="T200">{t('Inbox.public')}</Text>
</Chip>
<Chip
variant={isSpam ? 'Critical' : 'Surface'}
@ -407,7 +410,7 @@ function InviteFilters({
)
}
>
<Text size="T200">Spam</Text>
<Text size="T200">{t('Inbox.spam')}</Text>
</Chip>
</Box>
);
@ -427,9 +430,10 @@ function KnownInvites({
hour24Clock,
dateFormatString,
}: KnownInvitesProps) {
const { t } = useTranslation();
return (
<Box direction="Column" gap="200">
<Text size="H4">Primary</Text>
<Text size="H4">{t('Inbox.primary')}</Text>
{invites.length > 0 ? (
<Box direction="Column" gap="100">
{invites.map((invite) => (
@ -449,8 +453,8 @@ function KnownInvites({
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Mail} />}
title="No Invites"
subTitle="When someone you share a room with sends you an invite, itll show up here."
title={t('Inbox.no_invites')}
subTitle={t('Inbox.no_invites_known_desc')}
/>
</PageHeroSection>
</PageHeroEmpty>
@ -473,6 +477,7 @@ function UnknownInvites({
hour24Clock,
dateFormatString,
}: UnknownInvitesProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [declineAllStatus, declineAll] = useAsyncCallback(
@ -488,7 +493,7 @@ function UnknownInvites({
return (
<Box direction="Column" gap="200">
<Box gap="200" justifyContent="SpaceBetween" alignItems="Center">
<Text size="H4">Public</Text>
<Text size="H4">{t('Inbox.public')}</Text>
<Box>
{invites.length > 0 && (
<Chip
@ -498,7 +503,7 @@ function UnknownInvites({
disabled={declining}
radii="Pill"
>
<Text size="T200">Decline All</Text>
<Text size="T200">{t('Inbox.decline_all')}</Text>
</Chip>
)}
</Box>
@ -522,8 +527,8 @@ function UnknownInvites({
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Info} />}
title="No Invites"
subTitle="Invites from people outside your rooms will appear here."
title={t('Inbox.no_invites')}
subTitle={t('Inbox.no_invites_unknown_desc')}
/>
</PageHeroSection>
</PageHeroEmpty>
@ -546,6 +551,7 @@ function SpamInvites({
hour24Clock,
dateFormatString,
}: SpamInvitesProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [showInvites, setShowInvites] = useState(false);
@ -585,7 +591,7 @@ function SpamInvites({
return (
<Box direction="Column" gap="200">
<Text size="H4">Spam</Text>
<Text size="H4">{t('Inbox.spam')}</Text>
{invites.length > 0 ? (
<Box direction="Column" gap="100">
<SequenceCard
@ -597,8 +603,8 @@ function SpamInvites({
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Warning} />}
title={`${invites.length} Spam Invites`}
subTitle="Some of the following invites may contain harmful content or have been sent by banned users."
title={t('Inbox.spam_invites_count', { count: invites.length })}
subTitle={t('Inbox.spam_invites_desc')}
>
<Box direction="Row" gap="200" justifyContent="Center" wrap="Wrap">
<Button
@ -611,7 +617,7 @@ function SpamInvites({
disabled={loading}
>
<Text size="B300" truncate>
Decline All
{t('Inbox.decline_all')}
</Text>
</Button>
{reportRoomSupported && reportAllStatus.status !== AsyncStatus.Success && (
@ -625,7 +631,7 @@ function SpamInvites({
disabled={loading}
>
<Text size="B300" truncate>
Report All
{t('Inbox.report_all')}
</Text>
</Button>
)}
@ -640,7 +646,7 @@ function SpamInvites({
before={blocking && <Spinner size="100" variant="Secondary" fill="Solid" />}
>
<Text size="B300" truncate>
Block All
{t('Inbox.block_all')}
</Text>
</Button>
)}
@ -658,7 +664,7 @@ function SpamInvites({
}
onClick={() => setShowInvites(!showInvites)}
>
<Text size="B300">{showInvites ? 'Hide All' : 'View All'}</Text>
<Text size="B300">{showInvites ? t('Inbox.hide_all') : t('Inbox.view_all')}</Text>
</Button>
</PageHero>
</PageHeroSection>
@ -681,8 +687,8 @@ function SpamInvites({
<PageHeroSection>
<PageHero
icon={<Icon size="600" src={Icons.Warning} />}
title="No Spam Invites"
subTitle="Invites detected as spam appear here."
title={t('Inbox.no_spam_invites')}
subTitle={t('Inbox.no_spam_invites_desc')}
/>
</PageHeroSection>
</PageHeroEmpty>
@ -692,6 +698,7 @@ function SpamInvites({
}
export function Invites() {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const { navigateRoom, navigateSpace } = useRoomNavigate();
@ -763,7 +770,7 @@ export function Invites() {
<Box alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />}
<Text size="H3" truncate>
Invites
{t('Inbox.invites')}
</Text>
</Box>
<Box grow="Yes" basis="No" />
@ -776,7 +783,7 @@ export function Invites() {
<Box ref={containerRef} direction="Column" gap="600">
<Box direction="Column" gap="100">
<span data-spacing-node />
<Text size="L400">Filter</Text>
<Text size="L400">{t('Inbox.filter')}</Text>
<InviteFilters
filter={filter}
onFilter={setFilter}

View file

@ -14,6 +14,7 @@ import {
toRem,
} from 'folds';
import { useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
INotification,
INotificationsResponse,
@ -222,6 +223,7 @@ function RoomNotificationsGroupComp({
hour24Clock,
dateFormatString,
}: RoomNotificationsGroupProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
@ -438,7 +440,7 @@ function RoomNotificationsGroupComp({
onClick={handleMarkAsRead}
before={<Icon size="100" src={Icons.CheckTwice} />}
>
<Text size="T200">Mark as Read</Text>
<Text size="T200">{t('Inbox.mark_as_read')}</Text>
</Chip>
)}
</Box>
@ -524,7 +526,7 @@ function RoomNotificationsGroupComp({
variant="Secondary"
radii="400"
>
<Text size="T200">Open</Text>
<Text size="T200">{t('Inbox.open')}</Text>
</Chip>
</Box>
</Box>
@ -562,6 +564,7 @@ const useNotificationsSearchParams = (
const DEFAULT_REFRESH_MS = 7000;
export function Notifications() {
const { t } = useTranslation();
const mx = useMatrixClient();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
@ -652,7 +655,7 @@ export function Notifications() {
<Box alignItems="Center" gap="200">
{screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Message} />}
<Text size="H3" truncate>
Notification Messages
{t('Inbox.notification_messages')}
</Text>
</Box>
<Box grow="Yes" basis="No" />
@ -666,7 +669,7 @@ export function Notifications() {
<Box direction="Column" gap="200">
<Box ref={scrollTopAnchorRef} direction="Column" gap="100">
<span data-spacing-node />
<Text size="L400">Filter</Text>
<Text size="L400">{t('Inbox.filter')}</Text>
<Box gap="200">
<Chip
onClick={() => setOnlyHighlighted(false)}
@ -675,7 +678,7 @@ export function Notifications() {
before={!onlyHighlight && <Icon size="100" src={Icons.Check} />}
outlined
>
<Text size="T200">All Notifications</Text>
<Text size="T200">{t('Inbox.all_notifications')}</Text>
</Chip>
<Chip
onClick={() => setOnlyHighlighted(true)}
@ -684,7 +687,7 @@ export function Notifications() {
before={onlyHighlight && <Icon size="100" src={Icons.Check} />}
outlined
>
<Text size="T200">Highlighted</Text>
<Text size="T200">{t('Inbox.highlighted')}</Text>
</Chip>
</Box>
</Box>
@ -699,7 +702,7 @@ export function Notifications() {
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
aria-label={t('Inbox.scroll_to_top')}
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
@ -752,9 +755,9 @@ export function Notifications() {
direction="Column"
gap="200"
>
<Text>No Notifications</Text>
<Text>{t('Inbox.no_notifications')}</Text>
<Text size="T200">
You don&apos;t have any new notifications to display yet.
{t('Inbox.no_notifications_desc')}
</Text>
</Box>
)}

View file

@ -1,6 +1,7 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Icon, Icons } from 'folds';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import {
SidebarAvatar,
@ -21,6 +22,7 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { useNavToActivePathAtom } from '../../../state/hooks/navToActivePath';
export function InboxTab() {
const { t } = useTranslation();
const screenSize = useScreenSizeContext();
const navigate = useNavigate();
const navToActivePath = useAtomValue(useNavToActivePathAtom());
@ -45,7 +47,7 @@ export function InboxTab() {
return (
<SidebarItem active={inboxSelected}>
<SidebarItemTooltip tooltip="Inbox">
<SidebarItemTooltip tooltip={t('Inbox.inbox')}>
{(triggerRef) => (
<SidebarAvatar as="button" ref={triggerRef} outlined onClick={handleInboxClick}>
<Icon src={Icons.Inbox} filled={inboxSelected} />

View file

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Badge, color, Icon, Icons, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import {
SidebarAvatar,
SidebarItem,
@ -19,6 +20,7 @@ import { Modal500 } from '../../../components/Modal500';
import { Settings, SettingsPages } from '../../../features/settings';
function UnverifiedIndicator() {
const { t } = useTranslation();
const mx = useMatrixClient();
const crypto = mx.getCrypto();
@ -49,7 +51,7 @@ function UnverifiedIndicator() {
<>
{hasUnverified && (
<SidebarItem active={settings} className={css.UnverifiedTab}>
<SidebarItemTooltip tooltip={unverified ? 'Unverified Device' : 'Unverified Devices'}>
<SidebarItemTooltip tooltip={unverified ? t('Inbox.unverified_device') : t('Inbox.unverified_devices')}>
{(triggerRef) => (
<SidebarAvatar
className={unverified ? css.UnverifiedAvatar : css.UnverifiedOtherAvatar}