localize Add Space

This commit is contained in:
heaven 2026-04-14 02:18:51 +03:00
parent f4e94a48cc
commit d120a9a933
13 changed files with 177 additions and 48 deletions

View file

@ -554,5 +554,58 @@
"join_error_unknown": "Failed to join. Unknown Error.",
"view_error": "View Error",
"cancel": "Cancel"
},
"Create": {
"add_space": "Add Space",
"create_space": "Create Space",
"create_space_desc": "Build a space for your community.",
"join_with_address": "Join with Address",
"join_with_address_desc": "Join an existing community.",
"new_space": "New Space",
"access": "Access",
"name": "Name",
"topic_optional": "Topic (Optional)",
"options": "Options",
"advanced_options": "Advanced Options",
"knock_to_join": "Knock to Join",
"knock_to_join_desc": "Anyone can send a request to join this space.",
"allow_federation": "Allow Federation",
"allow_federation_desc": "Users from other servers can join.",
"create": "Create",
"rate_limited": "Server rate-limited your request for {{minutes}} minutes!",
"access_restricted": "Restricted",
"access_restricted_desc": "Only members of the parent space can join.",
"access_private": "Private",
"access_private_desc": "Only people with an invite can join.",
"access_public": "Public",
"access_public_desc": "Anyone with the address can join.",
"address_optional": "Address (Optional)",
"address_hint": "Pick a unique address to make it discoverable.",
"address_taken": "This address is already taken. Please choose a different one.",
"founders": "Founders",
"founders_desc": "Privileged users assigned during creation. They have elevated control and can only be changed during an upgrade.",
"enter": "Enter",
"no_suggestions": "No Suggestions",
"no_suggestions_desc": "Enter a user ID and press Enter.",
"version": "Version",
"versions": "Versions",
"chat_room": "Chat Room",
"chat_room_desc": "Messages, photos, and videos.",
"voice_room": "Voice Room",
"voice_room_desc": "Live audio and video conversations.",
"new_chat_room": "New Chat Room",
"new_voice_room": "New Voice Room",
"existing_space": "Existing Space",
"add_room": "Add Room",
"existing_room": "Existing Room"
}
}

View file

@ -556,5 +556,58 @@
"join_error_unknown": "Не удалось присоединиться. Неизвестная ошибка.",
"view_error": "Подробности",
"cancel": "Отмена"
},
"Create": {
"add_space": "Добавить пространство",
"create_space": "Создать пространство",
"create_space_desc": "Создайте пространство для вашего сообщества.",
"join_with_address": "Присоединиться по адресу",
"join_with_address_desc": "Присоединиться к существующему сообществу.",
"new_space": "Новое пространство",
"access": "Доступ",
"name": "Название",
"topic_optional": "Тема (необязательно)",
"options": "Параметры",
"advanced_options": "Дополнительные параметры",
"knock_to_join": "Запрос на вступление",
"knock_to_join_desc": "Любой может отправить запрос на вступление в это пространство.",
"allow_federation": "Разрешить федерацию",
"allow_federation_desc": "Пользователи с других серверов смогут присоединиться.",
"create": "Создать",
"rate_limited": "Сервер ограничил ваш запрос на {{minutes}} мин.!",
"access_restricted": "Ограниченный",
"access_restricted_desc": "Могут присоединиться только участники родительского пространства.",
"access_private": "Приватный",
"access_private_desc": "Могут присоединиться только приглашённые.",
"access_public": "Публичный",
"access_public_desc": "Любой, у кого есть адрес, может присоединиться.",
"address_optional": "Адрес (необязательно)",
"address_hint": "Выберите уникальный адрес, чтобы пространство можно было найти.",
"address_taken": "Этот адрес уже занят. Выберите другой.",
"founders": "Основатели",
"founders_desc": "Привилегированные пользователи, назначенные при создании. Они имеют расширенные полномочия; изменить их можно только при обновлении пространства.",
"enter": "Добавить",
"no_suggestions": "Нет предложений",
"no_suggestions_desc": "Введите ID пользователя и нажмите Добавить.",
"version": "Версия",
"versions": "Версии",
"chat_room": "Чат-комната",
"chat_room_desc": "Сообщения, фото и видео.",
"voice_room": "Голосовая комната",
"voice_room_desc": "Голосовые и видеозвонки в реальном времени.",
"new_chat_room": "Новая чат-комната",
"new_voice_room": "Новая голосовая комната",
"existing_space": "Существующее пространство",
"add_room": "Добавить комнату",
"existing_room": "Существующая комната"
}
}

