Implemented translation to all organisms

This commit is contained in:
Dylan Van Nielen 2022-07-14 10:48:23 +09:30
parent 8c7749532b
commit 7bb187bce1
33 changed files with 1112 additions and 406 deletions

View file

@ -1,19 +1,80 @@
{
"common" : {
"close": "Close",
"open": "Open",
"leave": "Leave",
"options": "Options"
"options": "Options",
"cinny": "Cinny",
"slogan": "Yet another matrix client",
"source_code": "Source code",
"sponsor": "Support",
"retry": "Retry",
"delete": "Delete",
"continue": "Continue",
"cancel": "Cancel",
"save": "Save",
"view_more": "View more",
"view_less": "View less",
"copy": "Copy",
"upload": "Upload",
"download": "Download",
"or": "Or",
"reset": "Reset",
"setup": "Setup",
"search": "Search",
"loading": "Loading...",
"joining": "Joining...",
"join": "Join",
"remove": "Remove",
"send": "Send",
"homeserver": "Homeserver",
"invite": "Invite",
"uninvite": "Uninvite",
"invited": "Invited",
"inviting": "Inviting...",
"uninviting": "Uninviting...",
"change": "Change",
"edit": "Edit",
"message_prompt": "Message"
},
"welcome": {
"errors": {
"browser_not_supported": "Not supported in this browser",
"generic": "Something went wrong!"
},
"Welcome": {
"heading": "Welcome to Cinny!",
"subheading": "Yet another Matrix client"
},
"view_source":{
"ViewSource":{
"title": "View Source",
"original_source": "Original source",
"decrypted_source": "Decrypted source"
},
"space_settings":{
"SpaceManage": {
"subtitle": "manage rooms",
"load_more": "Load more",
"rooms_and_spaces": "Rooms and spaces",
"private_rooms_message": "Either the space contains private rooms or you need to join space to view it's rooms.",
"items_selected_zero": "No selected items",
"items_selected_one": "{{count}} selected item",
"items_selected_other": "{{count}} selected items",
"room_members_zero": "No room members",
"room_members_one": "{{count}} room member",
"room_members_other": "{{count}} room members",
"mark_suggested_zero": "Marking no rooms as suggested",
"mark_suggested_one": "Marking {{count}} room as suggested",
"mark_suggested_other": "Marking {{count}} rooms as suggested",
"mark_not_suggested_zero": "Marking no rooms as suggested",
"mark_not_suggested_one": "Marking {{count}} room as suggested",
"mark_not_suggested_other": "Marking {{count}} rooms as suggested",
"remove_zero": "Removing no items",
"remove_one": "Removing {{count}} item",
"remove_other": "Removing {{count}} items.",
"suggested": "Suggested",
"mark_as_suggested":"Mark as suggested",
"mark_as_not_suggested": "Mark as not suggested"
},
"SpaceSettings":{
"subtitle": "space settings",
"leave":{
"leave_space": "Leave Space",
@ -30,5 +91,352 @@
"uncategorize_subspaces": "Uncategorize subspaces",
"pin_sidebar": "Pin to sidebar",
"unpin_sidebar": "Unpin from sidebar"
},
"Settings": {
"title": "Settings",
"theme": {
"follow_system": {
"title": "Follow system theme",
"description": "Use light or dark mode based on the system settings."
},
"title": "Theme",
"theme_light": "Light",
"theme_silver": "Silver",
"theme_dark": "Dark",
"theme_butter": "Butter"
},
"markdown": {
"title": "Markdown formatting",
"description": "Format messages with markdown before sending"
},
"hide_membership_events": {
"title": "Hide membership events",
"description": "Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)"
},
"hide_nickname_avatar_events": {
"title": "Hide nick/avatar events",
"description": "Hide nickname and avatar change messages from the room timeline."
},
"notifications_and_sound": {
"title": "Notifications & Sound",
"desktop": {
"title": "Desktop notifications",
"description": "Show desktop notifications when new messages arrive."
},
"sound": {
"title": "Notification sound",
"description": "Play a sound when new messages arrive."
}
},
"security": {
"cross_signing": {
"title": "Cross signing and backup"
},
"export_import_encryption_keys": {
"title": "Export / Import encryption keys"
},
"export_encryption_keys": {
"title": "Export E2E room keys",
"description": "Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing."
},
"import_encryption_keys": {
"title": "Import E2E room keys",
"description": "To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\\'ll have to enter the password you set in order to decrypt it."
}
},
"logout": {
"title": "Logout",
"dialog": {
"title": "Logout",
"description": "Are you sure that you want to logout your session?",
"confirm": "Logout"
}
},
"about":{
"application": "Application",
"credits": "Credits"
}
},
"ShortcutSpaces": {
"header": "Pin Spaces",
"pinned_spaces": "Pinned spaces",
"no_pinned_spaces": "No pinned spaces",
"unpinned_spaces": "Unpinned spaces",
"no_unpinned_spaces": "No unpinned spaces",
"spaces_selected_zero": "No selected spaces",
"spaces_selected_one": "{{count}} selected space",
"spaces_selected_other": "{{count}} selected spaces",
"pin_button": "Pin"
},
"SecretStorageAccess": {
"incorrect_security_key": "Incorrect security key",
"incorrect_security_phrase": "Incorrect security phrase",
"security_phrase": "Security Phrase",
"security_key": "Security Key",
"use_security_key": "Use Security Key",
"use_security_phrase": "Use Security Phrase"
},
"KeyBackup": {
"create_backup_title": "Create key backup",
"create_backup_tooltip": "Create backup",
"creating_backup": "Creating Backup...",
"backup_created": "Successfully created backup",
"backup_failed": "Failed to create backup",
"restoring": "Restoring backup keys...",
"restoring_progress": "Restoring backup keys... ({{progress}}/{{total}}",
"restore_backup_title": "Restore Key Backup",
"restore_backup_tooltip": "Restore Key Backup",
"restore_complete": "Successfully restored backup keys ({{progress}}/{{total}})",
"restore_failed_bad_key": "Failed to restore backup. Key is invalid!",
"restore_failed_unknown": "Failed to restore backup.",
"delete_key_backup_title": "Delete key backup",
"delete_key_backup_tooltip": "Delete backup",
"delete_key_backup_subtitle": "Deleting key backup is permanent.",
"delete_key_backup_message": "All encrypted message keys stored on the server will be permanently deleted.",
"encrypted_messages_backup_description": "Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.",
"encrypted_messages_backup_title": "Encrypted messages backup",
"encrypted_messages_backup_cross_signing_disabled": "Setup cross signing to backup your encrypted messages."
},
"DeviceManage": {
"edit_session_name_title": "Edit session name",
"edit_session_name_subtitle": "Session name",
"edit_session_name_tooltip": "Edit session name",
"current_device_label": "Current",
"verify_session_button": "Verify",
"unverified_sessions_title": "Unverified sessions",
"unverified_sessions_none": "No unverified sessions",
"unencrypted_sessions_title": "Sessions without encryption support",
"verified_sessions_title": "Verified sessions",
"verified_sessions_none": "No verified sessions",
"setup_cross_signing_message": "Setup cross signing in case you lose all your sessions",
"loading_devices": "Loading devices...",
"logout_device_title": "Logout {{device}}",
"logout_device_message": "You are about to log out the session for {{device}}",
"logout_device_confirm": "Logout",
"logout_device_tooltip": "Remove session",
"session_verification_title": "Session Verification",
"session_name_privacy_message": "Session names are visible to everyone, so do not put any private info here."
},
"CrossSigning":{
"title": "Cross Signing",
"setup_failed": "Failed to setup cross signing. Please try again",
"setup": "Setup cross signing",
"save_security_key_message": "Please save this security key somewhere safe",
"security_key_dialog_title": "Security Key",
"security_key_generation_message": "We will generate a Security Key, which you can use to manage message backups and session verification.",
"security_key_generation_button": "Generate Key",
"security_phrase_message": "Alternatively you can set a 'Security Phrase' so you don't have to remember the long Security Key, and optionally save the key as a backup",
"security_phrase_label": "Security Phrase",
"security_phrase_confirm_label": "Confirm Security Phrase",
"security_phrase_set_button": "Set Phrase & Generate Key",
"setup_dialog_title": "Setup cross signing",
"setup_message": "Setup to verify and keep track of all your sessions. Also required to backup encrypted message.",
"reset_keys_subtitle": "Resetting cross-signing keys is permanent.",
"reset_keys_message": "Anyone you have verified with will see security alerts and your message backup will lost. You almost certainly do not want to do this, unless you have lost Security Key or Phrase and every session you can cross-sign from."
},
"AuthRequest" : {
"wrong_password": "Wrong password. Please enter the correct password",
"request_failed": "Request failed!",
"password_label": "Account password"
},
"Search": {
"description": "Type # for rooms, @ for DMs and * for spaces. Hotkey: Ctrl + k"
},
"RoomViewInput":{
"upload_progress": "Uploading: {{progress}}/{{total}} ({{percent}}%)",
"tombstone_replaced": "This room has been replaced, and is no longer active.",
"tombstone_permission_denied": "You do not have permission to post to this room",
"send_message_placeholder": "Send a message...",
"emoji_tooltip": "Emoji",
"file_size": "Size: {{size}}",
"cancel_reply_tooltip": "Cancel reply"
},
"RoomViewHeader": {
"search_tooltip": "Search",
"people_tooltip": "People",
"members_tooltip": "Members"
},
"RoomViewFloating": {
"jump_unread": "Jump to unread messages",
"mark_read": "Mark as read",
"jump_latest": "Jump to latest",
"user_typing_one": "<bold>{{user_one}}</bold> is typing...",
"user_typing_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> are typing...",
"user_typing_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold> and <bold>{{user_three}}</bold> are typing...",
"user_typing_four": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and <bold>{{user_four}}</bold> are typing...",
"user_typing_other": "<bold>Several people</bold> are typing..."
},
"RoomViewContent": {
"welcome_to_room": "Welcome to {{room_name}}!",
"beginning_room": "This is the beginning of the <bold>{{room_name}}</bold> room.",
"beginning_dm": "This is the beginning of your direct message history with <bold>@{{user_name}}</bold>.",
"created_on": "Created on {{date, datetime}}",
"new_messages": "New messages"
},
"RoomSettings" : {
"leave_room": "Leave room",
"leave_room_confirm_message": "Are you sure you want to leave {{room_name}}?",
"leave_room_confirm_button": "Leave",
"notification_header": "Notifications (Changing this will only affect you)",
"visibility_header": "Room visibility (Who can join)",
"address_header": "Room addresses",
"encryption_header": "Encryption",
"message_history_header": "Message history visibility",
"room_settings_subtitle": "room settings"
},
"RoomCommon": {
"user_joined": "<bold>{{user_name}}</bold> joined the room",
"user_left": "<bold>{{user_name}}</bold> left the room",
"user_invited": "<bold>{{inviter_name}}</bold> invited <bold>{{user_name}}</bold>",
"invite_cancelled": "<bold>{{inviter_name}}</bold> cancelled <bold>{{user_name}}'s</bold> invite",
"invite_rejected": "<bold>{{user_name}}</bold> rejected the invitation",
"user_kicked": "<bold>{{actor}}</bold> kicked <bold>{{user_name}}</bold>: {{reason}}",
"user_banned": "<bold>{{actor}}</bold> banned <bold>{{user_name}}</bold>: {{reason}}",
"user_unbanned": "<bold>{{actor}} unbanned <bold>{{user_name}}</bold>",
"avatar_set": "<bold>{{user_name}}</bold> set an avatar",
"avatar_changed": "<bold>{{user_name}}</bold> changed their avatar",
"avatar_removed": "<bold>{{user_name}}</bold> removed their avatar",
"name_set": "<bold>{{user_name}}</bold> set their display name to <bold>{{new_name}}</bold>",
"name_changed": "<bold>{{user_name}}</bold> changed their display name to <bold>{{new_name}}</bold>",
"name_removed": "<bold>{{user_name}}</bold> removed their display name <bold>{{new_name}}</bold>"
},
"PublicRooms": {
"could_not_join_alias": "Unable to join {{alias}}. Either the room is private or doesn't exist",
"try_joining_alias": "Try joining {{alias}}",
"joining_alias": "Joining {{alias}}...",
"no_public_rooms": "No public rooms on {{homeserver}}",
"no_result_found": "No result found for '{{input}}' on {{homeserver}}",
"title": "Public Rooms",
"search_room_name_alias": "Room name or alias",
"search_button": "Search",
"loading": "Loading public rooms from {{homeserver}}...",
"searching": "Searching for '{{query}}' on {{homeserver}}...",
"result_title": "Public rooms on {{homeserver}}",
"search_result_title": "Search result for '{{query}}' on {{homeserver}}"
},
"ProfileViewer": {
"kick_button": "Kick",
"kick_reason_label": "Kick Reason",
"ban_button": "Ban",
"ban_reason_label": "Ban reason",
"loading_sessions" : "Loading sessions...",
"no_sessions_found": "No sessions found.",
"view_sessions_one": "View session",
"view_sessions_other": "View {{count}} sessions",
"send_direct_message_button": "Message",
"creating_dm_room": "Creating room...",
"ignore": "Ignore",
"ignoring": "Ignoring...",
"unignore": "Unignore",
"unignoring": "Unignoring...",
"change_power_level": "Change power level",
"shared_power_message": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?",
"demoting_self_message": "You will not be able to undo this change as you are demoting yourself. Are you sure?"
},
"ProfileEditor": {
"remove_avatar": "Remove avatar",
"remove_avatar_confirmation": "Are you sure that you want to remove your avatar?",
"display_name_message": "Display name of {{user_name}}"
},
"DrawerBreadcrumb": {
"home": "Home"
},
"DrawerHeader" : {
"add_rooms_or_spaces": "Add rooms or spaces",
"create_new_space": "Create new space",
"create_new_room": "Create new room",
"join_public_room": "Join public room",
"join_with_address": "Join with address",
"add_existing": "Add existing",
"manage_rooms": "Manage rooms",
"home": "Home",
"direct_messages": "Direct messages",
"start_dm_tooltip": "Start DM",
"add_rooms_spaces_tooltip": "Add rooms/spaces"
},
"SideBar": {
"settings_tooltip": "Settings",
"unverified_sessions_one": "{{count}} unverified session",
"unverified_sessions_other": "{{count}} unverified sessions",
"home_tooltip": "Home",
"direct_messages_tooltip": "People",
"pin_spaces_tooltip": "Pin spaces",
"search_tooltip": "Search",
"invites_tooltip": "Invites"
},
"JoinAlias": {
"invalid_address": "Invalid address.",
"looking_for_address": "Looking for address...",
"joining_alias": "Joining {{alias_name}}...",
"couldnt_find_room_or_space_alias": "Unable to find room/space with {{alias_name}}. Either the room/space is private or doesn't exist.",
"couldnt_find_room_or_space": "Unable to join {{alias_name}}. Either the room/space is private or doesn't exist.",
"address_label": "Address",
"title": "Join with address"
},
"InviteUser": {
"user_not_found": "{{user_name}} not found!",
"no_matches_found": "No matches found for {{user_name}}",
"invite_result": {
"invited": "Invited",
"already_joined": "Already joined",
"already_invited": "Already invited",
"banned": "Banned"
},
"search_label": "Name or User ID",
"search_result_title": "Search result for user {{user_name}}",
"searching_for_user": "Searching for user {{user_name}}...",
"invite_to_room": "Invite to {{room}}",
"invite_to_dm": "Direct Message"
},
"InviteList": {
"accept_invite": "Accept",
"reject_invite": "Reject",
"direct_messages_title": "Direct Messages",
"rooms_title": "Rooms",
"spaces_title": "Spaces",
"title": "Invites"
},
"EmojiVerification": {
"waiting_for_response": "Waiting for response from other device...",
"confirmation_prompt": "Confirm the emoji below are displayed on both devices, in the same order:",
"emojis_match_button": "They match",
"emojis_dont_match_button": "They don't match",
"accept_request_from_other_device_message": "Please accept the request from other device.",
"begin_verification_process_message": "Click accept to start the verification process.",
"begin_verification_button_text": "Accept",
"title": "Emoji Verification"
},
"DragDrop": {
"drop_file_to_upload_prompt": "Drop file to upload"
},
"CreateRoom": {
"private_room_short": "Private",
"restricted_room_short": "Restricted",
"public_room_short": "Public",
"private_room_long": "Private (invite only)",
"restricted_room_long": "Restricted (space member can join)",
"public_room_long": "Public (anyone can join)",
"visibility_title": "Visibility",
"visibility_message": "Visibility (who can join)",
"select_who_can_join_space": "Select who can join this space",
"select_who_can_join_room": "Select who can join this room",
"space_address": "Space address",
"room_address": "Room address",
"room_address_already_in_use": "{{room_address}} is already in use",
"e2e_title": "Enable end-to-end encryption",
"e2e_message": "You cant disable this later. Bridges & most bots wont work yet.",
"role_title": "Select your role",
"role_message": "Selecting 'Admin' sets your power level to 100 whereas 'Founder' sets it to 101.",
"creating_room": "Creating room...",
"creating_space": "Creating space...",
"topic_label": "Topic (optional)",
"space_name": "Space name",
"room_name": "Room name",
"role_admin": "Admin",
"role_founder": "Founder",
"create_room": "Create room",
"create_space": "Create space",
"home": "Home"
}
}

View file

@ -3,7 +3,8 @@
"close": "閉める",
"leave": "残す"
},
"welcome":{
"heading": "いらっしゃいませ"
"Welcome": {
"heading": "おはようございます",
"subheading": "Yet another Matrix client"
}
}

View file

@ -14,7 +14,6 @@ i18n
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
}

View file

@ -33,7 +33,14 @@ import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
const { t } = useTranslation();
const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
const [isEncrypted, setIsEncrypted] = useState(true);
const [isCreatingRoom, setIsCreatingRoom] = useState(false);
@ -130,8 +137,8 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
};
const joinRules = ['invite', 'restricted', 'public'];
const joinRuleShortText = ['Private', 'Restricted', 'Public'];
const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)'];
const joinRuleShortText = [ t("CreateRoom.private_room_short"), t("CreateRoom.restricted_room_short"), t("CreateRoom.public_room_short")];
const joinRuleText = [ t("CreateRoom.private_room_long"), t("CreateRoom.restricted_room_long"), t("CreateRoom.public_room_long")];
const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
const handleJoinRule = (evt) => {
@ -140,7 +147,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
getEventCords(evt, '.btn-surface'),
(closeMenu) => (
<>
<MenuHeader>Visibility (who can join)</MenuHeader>
<MenuHeader>{t("CreateRoom.visibility_message")}</MenuHeader>
{
joinRules.map((rule) => (
<MenuItem
@ -167,17 +174,17 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
<div className="create-room">
<form className="create-room__form" onSubmit={handleSubmit}>
<SettingTile
title="Visibility"
title={t("CreateRoom.visibility_title")}
options={(
<Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
{joinRuleShortText[joinRules.indexOf(joinRule)]}
</Button>
)}
content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
content={<Text variant="b3">{isSpace ? t("CreateRoom.select_who_can_join_space") : t("CreateRoom.select_who_can_join_room")}</Text>}
/>
{joinRule === 'public' && (
<div>
<Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
<Text className="create-room__address__label" variant="b2">{isSpace ? t("CreateRoom.space_address") : t("CreateRoom.room_address")}</Text>
<div className="create-room__address">
<Text variant="b1">#</Text>
<Input
@ -190,32 +197,32 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
/>
<Text variant="b1">{`:${userHs}`}</Text>
</div>
{isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{`#${addressValue}:${userHs} is already in use`}</span></Text>}
{isValidAddress === false && <Text className="create-room__address__tip" variant="b3"><span style={{ color: 'var(--bg-danger)' }}>{ t("CreateRoom.room_address_already_in_use", {room_address: `#${addressValue}:${userHs}`})}</span></Text>}
</div>
)}
{!isSpace && joinRule !== 'public' && (
<SettingTile
title="Enable end-to-end encryption"
title={t("CreateRoom.e2e_title")}
options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
content={<Text variant="b3">You cant disable this later. Bridges & most bots wont work yet.</Text>}
content={<Text variant="b3"> {t("CreateRoom.e2e_message")}</Text>}
/>
)}
<SettingTile
title="Select your role"
title={t("CreateRoom.role_title")}
options={(
<SegmentControl
selected={roleIndex}
segments={[{ text: 'Admin' }, { text: 'Founder' }]}
segments={[{ text: t("CreateRoom.role_admin")}, { text: t("CreateRoom.role_founder")}]}
onSelect={setRoleIndex}
/>
)}
content={(
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
<Text variant="b3"> {t("CreateRoom.role_message")}</Text>
)}
/>
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
<Input name="topic" minHeight={174} resizable label= {t("CreateRoom.topic_label")}/>
<div className="create-room__name-wrapper">
<Input name="name" label={`${isSpace ? 'Space' : 'Room'} name`} required />
<Input name="name" label={isSpace ? t("CreateRoom.space_name"): t("CreateRoom.room_name")} required />
<Button
disabled={isValidAddress === false || isCreatingRoom}
iconSrc={isSpace ? SpacePlusIC : HashPlusIC}
@ -228,7 +235,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
{isCreatingRoom && (
<div className="create-room__loading">
<Spinner size="small" />
<Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
<Text>{ isSpace ? t("CreateRoom.creating_space") : t("CreateRoom.creating_room")}</Text>
</div>
)}
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
@ -277,13 +284,13 @@ function CreateRoom() {
isOpen={create !== null}
title={(
<Text variant="s1" weight="medium" primary>
{parentId ? twemojify(room.name) : 'Home'}
{parentId ? twemojify(room.name) : t("CreateRoom.home")}
<span style={{ color: 'var(--tc-surface-low)' }}>
{`create ${isSpace ? 'space' : 'room'}`}
{`${isSpace ? t("CreateRoom.create_space") : t("CreateRoom.create_room")}`}
</span>
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose}
>
{

View file

@ -5,14 +5,20 @@ import './DragDrop.scss';
import RawModal from '../../atoms/modal/RawModal';
import Text from '../../atoms/text/Text';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function DragDrop({ isOpen }) {
const { t } = useTranslation();
return (
<RawModal
className="drag-drop__modal"
overlayClassName="drag-drop__overlay"
isOpen={isOpen}
>
<Text variant="h2" weight="medium">Drop file to upload</Text>
<Text variant="h2" weight="medium">{t("DragDrop.drop_file_to_upload_prompt")}</Text>
</RawModal>
);
}

View file

@ -20,6 +20,10 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';
import { accessSecretStorage } from '../settings/SecretStorageAccess';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function EmojiVerificationContent({ data, requestClose }) {
const [sas, setSas] = useState(null);
const [process, setProcess] = useState(false);
@ -28,6 +32,8 @@ function EmojiVerificationContent({ data, requestClose }) {
const mountStore = useStore();
const beginStore = useStore();
const { t } = useTranslation();
const beginVerification = async () => {
if (
isCrossVerified(mx.deviceId)
@ -94,14 +100,14 @@ function EmojiVerificationContent({ data, requestClose }) {
const renderWait = () => (
<>
<Spinner size="small" />
<Text>Waiting for response from other device...</Text>
<Text>{t("EmojiVerification.waiting_for_response")}</Text>
</>
);
if (sas !== null) {
return (
<div className="emoji-verification__content">
<Text>Confirm the emoji below are displayed on both devices, in the same order:</Text>
<Text>{t("EmojiVerification.confirmation_prompt")}</Text>
<div className="emoji-verification__emojis">
{sas.sas.emoji.map((emoji, i) => (
// eslint-disable-next-line react/no-array-index-key
@ -114,8 +120,8 @@ function EmojiVerificationContent({ data, requestClose }) {
<div className="emoji-verification__buttons">
{process ? renderWait() : (
<>
<Button variant="primary" onClick={sasConfirm}>They match</Button>
<Button onClick={sasMismatch}>{'They don\'t match'}</Button>
<Button variant="primary" onClick={sasConfirm}>{t("EmojiVerification.emojis_match_button")}</Button>
<Button onClick={sasMismatch}>{t("EmojiVerification.emojis_dont_match_button")}</Button>
</>
)}
</div>
@ -126,7 +132,7 @@ function EmojiVerificationContent({ data, requestClose }) {
if (targetDevice) {
return (
<div className="emoji-verification__content">
<Text>Please accept the request from other device.</Text>
<Text>{t("EmojiVerification.accept_request_from_other_device_message")}</Text>
<div className="emoji-verification__buttons">
{renderWait()}
</div>
@ -136,12 +142,12 @@ function EmojiVerificationContent({ data, requestClose }) {
return (
<div className="emoji-verification__content">
<Text>Click accept to start the verification process.</Text>
<Text>{t("EmojiVerification.begin_verification_process_message")}</Text>
<div className="emoji-verification__buttons">
{
process
? renderWait()
: <Button variant="primary" onClick={beginVerification}>Accept</Button>
: <Button variant="primary" onClick={beginVerification}>{t("EmojiVerification.begin_verification_button_text")}</Button>
}
</div>
</div>
@ -182,10 +188,10 @@ function EmojiVerification() {
className="emoji-verification"
title={(
<Text variant="s1" weight="medium" primary>
Emoji verification
{t("EmojiVerification.title")}
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip={t("common.close")} />}
onRequestClose={requestClose}
>
{

View file

@ -16,9 +16,17 @@ import RoomTile from '../../molecules/room-tile/RoomTile';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function InviteList({ isOpen, onRequestClose }) {
const [procInvite, changeProcInvite] = useState(new Set());
const { t } = useTranslation();
function acceptInvite(roomId, isDM) {
procInvite.add(roomId);
changeProcInvite(new Set(Array.from(procInvite)));
@ -73,8 +81,8 @@ function InviteList({ isOpen, onRequestClose }) {
? (<Spinner size="small" />)
: (
<div className="invite-btn__container">
<Button onClick={() => rejectInvite(myRoom.roomId)}>Reject</Button>
<Button onClick={() => acceptInvite(myRoom.roomId)} variant="primary">Accept</Button>
<Button onClick={() => rejectInvite(myRoom.roomId)}>{t("InviteList.reject_invite")}</Button>
<Button onClick={() => acceptInvite(myRoom.roomId)} variant="primary">{t("InviteList.accept_invite")}</Button>
</div>
)
}
@ -85,14 +93,14 @@ function InviteList({ isOpen, onRequestClose }) {
return (
<PopupWindow
isOpen={isOpen}
title="Invites"
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
title={t("InviteList.title")}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose}
>
<div className="invites-content">
{ initMatrix.roomList.inviteDirects.size !== 0 && (
<div className="invites-content__subheading">
<Text variant="b3" weight="bold">Direct Messages</Text>
<Text variant="b3" weight="bold">{t("InviteList.direct_messages_title")}</Text>
</div>
)}
{
@ -110,8 +118,8 @@ function InviteList({ isOpen, onRequestClose }) {
? (<Spinner size="small" />)
: (
<div className="invite-btn__container">
<Button onClick={() => rejectInvite(myRoom.roomId, true)}>Reject</Button>
<Button onClick={() => acceptInvite(myRoom.roomId, true)} variant="primary">Accept</Button>
<Button onClick={() => rejectInvite(myRoom.roomId, true)}>{t("InviteList.reject_invite")}</Button>
<Button onClick={() => acceptInvite(myRoom.roomId, true)} variant="primary">{t("InviteList.accept_invite")}</Button>
</div>
)
}
@ -121,14 +129,14 @@ function InviteList({ isOpen, onRequestClose }) {
}
{ initMatrix.roomList.inviteSpaces.size !== 0 && (
<div className="invites-content__subheading">
<Text variant="b3" weight="bold">Spaces</Text>
<Text variant="b3" weight="bold">{t("InviteList.spaces_title")}</Text>
</div>
)}
{ Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) }
{ initMatrix.roomList.inviteRooms.size !== 0 && (
<div className="invites-content__subheading">
<Text variant="b3" weight="bold">Rooms</Text>
<Text variant="b3" weight="bold">{t("InviteList.rooms_title")}</Text>
</div>
)}
{ Array.from(initMatrix.roomList.inviteRooms).map(renderRoomTile) }

View file

@ -19,6 +19,10 @@ import RoomTile from '../../molecules/room-tile/RoomTile';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import UserIC from '../../../../public/res/ic/outlined/user.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function InviteUser({
isOpen, roomId, searchTerm, onRequestClose,
}) {
@ -36,6 +40,8 @@ function InviteUser({
const usernameRef = useRef(null);
const { t } = useTranslation();
const mx = initMatrix.matrixClient;
function getMapCopy(myMap) {
@ -82,7 +88,7 @@ function InviteUser({
avatar_url: result.avatar_url,
}]);
} catch (e) {
updateSearchQuery({ error: `${inputUsername} not found!` });
updateSearchQuery({error: t("InviteUser.user_not_found", {user_name: inputUsername})});
}
} else {
try {
@ -91,13 +97,13 @@ function InviteUser({
limit: 20,
});
if (result.results.length === 0) {
updateSearchQuery({ error: `No matches found for "${inputUsername}"!` });
updateSearchQuery({ error: t("InviteUser.no_matches_found", {user_name: inputUsername})});
updateIsSearching(false);
return;
}
updateUsers(result.results);
} catch (e) {
updateSearchQuery({ error: 'Something went wrong!' });
updateSearchQuery({ error: t("errors.generic")});
}
}
updateIsSearching(false);
@ -135,7 +141,7 @@ function InviteUser({
} catch (e) {
deleteUserFromProc(userId);
if (typeof e.message === 'string') procUserError.set(userId, e.message);
else procUserError.set(userId, 'Something went wrong!');
else procUserError.set(userId, t("errors.generic"));
updateUserProcError(getMapCopy(procUserError));
}
}
@ -155,7 +161,7 @@ function InviteUser({
} catch (e) {
deleteUserFromProc(userId);
if (typeof e.message === 'string') procUserError.set(userId, e.message);
else procUserError.set(userId, 'Something went wrong!');
else procUserError.set(userId, t("errors.generic"));
updateUserProcError(getMapCopy(procUserError));
}
}
@ -173,7 +179,7 @@ function InviteUser({
return <Button onClick={() => { selectRoom(createdDM.get(userId)); onRequestClose(); }}>Open</Button>;
}
if (invitedUserIds.has(userId)) {
return messageJSX('Invited', true);
return messageJSX(t("InviteUser.invite_result.invited"), true);
}
if (typeof roomId === 'string') {
const member = mx.getRoom(roomId).getMember(userId);
@ -181,18 +187,18 @@ function InviteUser({
const userMembership = member.membership;
switch (userMembership) {
case 'join':
return messageJSX('Already joined', true);
return messageJSX(t("InviteUser.invite_result.already_joined"), true);
case 'invite':
return messageJSX('Already Invited', true);
return messageJSX(t("InviteUser.invite_result.already_invited"), true);
case 'ban':
return messageJSX('Banned', false);
return messageJSX(t("InviteUser.invite_result.banned"), false);
default:
}
}
}
return (typeof roomId === 'string')
? <Button onClick={() => inviteToRoom(userId)} variant="primary">Invite</Button>
: <Button onClick={() => createDM(userId)} variant="primary">Message</Button>;
? <Button onClick={() => inviteToRoom(userId)} variant="primary">{t("common.invite")}</Button>
: <Button onClick={() => createDM(userId)} variant="primary">{t("common.message_prompt")}</Button>;
};
const renderError = (userId) => {
if (!procUserError.has(userId)) return null;
@ -239,27 +245,27 @@ function InviteUser({
return (
<PopupWindow
isOpen={isOpen}
title={(typeof roomId === 'string' ? `Invite to ${mx.getRoom(roomId).name}` : 'Direct message')}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
title={(typeof roomId === 'string' ? t("InviteUser.invite_to_room", {room: mx.getRoom(roomId).name}) : t("InviteUser.invite_to_dm"))}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose}
>
<div className="invite-user">
<form className="invite-user__form" onSubmit={(e) => { e.preventDefault(); searchUser(usernameRef.current.value); }}>
<Input value={searchTerm} forwardRef={usernameRef} label="Name or userId" />
<Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">Search</Button>
<Input value={searchTerm} forwardRef={usernameRef} label={t("InviteUser.search_label")} />
<Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">{t("common.search")}</Button>
</form>
<div className="invite-user__search-status">
{
typeof searchQuery.username !== 'undefined' && isSearching && (
<div className="flex--center">
<Spinner size="small" />
<Text variant="b2">{`Searching for user "${searchQuery.username}"...`}</Text>
<Text variant="b2">{t("InviteUser.searching_for_user", {user_name: searchQuery.username})}</Text>
</div>
)
}
{
typeof searchQuery.username !== 'undefined' && !isSearching && (
<Text variant="b2">{`Search result for user "${searchQuery.username}"`}</Text>
<Text variant="b2">{t("InviteUser.search_result_title", {user_name: searchQuery.username})}</Text>
)
}
{

View file

@ -19,6 +19,11 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
function JoinAliasContent({ term, requestClose }) {
@ -29,6 +34,8 @@ function JoinAliasContent({ term, requestClose }) {
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const { t } = useTranslation();
const openRoom = (roomId) => {
const room = mx.getRoom(roomId);
if (!room) return;
@ -54,10 +61,10 @@ function JoinAliasContent({ term, requestClose }) {
const alias = e.target.alias.value;
if (alias?.trim() === '') return;
if (alias.match(ALIAS_OR_ID_REG) === null) {
setError('Invalid address.');
setError(t("JoinAlias.invalid_address"));
return;
}
setProcess('Looking for address...');
setProcess(t("JoinAlias.looking_for_address"));
setError(undefined);
let via;
if (alias.startsWith('#')) {
@ -65,12 +72,12 @@ function JoinAliasContent({ term, requestClose }) {
const aliasData = await mx.resolveRoomAlias(alias);
via = aliasData?.servers.slice(0, 3) || [];
if (mountStore.getItem()) {
setProcess(`Joining ${alias}...`);
setProcess(t("JoinAlias.joining_alias", {alias_name: alias}));
}
} catch (err) {
if (!mountStore.getItem()) return;
setProcess(false);
setError(`Unable to find room/space with ${alias}. Either room/space is private or doesn't exist.`);
setError(t("JoinAlias.couldnt_find_room_or_space_alias", {alias_name: alias}));
}
}
try {
@ -81,14 +88,14 @@ function JoinAliasContent({ term, requestClose }) {
} catch {
if (!mountStore.getItem()) return;
setProcess(false);
setError(`Unable to join ${alias}. Either room/space is private or doesn't exist.`);
setError(t("JoinAlias.couldnt_find_room_or_space", {alias_name: alias}));
}
};
return (
<form className="join-alias" onSubmit={handleSubmit}>
<Input
label="Address"
label={t("JoinAlias.address_label")}
value={term}
name="alias"
required
@ -103,7 +110,7 @@ function JoinAliasContent({ term, requestClose }) {
<Text>{process}</Text>
</>
)
: <Button variant="primary" type="submit">Join</Button>
: <Button variant="primary" type="submit">{t("common.join")}</Button>
}
</div>
</form>
@ -142,9 +149,9 @@ function JoinAlias() {
<Dialog
isOpen={data !== null}
title={(
<Text variant="s1" weight="medium" primary>Join with address</Text>
<Text variant="s1" weight="medium" primary>{t("JoinAlias.title")}</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip={"common.close"} />}
onRequestClose={requestClose}
>
{ data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> }

View file

@ -18,6 +18,11 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function DrawerBreadcrumb({ spaceId }) {
const [, forceUpdate] = useState({});
const scrollRef = useRef(null);
@ -25,6 +30,8 @@ function DrawerBreadcrumb({ spaceId }) {
const mx = initMatrix.matrixClient;
const spacePath = navigation.selectedSpacePath;
const { t } = useTranslation();
function onNotiChanged(roomId, total, prevTotal) {
if (total === prevTotal) return;
if (navigation.selectedSpacePath.includes(roomId)) {
@ -109,7 +116,7 @@ function DrawerBreadcrumb({ spaceId }) {
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
onClick={() => selectSpace(id)}
>
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
<Text variant="b2">{id === cons.tabs.HOME ? t("DrawerBreadcrumb.home") : twemojify(mx.getRoom(id).name)}</Text>
{ noti !== null && (
<NotificationBadge
alert={noti.highlight !== 0}

View file

@ -28,6 +28,11 @@ import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(spaceId);
@ -35,29 +40,31 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
? room.currentState.maySendStateEvent('m.space.child', mx.getUserId())
: true;
const { t } = useTranslation();
return (
<>
<MenuHeader>Add rooms or spaces</MenuHeader>
<MenuHeader>{t("DrawerHeader.add_rooms_or_spaces")}</MenuHeader>
<MenuItem
iconSrc={SpacePlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
disabled={!canManage}
>
Create new space
{t("DrawerHeader.create_new_space")}
</MenuItem>
<MenuItem
iconSrc={HashPlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
disabled={!canManage}
>
Create new room
{t("DrawerHeader.create_new_room")}
</MenuItem>
{ !spaceId && (
<MenuItem
iconSrc={HashGlobeIC}
onClick={() => { afterOptionSelect(); openPublicRooms(); }}
>
Join public room
{t("DrawerHeader.join_public_room")}
</MenuItem>
)}
{ !spaceId && (
@ -65,7 +72,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
iconSrc={PlusIC}
onClick={() => { afterOptionSelect(); openJoinAlias(); }}
>
Join with address
{t("DrawerHeader.join_with_address")}
</MenuItem>
)}
{ spaceId && (
@ -74,7 +81,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
disabled={!canManage}
>
Add existing
{t("DrawerHeader.add_existing")}
</MenuItem>
)}
{ spaceId && (
@ -82,7 +89,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
iconSrc={HashSearchIC}
>
Manage rooms
{t("DrawerHeader.manage_rooms")}
</MenuItem>
)}
</>
@ -98,7 +105,7 @@ HomeSpaceOptions.propTypes = {
function DrawerHeader({ selectedTab, spaceId }) {
const mx = initMatrix.matrixClient;
const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages';
const tabName = selectedTab !== cons.tabs.DIRECTS ? t("DrawerHeader.home") : t("DrawerHeader.direct_messages");
const isDMTab = selectedTab === cons.tabs.DIRECTS;
const room = mx.getRoom(spaceId);
@ -142,8 +149,8 @@ function DrawerHeader({ selectedTab, spaceId }) {
</TitleWrapper>
)}
{ isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
{ !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
{ isDMTab && <IconButton onClick={() => openInviteUser()} tooltip={t("DrawerHeader.start_dm_tooltip")} src={PlusIC} size="small" /> }
{ !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip={t("DrawerHeader.add_rooms_spaces_tooltip")} src={PlusIC} size="small" /> }
</Header>
);
}

View file

@ -34,6 +34,11 @@ import { useDeviceList } from '../../hooks/useDeviceList';
import { tabText as settingTabText } from '../settings/Settings';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function useNotificationUpdate() {
const { notifications } = initMatrix;
const [, forceUpdate] = useState({});
@ -50,6 +55,9 @@ function useNotificationUpdate() {
}
function ProfileAvatarMenu() {
const { t } = useTranslation();
const mx = initMatrix.matrixClient;
const [profile, setProfile] = useState({
avatarUrl: null,
@ -77,7 +85,7 @@ function ProfileAvatarMenu() {
return (
<SidebarAvatar
onClick={openSettings}
tooltip="Settings"
tooltip={t("SideBar.settings_tooltip")}
avatar={(
<Avatar
text={profile.displayName}
@ -99,7 +107,7 @@ function CrossSigninAlert() {
return (
<SidebarAvatar
className="sidebar__cross-signin-alert"
tooltip={`${unverified.length} unverified sessions`}
tooltip={t("SideBar.unverified_sessions", {count: unverified.length})}
onClick={() => openSettings(settingTabText.SECURITY)}
avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
/>
@ -147,7 +155,7 @@ function FeaturedTab() {
return (
<>
<SidebarAvatar
tooltip="Home"
tooltip={t("SideBar.home_tooltip")}
active={selectedTab === cons.tabs.HOME}
onClick={() => selectTab(cons.tabs.HOME)}
avatar={<Avatar iconSrc={HomeIC} size="normal" />}
@ -159,7 +167,7 @@ function FeaturedTab() {
) : null}
/>
<SidebarAvatar
tooltip="People"
tooltip={t("SideBar.direct_messages_tooltip")}
active={selectedTab === cons.tabs.DIRECTS}
onClick={() => selectTab(cons.tabs.DIRECTS)}
avatar={<Avatar iconSrc={UserIC} size="normal" />}
@ -355,7 +363,7 @@ function SideBar() {
<div className="space-container">
<SpaceShortcut />
<SidebarAvatar
tooltip="Pin spaces"
tooltip={t("SideBar.pin_spaces_tooltip")}
onClick={() => openShortcutSpaces()}
avatar={<Avatar iconSrc={AddPinIC} size="normal" />}
/>
@ -367,13 +375,13 @@ function SideBar() {
<div className="sidebar-divider" />
<div className="sticky-container">
<SidebarAvatar
tooltip="Search"
tooltip={t("SideBar.search_tooltip")}
onClick={() => openSearch()}
avatar={<Avatar iconSrc={SearchIC} size="normal" />}
/>
{ totalInvites !== 0 && (
<SidebarAvatar
tooltip="Invites"
tooltip={t("SideBar.invites_tooltip")}
onClick={() => openInviteList()}
avatar={<Avatar iconSrc={InviteIC} size="normal" />}
notificationBadge={<NotificationBadge alert content={totalInvites} />}

View file

@ -16,6 +16,11 @@ import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import './ProfileEditor.scss';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
function ProfileEditor({ userId }) {
const [isEditing, setIsEditing] = useState(false);
@ -27,6 +32,8 @@ function ProfileEditor({ userId }) {
const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true);
const { t } = useTranslation();
useEffect(() => {
let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => {
@ -42,9 +49,9 @@ function ProfileEditor({ userId }) {
const handleAvatarUpload = async (url) => {
if (url === null) {
const isConfirmed = await confirmDialog(
'Remove avatar',
'Are you sure that you want to remove avatar?',
'Remove',
t("ProfileEditor.remove_avatar"),
t("ProfileViewer.remove_avatar_confirmation"),
t("common.remove"),
'caution',
);
if (isConfirmed) {
@ -83,13 +90,13 @@ function ProfileEditor({ userId }) {
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
>
<Input
label={`Display name of ${mx.getUserId()}`}
label={t("ProfileEditor.display_name_message", {user_name: mx.getUserId()})}
onChange={onDisplayNameInputChange}
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
<Button variant="primary" type="submit" disabled={disabled}>{t("common.save")}</Button>
<Button onClick={cancelDisplayNameChanges}>{t("common.cancel")}</Button>
</form>
);
@ -100,7 +107,7 @@ function ProfileEditor({ userId }) {
<IconButton
src={PencilIC}
size="extra-small"
tooltip="Edit"
tooltip={t("common.edit")}
onClick={() => setIsEditing(true)}
/>
</div>

View file

@ -34,9 +34,16 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function ModerationTools({
roomId, userId,
}) {
const { t } = useTranslation();
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const roomMember = room.getMember(userId);
@ -70,14 +77,14 @@ function ModerationTools({
<div className="moderation-tools">
{canIKick && (
<form onSubmit={handleKick}>
<Input label="Kick reason" name="kick-reason" />
<Button type="submit">Kick</Button>
<Input label={t("ProfileViewer.kick_reason_label")} name="kick-reason" />
<Button type="submit">{t("ProfileViewer.kick_button")}</Button>
</form>
)}
{canIBan && (
<form onSubmit={handleBan}>
<Input label="Ban reason" name="ban-reason" />
<Button type="submit">Ban</Button>
<Input label={t("ProfileViewer.ban_reason_label")} name="ban-reason" />
<Button type="submit">{t("ProfileViewer.ban_button")}</Button>
</form>
)}
</div>
@ -93,6 +100,8 @@ function SessionInfo({ userId }) {
const [isVisible, setIsVisible] = useState(false);
const mx = initMatrix.matrixClient;
const { t } = useTranslation();
useEffect(() => {
let isUnmounted = false;
@ -118,8 +127,8 @@ function SessionInfo({ userId }) {
if (!isVisible) return null;
return (
<div className="session-info__chips">
{devices === null && <Text variant="b2">Loading sessions...</Text>}
{devices?.length === 0 && <Text variant="b2">No session found.</Text>}
{devices === null && <Text variant="b2">{t("ProfileViewer.loading_sessions")}</Text>}
{devices?.length === 0 && <Text variant="b2">{t("ProfileViewer.no_sessions_found")}</Text>}
{devices !== null && (devices.map((device) => (
<Chip
key={device.deviceId}
@ -137,7 +146,7 @@ function SessionInfo({ userId }) {
onClick={() => setIsVisible(!isVisible)}
iconSrc={isVisible ? ChevronBottomIC : ChevronRightIC}
>
<Text variant="b2">{`View ${devices?.length > 0 ? `${devices.length} ` : ''}sessions`}</Text>
<Text variant="b2">{t("ProfileViewer.view_sessions", {count: devices?.length})}</Text>
</MenuItem>
{renderSessionChips()}
</div>
@ -252,7 +261,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
onClick={openDM}
disabled={isCreatingDM}
>
{isCreatingDM ? 'Creating room...' : 'Message'}
{isCreatingDM ? t("ProfileViewer.creating_dm_room") : t("ProfileViewer.send_direct_message_button")}
</Button>
{ isBanned && canIKick && (
<Button
@ -269,8 +278,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
>
{
isInvited
? `${isInviting ? 'Disinviting...' : 'Disinvite'}`
: `${isInviting ? 'Inviting...' : 'Invite'}`
? `${isInviting ? t("common.uninviting") : t("common.uninvite")}`
: `${isInviting ? t("common.inviting") : t("common.invite")}`
}
</Button>
)}
@ -281,8 +290,8 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
>
{
isUserIgnored
? `${isIgnoring ? 'Unignoring...' : 'Unignore'}`
: `${isIgnoring ? 'Ignoring...' : 'Ignore'}`
? `${isIgnoring ? t("ProfileViewer.unignoring") : t("ProfileViewer.unignore")}`
: `${isIgnoring ? t("ProfileViewer.ignoring") : t("ProfileViewer.ignore")}`
}
</Button>
</div>
@ -365,16 +374,16 @@ function ProfileViewer() {
const handleChangePowerLevel = async (newPowerLevel) => {
if (newPowerLevel === powerLevel) return;
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
const SHARED_POWER_MSG = t("ProfileViewer.shared_power_message");
const DEMOTING_MYSELF_MSG = t("ProfileViewer.demoting_self_message");
const isSharedPower = newPowerLevel === myPowerLevel;
const isDemotingMyself = userId === mx.getUserId();
if (isSharedPower || isDemotingMyself) {
const isConfirmed = await confirmDialog(
'Change power level',
t("ProfileViewer.change_power_level"),
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
'Change',
t("common.change"),
'caution',
);
if (!isConfirmed) return;
@ -435,7 +444,7 @@ function ProfileViewer() {
title={room?.name ?? ''}
onAfterClose={handleAfterClose}
onRequestClose={closeDialog}
contentOptions={<IconButton src={CrossIC} onClick={closeDialog} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={closeDialog} tooltip={t("common.close")} />}
>
{roomId ? renderProfile() : <div />}
</Dialog>

View file

@ -18,9 +18,15 @@ import RoomTile from '../../molecules/room-tile/RoomTile';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const SEARCH_LIMIT = 20;
function TryJoinWithAlias({ alias, onRequestClose }) {
const { t } = useTranslation();
const [status, setStatus] = useState({
isJoining: false,
error: null,
@ -53,7 +59,7 @@ function TryJoinWithAlias({ alias, onRequestClose }) {
} catch (e) {
setStatus({
isJoining: false,
error: `Unable to join ${alias}. Either room is private or doesn't exist.`,
error: t("PublicRooms.could_not_join_alias", {alias: alias}),
roomId: null,
tempRoomId: null,
});
@ -63,16 +69,16 @@ function TryJoinWithAlias({ alias, onRequestClose }) {
return (
<div className="try-join-with-alias">
{status.roomId === null && !status.isJoining && status.error === null && (
<Button onClick={() => joinWithAlias()}>{`Try joining ${alias}`}</Button>
<Button onClick={() => joinWithAlias()}>{t("PublicRooms.try_joining_alias", {alias: alias})}</Button>
)}
{status.isJoining && (
<>
<Spinner size="small" />
<Text>{`Joining ${alias}...`}</Text>
<Text>{t("PublicRooms.joining_alias", {alias: alias})}</Text>
</>
)}
{status.roomId !== null && (
<Button onClick={() => { onRequestClose(); selectRoom(status.roomId); }}>Open</Button>
<Button onClick={() => { onRequestClose(); selectRoom(status.roomId); }}>{t("common.open")}</Button>
)}
{status.error !== null && <Text variant="b2"><span style={{ color: 'var(--bg-danger)' }}>{status.error}</span></Text>}
</div>
@ -92,6 +98,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
const [searchQuery, updateSearchQuery] = useState({});
const [joiningRooms, updateJoiningRooms] = useState(new Set());
const { t } = useTranslation();
const roomNameRef = useRef(null);
const hsRef = useRef(null);
const userId = initMatrix.matrixClient.getUserId();
@ -140,14 +147,14 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
if (totalRooms.length === 0) {
updateSearchQuery({
error: inputRoomName === ''
? `No public rooms on ${inputHs}`
: `No result found for "${inputRoomName}" on ${inputHs}`,
? t("PublicRooms.no_public_rooms", {homeserver: inputHs})
: t("PublicRooms.no_result_found", {homeserver: inputHs, input: inputRoomName}),
alias: isInputAlias ? inputRoomName : null,
});
}
} catch (e) {
updatePublicRooms([]);
let err = 'Something went wrong!';
let err = t("errors.generic");
if (e?.httpStatus >= 400 && e?.httpStatus < 500) {
err = e.message;
}
@ -206,8 +213,8 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
desc={typeof room.topic === 'string' ? room.topic : null}
options={(
<>
{isJoined && <Button onClick={() => handleViewRoom(room.room_id)}>Open</Button>}
{!isJoined && (joiningRooms.has(room.room_id) ? <Spinner size="small" /> : <Button onClick={() => joinRoom(room.aliases?.[0] || room.room_id)} variant="primary">Join</Button>)}
{isJoined && <Button onClick={() => handleViewRoom(room.room_id)}>{t("common.open")}</Button>}
{!isJoined && (joiningRooms.has(room.room_id) ? <Spinner size="small" /> : <Button onClick={() => joinRoom(room.aliases?.[0] || room.room_id)} variant="primary">{t("commom.join")}</Button>)}
</>
)}
/>
@ -218,17 +225,17 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
return (
<PopupWindow
isOpen={isOpen}
title="Public rooms"
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
title={t("PublicRooms.title")}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose}
>
<div className="public-rooms">
<form className="public-rooms__form" onSubmit={(e) => { e.preventDefault(); searchRooms(); }}>
<div className="public-rooms__input-wrapper">
<Input value={searchTerm} forwardRef={roomNameRef} label="Room name or alias" />
<Input forwardRef={hsRef} value={userId.slice(userId.indexOf(':') + 1)} label="Homeserver" required />
<Input value={searchTerm} forwardRef={roomNameRef} label={t("PublicRooms.search_room_name_alias")} />
<Input forwardRef={hsRef} value={userId.slice(userId.indexOf(':') + 1)} label={t("common.homeserver")} required />
</div>
<Button disabled={isSearching} iconSrc={HashSearchIC} variant="primary" type="submit">Search</Button>
<Button disabled={isSearching} iconSrc={HashSearchIC} variant="primary" type="submit">{t("PublicRooms.search_button")}</Button>
</form>
<div className="public-rooms__search-status">
{
@ -237,13 +244,13 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
? (
<div className="flex--center">
<Spinner size="small" />
<Text variant="b2">{`Loading public rooms from ${searchQuery.homeserver}...`}</Text>
<Text variant="b2">{t("PublicRooms.loading", {homeserver: searchQuery.homeserver})}</Text>
</div>
)
: (
<div className="flex--center">
<Spinner size="small" />
<Text variant="b2">{`Searching for "${searchQuery.name}" on ${searchQuery.homeserver}...`}</Text>
<Text variant="b2">{t("PublicRooms.searching", {homeserver: searchQuery.homeserver, query: searchQuery.name})}</Text>
</div>
)
)
@ -251,8 +258,8 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
{
typeof searchQuery.name !== 'undefined' && !isSearching && (
searchQuery.name === ''
? <Text variant="b2">{`Public rooms on ${searchQuery.homeserver}.`}</Text>
: <Text variant="b2">{`Search result for "${searchQuery.name}" on ${searchQuery.homeserver}.`}</Text>
? <Text variant="b2">{t("PublicRooms.result_title", {homeserver: searchQuery.homeserver})}</Text>
: <Text variant="b2">{t("PublicRooms.search_result_title", {homeserver: searchQuery.homeserver, query: searchQuery.name})}</Text>
)
}
{ searchQuery.error && (
@ -272,7 +279,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
{ publicRooms.length !== 0 && publicRooms.length % SEARCH_LIMIT === 0 && (
<div className="public-rooms__view-more">
{ isViewMore !== true && (
<Button onClick={() => searchRooms(true)}>View more</Button>
<Button onClick={() => searchRooms(true)}>{t("commom.view_more")}</Button>
)}
{ isViewMore && <Spinner /> }
</div>

View file

@ -38,6 +38,10 @@ import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
const tabText = {
GENERAL: 'General',
SEARCH: 'Search',
@ -73,6 +77,8 @@ function GeneralSettings({ roomId }) {
const room = mx.getRoom(roomId);
const canInvite = room.canInvite(mx.getUserId());
const { t } = useTranslation();
return (
<>
<div className="room-settings__card">
@ -88,9 +94,9 @@ function GeneralSettings({ roomId }) {
variant="danger"
onClick={async () => {
const isConfirmed = await confirmDialog(
'Leave room',
`Are you sure that you want to leave "${room.name}" room?`,
'Leave',
t("RoomSettings.leave_room"),
t("RoomSettings.leave_room_confirm_message", {room_name: room.name}),
t("RoomSettings.leave_room_confirm_button"),
'danger',
);
if (!isConfirmed) return;
@ -102,15 +108,15 @@ function GeneralSettings({ roomId }) {
</MenuItem>
</div>
<div className="room-settings__card">
<MenuHeader>Notification (Changing this will only affect you)</MenuHeader>
<MenuHeader>{t("RoomSettings.notification_header")}</MenuHeader>
<RoomNotification roomId={roomId} />
</div>
<div className="room-settings__card">
<MenuHeader>Room visibility (who can join)</MenuHeader>
<MenuHeader>{t("RoomSettings.visibility_header")}</MenuHeader>
<RoomVisibility roomId={roomId} />
</div>
<div className="room-settings__card">
<MenuHeader>Room addresses</MenuHeader>
<MenuHeader>{t("RoomSettings.address_header")}</MenuHeader>
<RoomAliases roomId={roomId} />
</div>
</>
@ -125,11 +131,11 @@ function SecuritySettings({ roomId }) {
return (
<>
<div className="room-settings__card">
<MenuHeader>Encryption</MenuHeader>
<MenuHeader>{t("RoomSettings.encryption_header")}</MenuHeader>
<RoomEncryption roomId={roomId} />
</div>
<div className="room-settings__card">
<MenuHeader>Message history visibility</MenuHeader>
<MenuHeader>{t("RoomSettings.message_history_header")}</MenuHeader>
<RoomHistoryVisibility roomId={roomId} />
</div>
</>
@ -181,7 +187,7 @@ function RoomSettings({ roomId }) {
<TitleWrapper>
<Text variant="s1" weight="medium" primary>
{`${room.name}`}
<span style={{ color: 'var(--tc-surface-low)' }}> room settings</span>
<span style={{ color: 'var(--tc-surface-low)' }}> {t("RoomSettings.room_settings_subtitle")}</span>
</Text>
</TitleWrapper>
<RawIcon size="small" src={ChevronTopIC} />

View file

@ -29,6 +29,10 @@ import { parseTimelineChange } from './common';
import TimelineScroll from './TimelineScroll';
import EventLimit from './EventLimit';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
const PAG_LIMIT = 30;
const MAX_MSG_DIFF_MINUTES = 5;
const PLACEHOLDER_COUNT = 2;
@ -55,29 +59,39 @@ function RoomIntroContainer({ event, timeline }) {
const [, nameForceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient;
const { roomList } = initMatrix;
const { t } = useTranslation();
const { room } = timeline;
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
const isDM = roomList.directs.has(timeline.roomId);
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
const heading = isDM ? room.name : `Welcome to ${room.name}`;
const heading = isDM ? room.name : t("RoomViewContent.welcome_to_room", {room_name: room.name});
const topic = twemojify(roomTopic || '', undefined, true);
const nameJsx = twemojify(room.name);
const desc = isDM
? (
<>
This is the beginning of your direct message history with @
<b>{nameJsx}</b>
{'. '}
{topic}
<Trans
i18nKey={"RoomViewContent.beginning_dm"}
values={{user_name: nameJsx}}
components={{bold: <b/>}}
/>
{topic == "" ? "" : " - "}
{topic }
</>
)
: (
<>
{'This is the beginning of the '}
<b>{nameJsx}</b>
{' room. '}
<Trans
i18nKey={"RoomViewContent.beginning_room"}
values={{room_name: nameJsx}}
components={{bold: <b/>}}
/>
{topic == "" ? "" : " - "}
{topic}
</>
);
@ -98,7 +112,7 @@ function RoomIntroContainer({ event, timeline }) {
name={room.name}
heading={twemojify(heading)}
desc={desc}
time={event ? `Created at ${dateFormat(event.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
time={event ? t("RoomViewContent.created_on", {date: event.getDate(), formatParams: { date: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}}}) : null}
/>
);
}
@ -386,6 +400,8 @@ let jumpToItemIndex = -1;
function RoomViewContent({ eventId, roomTimeline }) {
const [throttle] = useState(new Throttle());
const { t } = useTranslation();
const timelineSVRef = useRef(null);
const timelineScrollRef = useRef(null);
const eventLimitRef = useRef(null);
@ -523,7 +539,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
&& readUptoEvent.getTs() < mEvent.getTs());
if (unreadDivider) {
isNewEvent = true;
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text="New messages" />);
tl.push(<Divider key={`new-${mEvent.getId()}`} variant="positive" text={t("RoomViewContent.new_messages")} />);
itemCountIndex += 1;
if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex;
}

View file

@ -3,6 +3,8 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './RoomViewFloating.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { markAsRead } from '../../../client/action/notifications';
@ -14,8 +16,14 @@ import MessageIC from '../../../../public/res/ic/outlined/message.svg';
import MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
import { getUsersActionJsx } from './common';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
function useJumpToEvent(roomTimeline) {
const [eventId, setEventId] = useState(null);
@ -86,32 +94,55 @@ function useScrollToBottom(roomTimeline) {
function RoomViewFloating({
roomId, roomTimeline,
}) {
const { t } = useTranslation();
const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline);
const [typingMembers] = useTypingMembers(roomTimeline);
const [isAtBottom, setIsAtBottom] = useScrollToBottom(roomTimeline);
const room = initMatrix.matrixClient.getRoom(roomId)
const getUserDisplayName = (userId) => {
console.log(userId);
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
return getUsername(userId);
};
const handleScrollToBottom = () => {
roomTimeline.emit(cons.events.roomTimeline.SCROLL_TO_LIVE);
setIsAtBottom(true);
};
console.log(typingMembers)
let typingMemberValues = [...typingMembers];
console.log(typingMemberValues)
return (
<>
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary">
<Text variant="b3" weight="medium">Jump to unread messages</Text>
<Text variant="b3" weight="medium">{t("RoomViewFloating.jump_unread")}</Text>
</Button>
<Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary">
<Text variant="b3" weight="bold">Mark as read</Text>
<Text variant="b3" weight="bold">{t("RoomViewFloating.mark_read")}</Text>
</Button>
</div>
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
<div className="bouncing-loader"><div /></div>
<Text variant="b2">{getUsersActionJsx(roomId, [...typingMembers], 'typing...')}</Text>
<Text variant="b2">
<Trans
i18nKey="RoomViewFloating.user_typing"
values={{count: typingMembers.size, user_one: getUserDisplayName(typingMemberValues?.[0]), user_two: getUserDisplayName(typingMemberValues?.[1]), user_three: getUserDisplayName(typingMemberValues?.[2]), user_four: getUserDisplayName(typingMemberValues?.[3])}}
components={{bold: <b/>}}
/>
</Text>
</div>
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}>
<Text variant="b3" weight="medium">Jump to latest</Text>
<Text variant="b3" weight="medium">{t("RoomViewFloating.jump_latest")}</Text>
</Button>
</div>
</>

View file

@ -29,6 +29,9 @@ import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomViewHeader({ roomId }) {
const [, forceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient;
@ -37,6 +40,8 @@ function RoomViewHeader({ roomId }) {
avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
const roomName = mx.getRoom(roomId).name;
const { t } = useTranslation();
const roomHeaderBtnRef = useRef(null);
useEffect(() => {
const settingsToggle = (isVisibile) => {
@ -93,17 +98,18 @@ function RoomViewHeader({ roomId }) {
</TitleWrapper>
<RawIcon src={ChevronBottomIC} />
</button>
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip={t("RoomViewHeader.search_tooltip")} src={SearchIC} />
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip={t("RoomViewHeader.people_tooltip")} src={UserIC} />
<IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip={t("RoomViewHeader.members_tooltip")} src={UserIC} />
<IconButton
onClick={openRoomOptions}
tooltip="Options"
tooltip={t("common.options")}
src={VerticalMenuIC}
/>
</Header>
);
}
RoomViewHeader.propTypes = {
roomId: PropTypes.string.isRequired,
};

View file

@ -30,6 +30,9 @@ import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
import FileIC from '../../../../public/res/ic/outlined/file.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const CMD_REGEX = /(^\/|:|@)(\S*)$/;
let isTyping = false;
let isCmdActivated = false;
@ -41,6 +44,8 @@ function RoomViewInput({
const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
const [replyTo, setReplyTo] = useState(null);
const { t } = useTranslation();
const textAreaRef = useRef(null);
const inputBaseRef = useRef(null);
const uploadInputRef = useRef(null);
@ -81,7 +86,7 @@ function RoomViewInput({
function uploadingProgress(myRoomId, { loaded, total }) {
if (myRoomId !== roomId) return;
const progressPer = Math.round((loaded * 100) / total);
uploadProgressRef.current.textContent = `Uploading: ${bytesToSize(loaded)}/${bytesToSize(total)} (${progressPer}%)`;
uploadProgressRef.current.textContent = t("RoomViewInput.upload_progress", {progress: bytesToSize(loaded), total:bytesToSize(total), percent: progressPer});
inputBaseRef.current.style.backgroundImage = `linear-gradient(90deg, var(--bg-surface-hover) ${progressPer}%, var(--bg-surface-low) ${progressPer}%)`;
}
function clearAttachment(myRoomId) {
@ -311,8 +316,8 @@ function RoomViewInput({
<Text className="room-input__alert">
{
tombstoneEvent
? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.'
: 'You do not have permission to post to this room'
? tombstoneEvent.getContent()?.body ?? t("RoomViewInput.tombstone_replaced")
: t("RoomViewInput.tombstone_permission_denied")
}
</Text>
);
@ -321,7 +326,7 @@ function RoomViewInput({
<>
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
<input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
<IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
<IconButton onClick={handleUploadClick} tooltip={t(attachment === null ? 'common.upload' : 'common.cancel')} src={CirclePlusIC} />
</div>
<div ref={inputBaseRef} className="room-input__input-container">
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
@ -333,7 +338,7 @@ function RoomViewInput({
onChange={handleMsgTyping}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
placeholder="Send a message..."
placeholder={t("RoomViewInput.send_message_placeholder")}
/>
</Text>
</ScrollView>
@ -347,10 +352,10 @@ function RoomViewInput({
cords.y -= 250;
openEmojiBoard(cords, addEmoji);
}}
tooltip="Emoji"
tooltip={t("RoomViewInput.emoji_tooltip")}
src={EmojiIC}
/>
<IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
<IconButton onClick={sendMessage} tooltip={t("common.send")} src={SendIC} />
</div>
</>
);
@ -368,7 +373,7 @@ function RoomViewInput({
</div>
<div className="room-attachment__info">
<Text variant="b1">{attachment.name}</Text>
<Text variant="b3"><span ref={uploadProgressRef}>{`size: ${bytesToSize(attachment.size)}`}</span></Text>
<Text variant="b3"><span ref={uploadProgressRef}>{t("RoomViewInput.file_size", {size: bytesToSize(attachment.size)})}</span></Text>
</div>
</div>
);
@ -383,7 +388,7 @@ function RoomViewInput({
setReplyTo(null);
}}
src={CrossIC}
tooltip="Cancel reply"
tooltip={t("RoomViewInput.cancel_reply_tooltip")}
size="extra-small"
/>
<MessageReply

View file

@ -5,13 +5,22 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
function getTimelineJSXMessages() {
return {
join(user) {
return (
<>
<b>{twemojify(user)}</b>
{' joined the room'}
<Trans
i18nKey={"RoomCommon.user_joined"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</>
);
},
@ -19,118 +28,145 @@ function getTimelineJSXMessages() {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
return (
<>
<b>{twemojify(user)}</b>
{' left the room'}
{twemojify(reasonMsg)}
<Trans
i18nKey={"RoomCommon.user_left"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</>
);
},
invite(inviter, user) {
return (
<>
<b>{twemojify(inviter)}</b>
{' invited '}
<b>{twemojify(user)}</b>
<Trans
i18nKey={"RoomCommon.user_invited"}
values={{user_name: twemojify(user), inviter_name: twemojify(inviter)}}
components={{bold: <b/>}}
/>
</>
);
},
cancelInvite(inviter, user) {
return (
<>
<b>{twemojify(inviter)}</b>
{' canceled '}
<b>{twemojify(user)}</b>
{'\'s invite'}
<Trans
i18nKey={"RoomCommon.invite_cancelled"}
values={{user_name: twemojify(user), inviter_name: twemojify(inviter)}}
components={{bold: <b/>}}
/>
</>
);
},
rejectInvite(user) {
return (
<>
<b>{twemojify(user)}</b>
{' rejected the invitation'}
<Trans
i18nKey={"RoomCommon.invite_rejected"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</>
);
},
kick(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
const reasonMsg = (typeof reason === 'string') ? `${reason}` : '';
return (
<>
<b>{twemojify(actor)}</b>
{' kicked '}
<b>{twemojify(user)}</b>
{twemojify(reasonMsg)}
<Trans
i18nKey={"RoomCommon.user_kicked"}
values={{user_name: twemojify(user), actor: twemojify(actor), reason: twemojify(reasonMsg)}}
components={{bold: <b/>}}
/>
</>
);
},
ban(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
const reasonMsg = (typeof reason === 'string') ? `${reason}` : '';
return (
<>
<b>{twemojify(actor)}</b>
{' banned '}
<b>{twemojify(user)}</b>
{twemojify(reasonMsg)}
<Trans
i18nKey={"RoomCommon.user_banned"}
values={{user_name: twemojify(user), actor: twemojify(actor), reason: twemojify(reasonMsg)}}
components={{bold: <b/>}}
/>
</>
);
},
unban(actor, user) {
return (
<>
<b>{twemojify(actor)}</b>
{' unbanned '}
<b>{twemojify(user)}</b>
<Trans
i18nKey={"RoomCommon.user_unbanned"}
values={{user_name: twemojify(user), actor: twemojify(actor)}}
components={{bold: <b/>}}
/>
</>
);
},
avatarSets(user) {
return (
<>
<b>{twemojify(user)}</b>
{' set a avatar'}
<Trans
i18nKey={"RoomCommon.avatar_set"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</>
);
},
avatarChanged(user) {
return (
<>
<b>{twemojify(user)}</b>
{' changed their avatar'}
<Trans
i18nKey={"RoomCommon.avatar_changed"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</>
);
},
avatarRemoved(user) {
return (
<>
<b>{twemojify(user)}</b>
{' removed their avatar'}
<Trans
i18nKey={"RoomCommon.avatar_removed"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</>
);
},
nameSets(user, newName) {
return (
<>
<b>{twemojify(user)}</b>
{' set display name to '}
<b>{twemojify(newName)}</b>
<Trans
i18nKey={"RoomCommon.name_set"}
values={{user_name: twemojify(user), new_name: twemojify(newName)}}
components={{bold: <b/>}}
/>
</>
);
},
nameChanged(user, newName) {
return (
<>
<b>{twemojify(user)}</b>
{' changed their display name to '}
<b>{twemojify(newName)}</b>
<Trans
i18nKey={"RoomCommon.name_changed"}
values={{user_name: twemojify(user), new_name: twemojify(newName)}}
components={{bold: <b/>}}
/>
</>
);
},
nameRemoved(user, lastName) {
return (
<>
<b>{twemojify(user)}</b>
{' removed their display name '}
<b>{twemojify(lastName)}</b>
<Trans
i18nKey={"RoomCommon.name_removed"}
values={{user_name: twemojify(user), new_name: twemojify(newName)}}
components={{bold: <b/>}}
/>
</>
);
},

View file

@ -20,6 +20,9 @@ import RoomSelector from '../../molecules/room-selector/RoomSelector';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function useVisiblityToggle(setResult) {
const [isOpen, setIsOpen] = useState(false);
@ -81,6 +84,8 @@ function Search() {
const searchRef = useRef(null);
const mx = initMatrix.matrixClient;
const { t } = useTranslation();
const handleSearchResults = (chunk, term) => {
setResult({
term,
@ -212,7 +217,7 @@ function Search() {
<Input
onChange={handleOnChange}
forwardRef={searchRef}
placeholder="Search"
placeholder={t("common.search")}
/>
<IconButton size="small" src={CrossIC} type="reset" onClick={handleCross} tabIndex={-1} />
</form>
@ -224,7 +229,7 @@ function Search() {
</ScrollView>
</div>
<div className="search-dialog__footer">
<Text variant="b3">Type # for rooms, @ for DMs and * for spaces. Hotkey: Ctrl + k</Text>
<Text variant="b3">{t("Search.description")}</Text>
</div>
</div>
</RawModal>

View file

@ -12,6 +12,9 @@ import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
let lastUsedPassword;
const getAuthId = (password) => ({
type: 'm.login.password',
@ -25,6 +28,7 @@ const getAuthId = (password) => ({
function AuthRequest({ onComplete, makeRequest }) {
const [status, setStatus] = useState(false);
const mountStore = useStore();
const { t } = useTranslation();
const handleForm = async (e) => {
mountStore.setItem(true);
@ -41,10 +45,10 @@ function AuthRequest({ onComplete, makeRequest }) {
lastUsedPassword = undefined;
if (!mountStore.getItem()) return;
if (err.errcode === 'M_FORBIDDEN') {
setStatus({ error: 'Wrong password. Please enter correct password.' });
setStatus({ error: t("AuthRequest.wrong_password") });
return;
}
setStatus({ error: 'Request failed!' });
setStatus({ error: t("AuthRequest.request_failed") });
}
};
@ -57,14 +61,14 @@ function AuthRequest({ onComplete, makeRequest }) {
<form onSubmit={handleForm}>
<Input
name="password"
label="Account password"
label={t("AuthRequest.password_label")}
type="password"
onChange={handleChange}
required
/>
{status.ongoing && <Spinner size="small" />}
{status.error && <Text variant="b3">{status.error}</Text>}
{(status === false || status.error) && <Button variant="primary" type="submit" disabled={!!status.error}>Continue</Button>}
{(status === false || status.error) && <Button variant="primary" type="submit" disabled={!!status.error}>{t("common.continue")}</Button>}
</form>
</div>
);

View file

@ -19,22 +19,34 @@ import SettingTile from '../../molecules/setting-tile/SettingTile';
import { authRequest } from './AuthRequest';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
const failedDialog = () => {
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function CrossSigningSetup() {
const { t } = useTranslation();
const initialValues = { phrase: '', confirmPhrase: '' };
const [genWithPhrase, setGenWithPhrase] = useState(undefined);
const failedDialog = () => {
const renderFailure = (requestClose) => (
<div className="cross-signing__failure">
<Text variant="h1">{twemojify('❌')}</Text>
<Text weight="medium">Failed to setup cross signing. Please try again.</Text>
<Button onClick={requestClose}>Close</Button>
<Text weight="medium">{t("CrossSigning.setup_failed")}</Text>
<Button onClick={requestClose}>{t("common.close")}</Button>
</div>
);
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
<Text variant="s1" weight="medium">{t("CrossSigning.setup")}</Text>,
renderFailure,
);
};
};
const securityKeyDialog = (key) => {
const securityKeyDialog = (key) => {
const downloadKey = () => {
const blob = new Blob([key.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
@ -47,13 +59,13 @@ const securityKeyDialog = (key) => {
const renderSecurityKey = () => (
<div className="cross-signing__key">
<Text weight="medium">Please save this security key somewhere safe.</Text>
<Text weight="medium">{t("CrossSigning.save_security_key_message")}</Text>
<Text className="cross-signing__key-text">
{key.encodedPrivateKey}
</Text>
<div className="cross-signing__key-btn">
<Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
<Button onClick={() => downloadKey(key)}>Download</Button>
<Button variant="primary" onClick={() => copyKey(key)}>{t("common.copy")}</Button>
<Button onClick={() => downloadKey(key)}>{t("common.download")}</Button>
</div>
</div>
);
@ -62,14 +74,11 @@ const securityKeyDialog = (key) => {
downloadKey();
openReusableDialog(
<Text variant="s1" weight="medium">Security Key</Text>,
<Text variant="s1" weight="medium">{t("CrossSigning.security_key_dialog_title")}</Text>,
() => renderSecurityKey(),
);
};
};
function CrossSigningSetup() {
const initialValues = { phrase: '', confirmPhrase: '' };
const [genWithPhrase, setGenWithPhrase] = useState(undefined);
const setup = async (securityPhrase = undefined) => {
const mx = initMatrix.matrixClient;
@ -121,13 +130,12 @@ function CrossSigningSetup() {
<div className="cross-signing__setup">
<div className="cross-signing__setup-entry">
<Text>
We will generate a <b>Security Key</b>,
which you can use to manage messages backup and session verification.
{t("CrossSigning.security_key_generation_message")}
</Text>
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>{t("CrossSigning.security_key_generation_button")}</Button>}
{genWithPhrase === false && <Spinner size="small" />}
</div>
<Text className="cross-signing__setup-divider">OR</Text>
<Text className="cross-signing__setup-divider">{t("common.or")}</Text>
<Formik
initialValues={initialValues}
onSubmit={(values) => setup(values.phrase)}
@ -142,15 +150,13 @@ function CrossSigningSetup() {
disabled={genWithPhrase !== undefined}
>
<Text>
Alternatively you can also set a <b>Security Phrase </b>
so you don't have to remember long Security Key,
and optionally save the Key as backup.
{t("CrossSigning.security_phrase_message")}
</Text>
<Input
name="phrase"
value={values.phrase}
onChange={handleChange}
label="Security Phrase"
label={t("CrossSigning.security_phrase_label")}
type="password"
required
disabled={genWithPhrase !== undefined}
@ -160,13 +166,13 @@ function CrossSigningSetup() {
name="confirmPhrase"
value={values.confirmPhrase}
onChange={handleChange}
label="Confirm Security Phrase"
label={t("CrossSigning.security_phrase_confirm_label")}
type="password"
required
disabled={genWithPhrase !== undefined}
/>
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>{t("CrossSigning.security_phrase_set_button")}</Button>}
{genWithPhrase === true && <Spinner size="small" />}
</form>
)}
@ -180,41 +186,41 @@ const setupDialog = () => {
<Text variant="s1" weight="medium">Setup cross signing</Text>,
() => <CrossSigningSetup />,
);
};
}
function CrossSigningReset() {
const { t } = useTranslation();
return (
<div className="cross-signing__reset">
<Text variant="h1">{twemojify('✋🧑‍🚒🤚')}</Text>
<Text weight="medium">Resetting cross-signing keys is permanent.</Text>
<Text weight="medium">{t("CrossSigning.reset_keys_subtitle")}</Text>
<Text>
Anyone you have verified with will see security alerts and your message backup will lost.
You almost certainly do not want to do this,
unless you have lost <b>Security Key</b> or <b>Phrase</b> and
every session you can cross-sign from.
{t("CrossSigning.reset_keys_message")}
</Text>
<Button variant="danger" onClick={setupDialog}>Reset</Button>
<Button variant="danger" onClick={setupDialog}>{t("common.reset")}</Button>
</div>
);
}
const resetDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Reset cross signing</Text>,
() => <CrossSigningReset />,
);
};
}
function CrossSignin() {
const { t } = useTranslation();
const isCSEnabled = useCrossSigningStatus();
return (
<SettingTile
title="Cross signing"
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
title={t("CrossSigning.title")}
content={<Text variant="b3">{t("CrossSigning.setup_message")}</Text>}
options={(
isCSEnabled
? <Button variant="danger" onClick={resetDialog}>Reset</Button>
: <Button variant="primary" onClick={setupDialog}>Setup</Button>
? <Button variant="danger" onClick={resetDialog}>{t("common.reset")}</Button>
: <Button variant="primary" onClick={setupDialog}>{t("common.setup")}</Button>
)}
/>
);

View file

@ -27,9 +27,15 @@ import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import { accessSecretStorage } from './SecretStorageAccess';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false;
const { t } = useTranslation();
const renderContent = (onComplete) => {
const handleSubmit = (e) => {
e.preventDefault();
@ -39,17 +45,17 @@ const promptDeviceName = async (deviceName) => new Promise((resolve) => {
};
return (
<form className="device-manage__rename" onSubmit={handleSubmit}>
<Input value={deviceName} label="Session name" name="session" />
<Input value={deviceName} label={t("DeviceManage.edit_session_name_subtitle")} name="session" />
<div className="device-manage__rename-btn">
<Button variant="primary" type="submit">Save</Button>
<Button onClick={() => onComplete(null)}>Cancel</Button>
<Button variant="primary" type="submit">{t("common.save")}</Button>
<Button onClick={() => onComplete(null)}>{t("common.cancel")}</Button>
</div>
</form>
);
};
openReusableDialog(
<Text variant="s1" weight="medium">Edit session name</Text>,
<Text variant="s1" weight="medium">{t("DeviceManage.edit_session_name_title")}</Text>,
(requestClose) => renderContent((name) => {
isCompleted = true;
resolve(name);
@ -76,6 +82,41 @@ function DeviceManage() {
setProcessing([]);
}, [deviceList]);
const { t } = useTranslation();
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false;
const renderContent = (onComplete) => {
const handleSubmit = (e) => {
e.preventDefault();
const name = e.target.session.value;
if (typeof name !== 'string') onComplete(null);
onComplete(name);
};
return (
<form className="device-manage__rename" onSubmit={handleSubmit}>
<Input value={deviceName} label={t("DeviceManage.edit_session_name_subtitle")} name="session" />
<div className="device-manage__rename-btn">
<Button variant="primary" type="submit">{t("common.save")}</Button>
<Button onClick={() => onComplete(null)}>{t("common.cancel")}</Button>
</div>
</form>
);
};
openReusableDialog(
<Text variant="s1" weight="medium">{t("DeviceManage.edit_session_name_title")}</Text>,
(requestClose) => renderContent((name) => {
isCompleted = true;
resolve(name);
requestClose();
}),
() => {
if (!isCompleted) resolve(null);
},
);
});
const addToProcessing = (device) => {
const old = [...processing];
old.push(device.device_id);
@ -91,7 +132,7 @@ function DeviceManage() {
<div className="device-manage">
<div className="device-manage__loading">
<Spinner size="small" />
<Text>Loading devices...</Text>
<Text>{t("DeviceManage.loading_devices")}</Text>
</div>
</div>
);
@ -114,14 +155,14 @@ function DeviceManage() {
const handleRemove = async (device) => {
const isConfirmed = await confirmDialog(
`Logout ${device.display_name}`,
`You are about to logout "${device.display_name}" session.`,
'Logout',
t("DeviceManage.logout_device_title", {device: device.display_name}),
t("DeviceManage.logout_device_message", {device: device.display_name}),
t("DeviceManage.logout_device_confirm"),
'danger',
);
if (!isConfirmed) return;
addToProcessing(device);
await authRequest(`Logout "${device.display_name}"`, async (auth) => {
await authRequest(t("DeviceManage.logout_device_title", {device: device.display_name}), async (auth) => {
await mx.deleteDevice(device.device_id, auth);
});
@ -130,7 +171,7 @@ function DeviceManage() {
};
const verifyWithKey = async (device) => {
const keyData = await accessSecretStorage('Session verification');
const keyData = await accessSecretStorage(t("DeviceManage.session_verification_title"));
if (!keyData) return;
addToProcessing(device);
await mx.checkOwnCrossSigningTrust();
@ -164,7 +205,7 @@ function DeviceManage() {
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text>
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">Current</Text>}
{isCurrentDevice && <Text span className="device-manage__current-label" variant="b3">{t("DeviceManage.current_device_label")}</Text>}
</Text>
)}
options={
@ -172,9 +213,9 @@ function DeviceManage() {
? <Spinner size="small" />
: (
<>
{(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>}
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
{(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">{t("DeviceManage.verify_session_button")}</Button>}
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip={t("DeviceManage.edit_session_name_tooltip")} />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip={t("DeviceManage.logout_device_tooltip")}/>
</>
)
}
@ -211,49 +252,50 @@ function DeviceManage() {
noEncryption.push(device);
}
});
return (
<div className="device-manage">
<div>
<MenuHeader>Unverified sessions</MenuHeader>
<MenuHeader>{t("DeviceManage.unverified_sessions_title")}</MenuHeader>
{!isCSEnabled && (
<div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
<InfoCard
rounded
variant="caution"
iconSrc={InfoIC}
title="Setup cross signing in case you lose all your sessions."
title={t("DeviceManage.setup_cross_signing_message")}
/>
</div>
)}
{
unverified.length > 0
? unverified.map((device) => renderDevice(device, false))
: <Text className="device-manage__info">No unverified sessions</Text>
: <Text className="device-manage__info">{t("DeviceManage.unverified_sessions_none")}</Text>
}
</div>
{noEncryption.length > 0 && (
<div>
<MenuHeader>Sessions without encryption support</MenuHeader>
<MenuHeader>{t("DeviceManage.unencrypted_sessions_title")}</MenuHeader>
{noEncryption.map((device) => renderDevice(device, null))}
</div>
)}
<div>
<MenuHeader>Verified sessions</MenuHeader>
<MenuHeader>{t("DeviceManage.verified_sessions_title")}</MenuHeader>
{
verified.length > 0
? verified.map((device, index) => {
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
: <Text className="device-manage__info">No verified sessions</Text>
: <Text className="device-manage__info">{t("DeviceManage.verified_sessions_none")}</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
{truncated ? `View ${verified.length - 4} more` : 'View less'}
{t(truncated ? "common.view_more" : "common.view_less")}
</Button>
)}
{ deviceList.length > 0 && (
<Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
<Text className="device-manage__info" variant="b3">{t("DeviceManage.session_name_privacy_message")}</Text>
)}
</div>
</div>

View file

@ -24,11 +24,17 @@ import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
import { useStore } from '../../hooks/useStore';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function CreateKeyBackupDialog({ keyData }) {
const [done, setDone] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const { t } = useTranslation();
const doBackup = async () => {
setDone(false);
let info;
@ -60,19 +66,19 @@ function CreateKeyBackupDialog({ keyData }) {
{done === false && (
<div>
<Spinner size="small" />
<Text>Creating backup...</Text>
<Text>{t("KeyBackup.creating_backup")}</Text>
</div>
)}
{done === true && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text>Successfully created backup</Text>
<Text>{t("KeyBackup.backup_created")}</Text>
</>
)}
{done === null && (
<>
<Text>Failed to create backup</Text>
<Button onClick={doBackup}>Retry</Button>
<Text>{t("KeyBackup.backup_failed")}</Text>
<Button onClick={doBackup}>{t("common.retry")}</Button>
</>
)}
</div>
@ -83,6 +89,9 @@ CreateKeyBackupDialog.propTypes = {
};
function RestoreKeyBackupDialog({ keyData }) {
const { t } = useTranslation();
const [status, setStatus] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
@ -99,7 +108,7 @@ function RestoreKeyBackupDialog({ keyData }) {
meBreath = true;
}, 200);
setStatus({ message: `Restoring backup keys... (${progress.successes}/${progress.total})` });
setStatus({ message: t("KeyBackup.restoring_progress", {progress: progress.successes, total: progress.total}) });
};
try {
@ -111,14 +120,14 @@ function RestoreKeyBackupDialog({ keyData }) {
{ progressCallback },
);
if (!mountStore.getItem()) return;
setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
setStatus({ done: t("KeyBackup.restore_complete", {progress: info.imported, total: info.total})});
} catch (e) {
if (!mountStore.getItem()) return;
if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') {
deletePrivateKey(keyData.keyId);
setStatus({ error: 'Failed to restore backup. Key is invalid!', errorCode: 'BAD_KEY' });
setStatus({ error: t("KeyBackup.restore_failed_bad_key"), errorCode: 'BAD_KEY' });
} else {
setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' });
setStatus({ error: t("KeyBackup.restore_failed_unknown"), errCode: 'UNKNOWN' });
}
}
};
@ -133,7 +142,7 @@ function RestoreKeyBackupDialog({ keyData }) {
{(status === false || status.message) && (
<div>
<Spinner size="small" />
<Text>{status.message ?? 'Restoring backup keys...'}</Text>
<Text>{status.message ?? t("KeyBackup.restoring")}</Text>
</div>
)}
{status.done && (
@ -145,7 +154,7 @@ function RestoreKeyBackupDialog({ keyData }) {
{status.error && (
<>
<Text>{status.error}</Text>
<Button onClick={restoreBackup}>Retry</Button>
<Button onClick={restoreBackup}>{t("common.retry")}}</Button>
</>
)}
</div>
@ -159,6 +168,7 @@ function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const { t } = useTranslation();
const deleteBackup = async () => {
mountStore.setItem(true);
@ -177,12 +187,12 @@ function DeleteKeyBackupDialog({ requestClose }) {
return (
<div className="key-backup__delete">
<Text variant="h1">{twemojify('🗑')}</Text>
<Text weight="medium">Deleting key backup is permanent.</Text>
<Text>All encrypted messages keys stored on server will be deleted.</Text>
<Text weight="medium">{t("KeyBackup.delete_key_backup_subtitle")}</Text>
<Text>{t("KeyBackup.delete_key_backup_message")}</Text>
{
isDeleting
? <Spinner size="small" />
: <Button variant="danger" onClick={deleteBackup}>Delete</Button>
: <Button variant="danger" onClick={deleteBackup}>{t("common.delete")}</Button>
}
</div>
);
@ -196,6 +206,7 @@ function KeyBackup() {
const isCSEnabled = useCrossSigningStatus();
const [keyBackup, setKeyBackup] = useState(undefined);
const mountStore = useStore();
const { t } = useTranslation();
const fetchKeyBackupVersion = async () => {
const info = await mx.getKeyBackupVersion();
@ -220,28 +231,28 @@ function KeyBackup() {
}, [isCSEnabled]);
const openCreateKeyBackup = async () => {
const keyData = await accessSecretStorage('Create Key Backup');
const keyData = await accessSecretStorage(t('KeyBackup.create_backup_title'));
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Create Key Backup</Text>,
<Text variant="s1" weight="medium">{t('KeyBackup.create_backup_title')}</Text>,
() => <CreateKeyBackupDialog keyData={keyData} />,
() => fetchKeyBackupVersion(),
);
};
const openRestoreKeyBackup = async () => {
const keyData = await accessSecretStorage('Restore Key Backup');
const keyData = await accessSecretStorage(t('KeyBackup.restore_backup_title'));
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Restore Key Backup</Text>,
<Text variant="s1" weight="medium">{t('KeyBackup.restore_backup_title')}</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} />,
);
};
const openDeleteKeyBackup = () => openReusableDialog(
<Text variant="s1" weight="medium">Delete Key Backup</Text>,
<Text variant="s1" weight="medium">{t('KeyBackup.delete_key_backup_title')}</Text>,
(requestClose) => (
<DeleteKeyBackupDialog
requestClose={(isDone) => {
@ -254,28 +265,28 @@ function KeyBackup() {
const renderOptions = () => {
if (keyBackup === undefined) return <Spinner size="small" />;
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>{t('KeyBackup.create_backup_tooltip')}</Button>;
return (
<>
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip={t('KeyBackup.restore_backup_tooltip')} />
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip={t('KeyBackup.delete_key_backup_tooltip')} />
</>
);
};
return (
<SettingTile
title="Encrypted messages backup"
title={t("KeyBackup.encrypted_messages_backup_title")}
content={(
<>
<Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
<Text variant="b3">{t("KeyBackup.encrypted_messages_backup_description")}</Text>
{!isCSEnabled && (
<InfoCard
style={{ marginTop: 'var(--sp-ultra-tight)' }}
rounded
variant="caution"
iconSrc={InfoIC}
title="Setup cross signing to backup your encrypted messages."
title={t("KeyBackup.encrypted_messages_backup_cross_signing_disabled")}
/>
)}
</>

View file

@ -13,6 +13,11 @@ import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { useStore } from '../../hooks/useStore';
function SecretStorageAccess({ onComplete }) {
@ -24,6 +29,7 @@ function SecretStorageAccess({ onComplete }) {
const [process, setProcess] = useState(false);
const [error, setError] = useState(null);
const mountStore = useStore();
const { t } = useTranslation();
const toggleWithPhrase = () => setWithPhrase(!withPhrase);
@ -39,7 +45,7 @@ function SecretStorageAccess({ onComplete }) {
if (!mountStore.getItem()) return;
if (!isCorrect) {
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
setError(t(key ? "SecretStorageAccess.incorrect_security_key" : "SecretStorageAccess.incorrect_security_phrase"));
setProcess(false);
return;
}
@ -51,7 +57,7 @@ function SecretStorageAccess({ onComplete }) {
});
} catch (e) {
if (!mountStore.getItem()) return;
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
setError(t(key ? "SecretStorageAccess.incorrect_security_key" : "SecretStorageAccess.incorrect_security_phrase"));
setProcess(false);
}
};
@ -76,7 +82,7 @@ function SecretStorageAccess({ onComplete }) {
<form onSubmit={handleForm}>
<Input
name="password"
label={`Security ${withPhrase ? 'Phrase' : 'Key'}`}
label={t(withPhrase ? "SecretStorageAccess.security_phrase" : "SecretStorageAccess.security_key")}
type="password"
onChange={handleChange}
required
@ -84,8 +90,8 @@ function SecretStorageAccess({ onComplete }) {
{error && <Text variant="b3">{error}</Text>}
{!process && (
<div className="secret-storage-access__btn">
<Button variant="primary" type="submit">Continue</Button>
{isPassphrase && <Button onClick={toggleWithPhrase}>{`Use Security ${withPhrase ? 'Key' : 'Phrase'}`}</Button>}
<Button variant="primary" type="submit">{t("common.continue")}</Button>
{isPassphrase && <Button onClick={toggleWithPhrase}>{t( withPhrase ? "SecretStorageAccess.use_security_key" : "SecretStorageAccess.use_security_phrase")}</Button>}
</div>
)}
</form>

View file

@ -40,34 +40,39 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import CinnySVG from '../../../../public/res/svg/cinny.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function AppearanceSection() {
const [, updateState] = useState({});
const { t } = useTranslation();
return (
<div className="settings-appearance">
<div className="settings-appearance__card">
<MenuHeader>Theme</MenuHeader>
<SettingTile
title="Follow system theme"
title={t("Settings.theme.follow_system.title")}
options={(
<Toggle
isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }}
/>
)}
content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
content={<Text variant="b3">{t("Settings.theme.follow_system.description")}</Text>}
/>
{!settings.useSystemTheme && (
<SettingTile
title="Theme"
title={t("Settings.theme.title")}
content={(
<SegmentedControls
selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
{ text: t("Settings.theme.theme_light") },
{ text: t("Settings.theme.theme_silver") },
{ text: t("Settings.theme.theme_dark") },
{ text: t("Settings.theme.theme_butter") },
]}
onSelect={(index) => settings.setTheme(index)}
/>
@ -78,34 +83,34 @@ function AppearanceSection() {
<div className="settings-appearance__card">
<MenuHeader>Room messages</MenuHeader>
<SettingTile
title="Markdown formatting"
title={t("Settings.markdown.title")}
options={(
<Toggle
isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }}
/>
)}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
content={<Text variant="b3">{t("Settings.markdown.description")}</Text>}
/>
<SettingTile
title="Hide membership events"
title={t("Settings.hide_membership_events.title")}
options={(
<Toggle
isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
content={<Text variant="b3">{t("Settings.hide_membership_events.description")}</Text>}
/>
<SettingTile
title="Hide nick/avatar events"
title={t("Settings.hide_nickname_avatar_events.title")}
options={(
<Toggle
isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
/>
)}
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
content={<Text variant="b3">{t("Settings.hide_nickname_avatar_events.description")}</Text>}
/>
</div>
</div>
@ -117,9 +122,11 @@ function NotificationsSection() {
const [, updateState] = useState({});
const { t } = useTranslation();
const renderOptions = () => {
if (window.Notification === undefined) {
return <Text className="settings-notifications__not-supported">Not supported in this browser.</Text>;
return <Text className="settings-notifications__not-supported">{t("errors.browser_not_supported")}</Text>;
}
if (permission === 'granted') {
@ -147,51 +154,54 @@ function NotificationsSection() {
return (
<div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader>
<MenuHeader>{t("Settings.notifications_and_sound.title")}</MenuHeader>
<SettingTile
title="Desktop notification"
title={t("Settings.notifications_and_sound.desktop.title")}
options={renderOptions()}
content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
content={<Text variant="b3">{t("Settings.notifications_and_sound.desktop.description")}</Text>}
/>
<SettingTile
title="Notification Sound"
title={t("Settings.notifications_and_sound.sound.title")}
options={(
<Toggle
isActive={settings.isNotificationSounds}
onToggle={() => { toggleNotificationSounds(); updateState({}); }}
/>
)}
content={<Text variant="b3">Play sound when new messages arrive.</Text>}
content={<Text variant="b3">{t("Settings.notifications_and_sound.desktop.description")}</Text>}
/>
</div>
);
}
function SecuritySection() {
const { t } = useTranslation();
return (
<div className="settings-security">
<div className="settings-security__card">
<MenuHeader>Cross signing and backup</MenuHeader>
<MenuHeader>{t("Settings.security.cross_signing.title")}</MenuHeader>
<CrossSigning />
<KeyBackup />
</div>
<DeviceManage />
<div className="settings-security__card">
<MenuHeader>Export/Import encryption keys</MenuHeader>
<MenuHeader>{t("Settings.security.export_import_encryption_keys.title")}</MenuHeader>
<SettingTile
title="Export E2E room keys"
title={t("Settings.security.export_encryption_keys.title")}
content={(
<>
<Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
<Text variant="b3">{t("Settings.security.export_encryption_keys.description")}</Text>
<ExportE2ERoomKeys />
</>
)}
/>
<SettingTile
title="Import E2E room keys"
title={t("Settings.security.import_encryption_keys.title")}
content={(
<>
<Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
<Text variant="b3">{t("Settings.security.import_encryption_keys.description")}</Text>
<ImportE2ERoomKeys />
</>
)}
@ -202,28 +212,31 @@ function SecuritySection() {
}
function AboutSection() {
const { t } = useTranslation();
return (
<div className="settings-about">
<div className="settings-about__card">
<MenuHeader>Application</MenuHeader>
<MenuHeader>{t("Settings.about.application")}</MenuHeader>
<div className="settings-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div>
<Text variant="h2" weight="medium">
Cinny
{t("common.cinny")}
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text>
<Text>Yet another matrix client</Text>
<Text>{t("common.slogan")}</Text>
<div className="settings-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>{t("common.source_code")}</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>{t("common.sponsor")}</Button>
</div>
</div>
</div>
</div>
<div className="settings-about__card">
<MenuHeader>Credits</MenuHeader>
<MenuHeader>{t("Settings.about.credits")}</MenuHeader>
<div className="settings-about__credits">
<ul>
<li>
@ -297,9 +310,11 @@ function Settings() {
const [selectedTab, setSelectedTab] = useState(tabItems[0]);
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
const { t } = useTranslation();
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
const handleLogout = async () => {
if (await confirmDialog('Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger')) {
if (await confirmDialog(t("Settings.logout.dialog.title"), t("Settings.logout.dialog.description"), t("Settings.logout.dialog.confirm"), 'danger')) {
logout();
}
};
@ -308,13 +323,13 @@ function Settings() {
<PopupWindow
isOpen={isOpen}
className="settings-window"
title={<Text variant="s1" weight="medium" primary>Settings</Text>}
title={<Text variant="s1" weight="medium" primary>{t("Settings.title")}</Text>}
contentOptions={(
<>
<Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
Logout
{t("Settings.logout.title")}
</Button>
<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
<IconButton src={CrossIC} onClick={requestClose} tooltip={t("common.close")} />
</>
)}
onRequestClose={requestClose}

View file

@ -22,6 +22,10 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useSpaceShortcut } from '../../hooks/useSpaceShortcut';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ShortcutSpacesContent() {
const mx = initMatrix.matrixClient;
const { spaces, roomIdToParents } = initMatrix.roomList;
@ -73,6 +77,8 @@ function ShortcutSpacesContent() {
const toggleSelected = () => toggleSelection(spaceId);
const deleteShortcut = () => deleteSpaceShortcut(spaceId);
const { t } = useTranslation();
return (
<RoomSelector
key={spaceId}
@ -105,20 +111,22 @@ function ShortcutSpacesContent() {
);
};
const { t } = useTranslation();
return (
<>
<Text className="shortcut-spaces__header" variant="b3" weight="bold">Pinned spaces</Text>
{spaceShortcut.length === 0 && <Text>No pinned spaces</Text>}
<Text className="shortcut-spaces__header" variant="b3" weight="bold">{t("ShortcutSpaces.pinned_spaces")}</Text>
{spaceShortcut.length === 0 && <Text>{t("ShortcutSpaces.no_pinned_spaces")}</Text>}
{spaceShortcut.map((spaceId) => renderSpace(spaceId, true))}
<Text className="shortcut-spaces__header" variant="b3" weight="bold">Unpinned spaces</Text>
{spaceWithoutShortcut.length === 0 && <Text>No unpinned spaces</Text>}
<Text className="shortcut-spaces__header" variant="b3" weight="bold">{t("ShortcutSpaces.unpinned_spaces")}</Text>
{spaceWithoutShortcut.length === 0 && <Text>{t("ShortcutSpaces.no_unpinned_spaces")}</Text>}
{spaceWithoutShortcut.map((spaceId) => renderSpace(spaceId, false))}
{selected.length !== 0 && (
<div className="shortcut-spaces__footer">
{process && <Spinner size="small" />}
<Text weight="medium">{process || `${selected.length} spaces selected`}</Text>
<Text weight="medium">{process || t("ShortcutSpaces.spaces_selected", {count: selected.length})}</Text>
{ !process && (
<Button onClick={handleAdd} variant="primary">Pin</Button>
<Button onClick={handleAdd} variant="primary">{t("ShortcutSpaces.pin_button")}</Button>
)}
</div>
)}
@ -144,6 +152,7 @@ function useVisibilityToggle() {
function ShortcutSpaces() {
const [isOpen, requestClose] = useVisibilityToggle();
const { t } = useTranslation();
return (
<Dialog
@ -151,10 +160,10 @@ function ShortcutSpaces() {
className="shortcut-spaces"
title={(
<Text variant="s1" weight="medium" primary>
Pin spaces
{t("ShortcutSpaces.header")}
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip= {t("common.close")}/>}
onRequestClose={requestClose}
>
{

View file

@ -32,6 +32,10 @@ import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function SpaceManageBreadcrumb({ path, onSelect }) {
return (
<div className="space-manage-breadcrumb__wrapper">
@ -75,6 +79,8 @@ function SpaceManageItem({
const canManage = parentRoom?.currentState.maySendStateEvent('m.space.child', mx.getUserId()) || false;
const isSuggested = parentRoom?.currentState.getStateEvents('m.space.child', roomId)?.getContent().suggested === true;
const { t } = useTranslation();
const room = mx.getRoom(roomId);
const isJoined = !!(room?.getMyMembership() === 'join' || null);
const name = room?.name || roomInfo.name || roomInfo.canonical_alias || roomId;
@ -114,7 +120,7 @@ function SpaceManageItem({
const roomNameJSX = (
<Text>
{twemojify(name)}
<Text variant="b3" span>{`${roomInfo.num_joined_members} members`}</Text>
<Text variant="b3" span> {t("SpaceManage.room_members", {count: roomInfo.num_joined_members})}</Text>
</Text>
);
@ -142,19 +148,21 @@ function SpaceManageItem({
>
{roomAvatarJSX}
{roomNameJSX}
{isSuggested && <Text variant="b2">Suggested</Text>}
{isSuggested && <Text variant="b2">{t("SpaceManage.suggested")}</Text>}
</button>
{roomInfo.topic && expandBtnJsx}
{
isJoined
? <Button onClick={handleOpen}>Open</Button>
: <Button variant="primary" onClick={handleJoin} disabled={isJoining}>{isJoining ? 'Joining...' : 'Join'}</Button>
? <Button onClick={handleOpen}>{t("common.open")}</Button>
: <Button variant="primary" onClick={handleJoin} disabled={isJoining}>{t(isJoining ? "common.joining" : "common.join")}</Button>
}
</div>
{isExpand && roomInfo.topic && <Text variant="b2">{twemojify(roomInfo.topic, undefined, true)}</Text>}
</div>
);
}
SpaceManageItem.propTypes = {
parentId: PropTypes.string.isRequired,
roomHierarchy: PropTypes.shape({}).isRequired,
@ -171,21 +179,23 @@ function SpaceManageFooter({ parentId, selected }) {
const room = mx.getRoom(parentId);
const { currentState } = room;
const { t } = useTranslation();
const allSuggested = selected.every((roomId) => {
const sEvent = currentState.getStateEvents('m.space.child', roomId);
return !!sEvent?.getContent()?.suggested;
});
const handleRemove = () => {
setProcess(`Removing ${selected.length} items`);
setProcess(t("SpaceManage.remove", {count: selected.length}));
selected.forEach((roomId) => {
mx.sendStateEvent(parentId, 'm.space.child', {}, roomId);
});
};
const handleToggleSuggested = (isMark) => {
if (isMark) setProcess(`Marking as suggested ${selected.length} items`);
else setProcess(`Marking as not suggested ${selected.length} items`);
if (isMark) setProcess(t("SpaceManage.mark_suggested", {count: selected.length}));
else setProcess(t("SpaceManage.mark_not_suggested", {count: selected.length}));
selected.forEach((roomId) => {
const sEvent = room.currentState.getStateEvents('m.space.child', roomId);
if (!sEvent) return;
@ -200,15 +210,15 @@ function SpaceManageFooter({ parentId, selected }) {
return (
<div className="space-manage__footer">
{process && <Spinner size="small" />}
<Text weight="medium">{process || `${selected.length} item selected`}</Text>
<Text weight="medium">{process || t("SpaceManage.items_selected", {count: selected.length})}</Text>
{ !process && (
<>
<Button onClick={handleRemove} variant="danger">Remove</Button>
<Button onClick={handleRemove} variant="danger">{t("common.remove")}</Button>
<Button
onClick={() => handleToggleSuggested(!allSuggested)}
variant={allSuggested ? 'surface' : 'primary'}
>
{allSuggested ? 'Mark as not suggested' : 'Mark as suggested'}
{t(allSuggested ? 'SpaceManage.mark_as_not_suggested' : 'SpaceManage.mark_as_suggested')}
</Button>
</>
)}
@ -282,6 +292,9 @@ function useChildUpdate(roomId, roomsHierarchy) {
}
function SpaceManageContent({ roomId, requestClose }) {
const { t } = useTranslation();
const mx = initMatrix.matrixClient;
useUpdateOnJoin(roomId);
const [, forceUpdate] = useForceUpdate();
@ -339,11 +352,11 @@ function SpaceManageContent({ roomId, requestClose }) {
{spacePath.length > 1 && (
<SpaceManageBreadcrumb path={spacePath} onSelect={addPathItem} />
)}
<Text variant="b3" weight="bold">Rooms and spaces</Text>
<Text variant="b3" weight="bold">{t("SpaceManage.rooms_and_spaces")}</Text>
<div className="space-manage__content-items">
{!isLoading && currentHierarchy?.rooms?.length === 1 && (
<Text>
Either the space contains private rooms or you need to join space to view it's rooms.
{t("SpaceManage.private_rooms_message")}
</Text>
)}
{currentHierarchy && (currentHierarchy.rooms?.map((roomInfo) => (
@ -362,15 +375,15 @@ function SpaceManageContent({ roomId, requestClose }) {
/>
)
)))}
{!currentHierarchy && <Text>loading...</Text>}
{!currentHierarchy && <Text>{t("common.loading")}</Text>}
</div>
{currentHierarchy?.canLoadMore && !isLoading && (
<Button onClick={loadRoomHierarchy}>Load more</Button>
<Button onClick={loadRoomHierarchy}>{t("SpaceManage.load_more")}</Button>
)}
{isLoading && (
<div className="space-manage__content-loading">
<Spinner size="small" />
<Text>Loading rooms...</Text>
<Text>{t("common.loading")}</Text>
</div>
)}
{selected.length > 0 && (
@ -406,6 +419,8 @@ function SpaceManage() {
const [roomId, requestClose] = useWindowToggle();
const room = mx.getRoom(roomId);
const { t } = useTranslation();
return (
<PopupWindow
isOpen={roomId !== null}
@ -413,7 +428,7 @@ function SpaceManage() {
title={(
<Text variant="s1" weight="medium" primary>
{roomId && twemojify(room.name)}
<span style={{ color: 'var(--tc-surface-low)' }}> manage rooms</span>
<span style={{ color: 'var(--tc-surface-low)' }}> {t("SpaceManage.subtitle")}</span>
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}

View file

@ -83,7 +83,7 @@ function GeneralSettings({ roomId }) {
}}
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
>
{isCategorized ? t("space_settings.uncategorize_subspaces") : t("space_settings.categorize_subspaces")}
{isCategorized ? t("SpaceSettings.uncategorize_subspaces") : t("SpaceSettings.categorize_subspaces")}
</MenuItem>
<MenuItem
onClick={() => {
@ -93,30 +93,30 @@ function GeneralSettings({ roomId }) {
}}
iconSrc={isPinned ? PinFilledIC : PinIC}
>
{isPinned ? t("space_settings.unpin_sidebar") : t("space_settings.pin_sidebar")}
{isPinned ? t("SpaceSettings.unpin_sidebar") : t("SpaceSettings.pin_sidebar")}
</MenuItem>
<MenuItem
variant="danger"
onClick={async () => {
const isConfirmed = await confirmDialog(
t("space_settings.leave.leave_dialog_title"),
t("space_settings.leave.leave_dialog_message", {space: roomName}),
t("space_settings.leave.leave_space"),
t("SpaceSettings.leave.leave_dialog_title"),
t("SpaceSettings.leave.leave_dialog_message", {space: roomName}),
t("SpaceSettings.leave.leave_space"),
'danger',
);
if (isConfirmed) leave(roomId);
}}
iconSrc={LeaveArrowIC}
>
{t("space_settings.leave.leave_space")}
{t("SpaceSettings.leave.leave_space")}
</MenuItem>
</div>
<div className="space-settings__card">
<MenuHeader>{t("space_settings.visibility.header")}</MenuHeader>
<MenuHeader>{t("SpaceSettings.visibility.header")}</MenuHeader>
<RoomVisibility roomId={roomId} />
</div>
<div className="space-settings__card">
<MenuHeader>{t("space_settings.addresses.header")}</MenuHeader>
<MenuHeader>{t("SpaceSettings.addresses.header")}</MenuHeader>
<RoomAliases roomId={roomId} />
</div>
</>
@ -169,7 +169,7 @@ function SpaceSettings() {
title={(
<Text variant="s1" weight="medium" primary>
{isOpen && twemojify(room.name)}
<span style={{ color: 'var(--tc-surface-low)' }}> {t("space_settings.subtitle")}</span>
<span style={{ color: 'var(--tc-surface-low)' }}> {t("SpaceSettings.subtitle")}</span>
</Text>
)}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip={t("common.close")} />}

View file

@ -56,15 +56,15 @@ function ViewSource() {
const renderViewSource = () => (
<div className="view-source">
{event.isEncrypted() && <ViewSourceBlock title={t("view_source.decrypted_source")} json={event.getEffectiveEvent()} />}
<ViewSourceBlock title={t("view_source.original_source")} json={event.event} />
{event.isEncrypted() && <ViewSourceBlock title={t("ViewSource.decrypted_source")} json={event.getEffectiveEvent()} />}
<ViewSourceBlock title={t("ViewSource.original_source")} json={event.event} />
</div>
);
return (
<PopupWindow
isOpen={isOpen}
title={t("view_source.title")}
title={t("ViewSource.title")}
onAfterClose={handleAfterClose}
onRequestClose={() => setIsOpen(false)}
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip={t("common.close")} />}

View file

@ -16,8 +16,8 @@ function Welcome() {
<div className="app-welcome flex--center">
<div>
<img className="app-welcome__logo noselect" src={CinnySvg} alt="Cinny logo" />
<Text className="app-welcome__heading" variant="h1" weight="medium" primary>{t('welcome.heading')}</Text>
<Text className="app-welcome__subheading" variant="s1">{t('welcome.subheading')}</Text>
<Text className="app-welcome__heading" variant="h1" weight="medium" primary>{t('Welcome.heading')}</Text>
<Text className="app-welcome__subheading" variant="s1">{t('Welcome.subheading')}</Text>
</div>
</div>
);