View file

@ -17,6 +17,7 @@ import {
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import React, {
ChangeEventHandler,
KeyboardEventHandler,
@ -83,6 +84,7 @@ export function AdditionalCreatorInput({
onRemove,
disabled,
}: AdditionalCreatorInputProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const [menuCords, setMenuCords] = useState<RectCords>();
const directUsers = useDirectUsers();
@ -150,8 +152,8 @@ export function AdditionalCreatorInput({
return (
<SettingTile
title="Founders"
description="Special privileged users can be assigned during creation. These users have elevated control and can only be modified during a upgrade."
title={t('Create.founders')}
description={t('Create.founders_desc')}
>
<Box shrink="No" direction="Column" gap="100">
<Box gap="200" wrap="Wrap">
@ -213,7 +215,7 @@ export function AdditionalCreatorInput({
onClick={handleEnterClick}
disabled={!validUserId}
>
<Text size="B400">Enter</Text>
<Text size="B400">{t('Create.enter')}</Text>
</Button>
</Box>
<Line size="300" />
@ -263,10 +265,10 @@ export function AdditionalCreatorInput({
gap="100"
>
<Text size="H6" align="Center">
No Suggestions
{t('Create.no_suggestions')}
</Text>
<Text size="T200" align="Center">
Please provide the user ID and hit Enter.
{t('Create.no_suggestions_desc')}
</Text>
</Box>
)}

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { useTranslation } from 'react-i18next';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
import { CreateRoomAccess } from './types';
@ -18,6 +19,7 @@ export function CreateRoomAccessSelector({
disabled,
getIcon,
}: CreateRoomAccessSelectorProps) {
const { t } = useTranslation();
return (
<Box shrink="No" direction="Column" gap="100">
{canRestrict && (
@ -36,9 +38,9 @@ export function CreateRoomAccessSelector({
before={<Icon size="400" src={getIcon(CreateRoomAccess.Restricted)} />}
after={value === CreateRoomAccess.Restricted && <Icon src={Icons.Check} />}
>
<Text size="H6">Restricted</Text>
<Text size="H6">{t('Create.access_restricted')}</Text>
<Text size="T300" priority="300">
Only member of parent space can join.
{t('Create.access_restricted_desc')}
</Text>
</SettingTile>
</SequenceCard>
@ -58,9 +60,9 @@ export function CreateRoomAccessSelector({
before={<Icon size="400" src={getIcon(CreateRoomAccess.Private)} />}
after={value === CreateRoomAccess.Private && <Icon src={Icons.Check} />}
>
<Text size="H6">Private</Text>
<Text size="H6">{t('Create.access_private')}</Text>
<Text size="T300" priority="300">
Only people with invite can join.
{t('Create.access_private_desc')}
</Text>
</SettingTile>
</SequenceCard>
@ -79,9 +81,9 @@ export function CreateRoomAccessSelector({
before={<Icon size="400" src={getIcon(CreateRoomAccess.Public)} />}
after={value === CreateRoomAccess.Public && <Icon src={Icons.Check} />}
>
<Text size="H6">Public</Text>
<Text size="H6">{t('Create.access_public')}</Text>
<Text size="T300" priority="300">
Anyone with the address can join.
{t('Create.access_public_desc')}
</Text>
</SettingTile>
</SequenceCard>

View file

@ -9,6 +9,7 @@ import React, {
import { MatrixError } from 'matrix-js-sdk';
import { Box, color, Icon, Icons, Input, Spinner, Text, toRem } from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next';
import { getMxIdServer } from '../../utils/matrix';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { replaceSpaceWithDash } from '../../utils/common';
@ -16,6 +17,7 @@ import { AsyncState, AsyncStatus, useAsync } from '../../hooks/useAsyncCallback'
import { useDebounce } from '../../hooks/useDebounce';
export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
const { t } = useTranslation();
const mx = useMatrixClient();
const aliasInputRef = useRef<HTMLInputElement>(null);
const [aliasAvail, setAliasAvail] = useState<AsyncState<boolean, Error>>({
@ -78,9 +80,9 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
return (
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Address (Optional)</Text>
<Text size="L400">{t('Create.address_optional')}</Text>
<Text size="T200" priority="300">
Pick an unique address to make it discoverable.
{t('Create.address_hint')}
</Text>
<Input
ref={aliasInputRef}
@ -109,7 +111,7 @@ export function CreateRoomAliasInput({ disabled }: { disabled?: boolean }) {
<Box style={{ color: color.Critical.Main }} alignItems="Center" gap="100">
<Icon src={Icons.Warning} filled size="50" />
<Text size="T200">
<b>This address is already taken. Please select a different one.</b>
<b>{t('Create.address_taken')}</b>
</Text>
</Box>
)}

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Text, Icon, Icons, config, IconSrc } from 'folds';
import { useTranslation } from 'react-i18next';
import { SequenceCard } from '../sequence-card';
import { SettingTile } from '../setting-tile';
import { CreateRoomType } from './types';
@ -17,6 +18,7 @@ export function CreateRoomTypeSelector({
disabled,
getIcon,
}: CreateRoomTypeSelectorProps) {
const { t } = useTranslation();
return (
<Box shrink="No" direction="Column" gap="100">
<SequenceCard
@ -36,10 +38,10 @@ export function CreateRoomTypeSelector({
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Chat Room
{t('Create.chat_room')}
</Text>
<Text size="T300" priority="300" truncate>
- Messages, photos, and videos.
- {t('Create.chat_room_desc')}
</Text>
</Box>
</SettingTile>
@ -61,10 +63,10 @@ export function CreateRoomTypeSelector({
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Voice Room
{t('Create.voice_room')}
</Text>
<Text size="T300" priority="300" truncate>
- Live audio and video conversations.
- {t('Create.voice_room_desc')}
</Text>
<BetaNoticeBadge />
</Box>

View file

@ -1,4 +1,5 @@
import React, { MouseEventHandler, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Button,
@ -28,6 +29,7 @@ export function RoomVersionSelector({
onChange: (value: string) => void;
disabled?: boolean;
}) {
const { t } = useTranslation();
const [menuCords, setMenuCords] = useState<RectCords>();
const handleMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
@ -47,7 +49,7 @@ export function RoomVersionSelector({
gap="500"
>
<SettingTile
title="Version"
title={t('Create.version')}
after={
<PopOut
anchor={menuCords}
@ -73,7 +75,7 @@ export function RoomVersionSelector({
gap="200"
style={{ padding: config.space.S200, maxWidth: toRem(300) }}
>
<Text size="L400">Versions</Text>
<Text size="L400">{t('Create.versions')}</Text>
<Box wrap="Wrap" gap="100">
{versions.map((version) => (
<Chip

View file

@ -14,6 +14,7 @@ import {
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { SpaceProvider } from '../../hooks/useSpace';
import { CreateRoomForm } from './CreateRoom';
@ -29,6 +30,7 @@ type CreateRoomModalProps = {
state: CreateRoomModalState;
};
function CreateRoomModal({ state }: CreateRoomModalProps) {
const { t } = useTranslation();
const { spaceId, type } = state;
const closeDialog = useCloseCreateRoomModal();
@ -59,7 +61,7 @@ function CreateRoomModal({ state }: CreateRoomModalProps) {
>
<Box grow="Yes">
<Text size="H4">
{type === CreateRoomType.VoiceRoom ? 'New Voice Room' : 'New Chat Room'}
{type === CreateRoomType.VoiceRoom ? t('Create.new_voice_room') : t('Create.new_chat_room')}
</Text>
</Box>
<Box shrink="No">

View file

@ -14,6 +14,7 @@ import {
Text,
TextArea,
} from 'folds';
import { useTranslation } from 'react-i18next';
import { SettingTile } from '../../components/setting-tile';
import { SequenceCard } from '../../components/sequence-card';
import {
@ -52,6 +53,7 @@ type CreateSpaceFormProps = {
onCreate?: (roomId: string) => void;
};
export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceFormProps) {
const { t } = useTranslation();
const mx = useMatrixClient();
const alive = useAlive();
@ -138,7 +140,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
return (
<Box as="form" onSubmit={handleSubmit} grow="Yes" direction="Column" gap="500">
<Box direction="Column" gap="100">
<Text size="L400">Access</Text>
<Text size="L400">{t('Create.access')}</Text>
<CreateRoomAccessSelector
value={access}
onSelect={setAccess}
@ -148,7 +150,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
/>
</Box>
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Name</Text>
<Text size="L400">{t('Create.name')}</Text>
<Input
required
before={<Icon size="100" src={getCreateSpaceAccessToIcon(access)} />}
@ -162,7 +164,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
/>
</Box>
<Box shrink="No" direction="Column" gap="100">
<Text size="L400">Topic (Optional)</Text>
<Text size="L400">{t('Create.topic_optional')}</Text>
<TextArea
name="topicTextAria"
size="500"
@ -176,7 +178,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
<Box shrink="No" direction="Column" gap="100">
<Box gap="200" alignItems="End">
<Text size="L400">Options</Text>
<Text size="L400">{t('Create.options')}</Text>
<Box grow="Yes" justifyContent="End">
<Chip
radii="Pill"
@ -184,7 +186,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
onClick={() => setAdvance(!advance)}
type="button"
>
<Text size="T200">Advanced Options</Text>
<Text size="T200">{t('Create.advanced_options')}</Text>
</Chip>
</Box>
</Box>
@ -210,8 +212,8 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
gap="500"
>
<SettingTile
title="Knock to Join"
description="Anyone can send request to join this space."
title={t('Create.knock_to_join')}
description={t('Create.knock_to_join_desc')}
after={
<Switch variant="Primary" value={knock} onChange={setKnock} disabled={disabled} />
}
@ -226,8 +228,8 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
gap="500"
>
<SettingTile
title="Allow Federation"
description="Users from other servers can join."
title={t('Create.allow_federation')}
description={t('Create.allow_federation_desc')}
after={
<Switch
variant="Primary"
@ -254,9 +256,9 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
<Text size="T300" style={{ color: color.Critical.Main }}>
<b>
{error instanceof MatrixError && error.name === ErrorCode.M_LIMIT_EXCEEDED
? `Server rate-limited your request for ${millisecondsToMinutes(
? t('Create.rate_limited', { minutes: millisecondsToMinutes(
(error.data.retry_after_ms as number | undefined) ?? 0
)} minutes!`
) })
: error.message}
</b>
</Text>
@ -271,7 +273,7 @@ export function CreateSpaceForm({ defaultAccess, space, onCreate }: CreateSpaceF
disabled={disabled}
before={loading && <Spinner variant="Primary" fill="Solid" size="200" />}
>
<Text size="B500">Create</Text>
<Text size="B500">{t('Create.create')}</Text>
</Button>
</Box>
</Box>

View file

@ -14,6 +14,7 @@ import {
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import { useAllJoinedRoomsSet, useGetRoom } from '../../hooks/useGetRoom';
import { SpaceProvider } from '../../hooks/useSpace';
import { CreateSpaceForm } from './CreateSpace';
@ -28,6 +29,7 @@ type CreateSpaceModalProps = {
state: CreateSpaceModalState;
};
function CreateSpaceModal({ state }: CreateSpaceModalProps) {
const { t } = useTranslation();
const { spaceId } = state;
const closeDialog = useCloseCreateSpaceModal();
@ -58,7 +60,7 @@ function CreateSpaceModal({ state }: CreateSpaceModalProps) {
}}
>
<Box grow="Yes">
<Text size="H4">New Space</Text>
<Text size="H4">{t('Create.new_space')}</Text>
</Box>
<Box shrink="No">
<IconButton size="300" radii="300" onClick={closeDialog}>

View file

@ -17,6 +17,7 @@ import {
config,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { MatrixError, Room } from 'matrix-js-sdk';
import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
@ -243,6 +244,7 @@ function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileP
}
function AddRoomButton({ item }: { item: HierarchyItem }) {
const { t } = useTranslation();
const [cords, setCords] = useState<RectCords>();
const openCreateRoomModal = useOpenCreateRoomModal();
const [addExisting, setAddExisting] = useState(false);
@ -285,7 +287,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
fill="None"
onClick={() => handleCreateRoom(CreateRoomType.TextRoom)}
>
<Text size="T300">Chat Room</Text>
<Text size="T300">{t('Create.chat_room')}</Text>
</MenuItem>
<MenuItem
size="300"
@ -295,10 +297,10 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
onClick={() => handleCreateRoom(CreateRoomType.VoiceRoom)}
after={<BetaNoticeBadge />}
>
<Text size="T300">Voice Room</Text>
<Text size="T300">{t('Create.voice_room')}</Text>
</MenuItem>
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
<Text size="T300">Existing Room</Text>
<Text size="T300">{t('Create.existing_room')}</Text>
</MenuItem>
</Menu>
</FocusTrap>
@ -311,7 +313,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
onClick={handleAddRoom}
aria-pressed={!!cords}
>
<Text size="B300">Add Room</Text>
<Text size="B300">{t('Create.add_room')}</Text>
</Chip>
{addExisting && (
<AddExistingModal parentId={item.roomId} requestClose={() => setAddExisting(false)} />
@ -321,6 +323,7 @@ function AddRoomButton({ item }: { item: HierarchyItem }) {
}
function AddSpaceButton({ item }: { item: HierarchyItem }) {
const { t } = useTranslation();
const [cords, setCords] = useState<RectCords>();
const openCreateSpaceModal = useOpenCreateSpaceModal();
const [addExisting, setAddExisting] = useState(false);
@ -362,10 +365,10 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
fill="None"
onClick={handleCreateSpace}
>
<Text size="T300">New Space</Text>
<Text size="T300">{t('Create.new_space')}</Text>
</MenuItem>
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
<Text size="T300">Existing Space</Text>
<Text size="T300">{t('Create.existing_space')}</Text>
</MenuItem>
</Menu>
</FocusTrap>
@ -378,7 +381,7 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
onClick={handleAddSpace}
aria-pressed={!!cords}
>
<Text size="B300">Add Space</Text>
<Text size="B300">{t('Create.add_space')}</Text>
</Chip>
{addExisting && (
<AddExistingModal space parentId={item.roomId} requestClose={() => setAddExisting(false)} />

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box, Icon, Icons, Scroll } from 'folds';
import { useTranslation } from 'react-i18next';
import {
Page,
PageContent,
@ -11,6 +12,7 @@ import { CreateSpaceForm } from '../../../features/create-space';
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
export function Create() {
const { t } = useTranslation();
const { navigateSpace } = useRoomNavigate();
return (
@ -23,8 +25,8 @@ export function Create() {
<Box direction="Column" gap="700">
<PageHero
icon={<Icon size="600" src={Icons.Space} />}
title="Create Space"
subTitle="Build a space for your community."
title={t('Create.create_space')}
subTitle={t('Create.create_space_desc')}
/>
<CreateSpaceForm onCreate={navigateSpace} />
</Box>

View file

@ -2,6 +2,7 @@ import React, { MouseEventHandler, useState } from 'react';
import { Box, config, Icon, Icons, Menu, PopOut, RectCords, Text } from 'folds';
import FocusTrap from 'focus-trap-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '../../../components/sidebar';
import { stopPropagation } from '../../../utils/keyboard';
import { SequenceCard } from '../../../components/sequence-card';
@ -18,6 +19,7 @@ import { JoinAddressPrompt } from '../../../components/join-address-prompt';
import { _RoomSearchParams } from '../../paths';
export function CreateTab() {
const { t } = useTranslation();
const createSelected = useCreateSelected();
const navigate = useNavigate();
@ -40,7 +42,7 @@ export function CreateTab() {
return (
<SidebarItem active={createSelected}>
<SidebarItemTooltip tooltip="Add Space">
<SidebarItemTooltip tooltip={t('Create.add_space')}>
{(triggerRef) => (
<PopOut
anchor={menuCords}
@ -73,9 +75,9 @@ export function CreateTab() {
onClick={handleCreateSpace}
>
<SettingTile before={<Icon size="400" src={Icons.Space} />}>
<Text size="H6">Create Space</Text>
<Text size="H6">{t('Create.create_space')}</Text>
<Text size="T300" priority="300">
Build a space for your community.
{t('Create.create_space_desc')}
</Text>
</SettingTile>
</SequenceCard>
@ -90,9 +92,9 @@ export function CreateTab() {
onClick={handleJoinWithAddress}
>
<SettingTile before={<Icon size="400" src={Icons.Link} />}>
<Text size="H6">Join with Address</Text>
<Text size="H6">{t('Create.join_with_address')}</Text>
<Text size="T300" priority="300">
Become a part of existing community.
{t('Create.join_with_address_desc')}
</Text>
</SettingTile>
</SequenceCard>