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" : { "common" : {
"close": "Close", "close": "Close",
"open": "Open",
"leave": "Leave", "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!", "heading": "Welcome to Cinny!",
"subheading": "Yet another Matrix client" "subheading": "Yet another Matrix client"
}, },
"view_source":{ "ViewSource":{
"title": "View Source", "title": "View Source",
"original_source": "Original source", "original_source": "Original source",
"decrypted_source": "Decrypted 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", "subtitle": "space settings",
"leave":{ "leave":{
"leave_space": "Leave Space", "leave_space": "Leave Space",
@ -30,5 +91,352 @@
"uncategorize_subspaces": "Uncategorize subspaces", "uncategorize_subspaces": "Uncategorize subspaces",
"pin_sidebar": "Pin to sidebar", "pin_sidebar": "Pin to sidebar",
"unpin_sidebar": "Unpin from 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": "閉める", "close": "閉める",
"leave": "残す" "leave": "残す"
}, },
"welcome":{ "Welcome": {
"heading": "いらっしゃいませ" "heading": "おはようございます",
"subheading": "Yet another Matrix client"
} }
} }

View file

@ -14,7 +14,6 @@ i18n
// for all options read: https://www.i18next.com/overview/configuration-options // for all options read: https://www.i18next.com/overview/configuration-options
.init({ .init({
debug: true, debug: true,
fallbackLng: 'en',
interpolation: { interpolation: {
escapeValue: false, // not needed for react as it escapes by default 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 ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.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 }) { function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
const { t } = useTranslation();
const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite'); const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
const [isEncrypted, setIsEncrypted] = useState(true); const [isEncrypted, setIsEncrypted] = useState(true);
const [isCreatingRoom, setIsCreatingRoom] = useState(false); const [isCreatingRoom, setIsCreatingRoom] = useState(false);
@ -130,8 +137,8 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
}; };
const joinRules = ['invite', 'restricted', 'public']; const joinRules = ['invite', 'restricted', 'public'];
const joinRuleShortText = ['Private', 'Restricted', 'Public']; const joinRuleShortText = [ t("CreateRoom.private_room_short"), t("CreateRoom.restricted_room_short"), t("CreateRoom.public_room_short")];
const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)']; const joinRuleText = [ t("CreateRoom.private_room_long"), t("CreateRoom.restricted_room_long"), t("CreateRoom.public_room_long")];
const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC]; const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC];
const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC]; const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC];
const handleJoinRule = (evt) => { const handleJoinRule = (evt) => {
@ -140,7 +147,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
getEventCords(evt, '.btn-surface'), getEventCords(evt, '.btn-surface'),
(closeMenu) => ( (closeMenu) => (
<> <>
<MenuHeader>Visibility (who can join)</MenuHeader> <MenuHeader>{t("CreateRoom.visibility_message")}</MenuHeader>
{ {
joinRules.map((rule) => ( joinRules.map((rule) => (
<MenuItem <MenuItem
@ -167,17 +174,17 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
<div className="create-room"> <div className="create-room">
<form className="create-room__form" onSubmit={handleSubmit}> <form className="create-room__form" onSubmit={handleSubmit}>
<SettingTile <SettingTile
title="Visibility" title={t("CreateRoom.visibility_title")}
options={( options={(
<Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}> <Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
{joinRuleShortText[joinRules.indexOf(joinRule)]} {joinRuleShortText[joinRules.indexOf(joinRule)]}
</Button> </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' && ( {joinRule === 'public' && (
<div> <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"> <div className="create-room__address">
<Text variant="b1">#</Text> <Text variant="b1">#</Text>
<Input <Input
@ -190,32 +197,32 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
/> />
<Text variant="b1">{`:${userHs}`}</Text> <Text variant="b1">{`:${userHs}`}</Text>
</div> </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> </div>
)} )}
{!isSpace && joinRule !== 'public' && ( {!isSpace && joinRule !== 'public' && (
<SettingTile <SettingTile
title="Enable end-to-end encryption" title={t("CreateRoom.e2e_title")}
options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />} 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 <SettingTile
title="Select your role" title={t("CreateRoom.role_title")}
options={( options={(
<SegmentControl <SegmentControl
selected={roleIndex} selected={roleIndex}
segments={[{ text: 'Admin' }, { text: 'Founder' }]} segments={[{ text: t("CreateRoom.role_admin")}, { text: t("CreateRoom.role_founder")}]}
onSelect={setRoleIndex} onSelect={setRoleIndex}
/> />
)} )}
content={( 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"> <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 <Button
disabled={isValidAddress === false || isCreatingRoom} disabled={isValidAddress === false || isCreatingRoom}
iconSrc={isSpace ? SpacePlusIC : HashPlusIC} iconSrc={isSpace ? SpacePlusIC : HashPlusIC}
@ -228,7 +235,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
{isCreatingRoom && ( {isCreatingRoom && (
<div className="create-room__loading"> <div className="create-room__loading">
<Spinner size="small" /> <Spinner size="small" />
<Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text> <Text>{ isSpace ? t("CreateRoom.creating_space") : t("CreateRoom.creating_room")}</Text>
</div> </div>
)} )}
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>} {typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
@ -277,13 +284,13 @@ function CreateRoom() {
isOpen={create !== null} isOpen={create !== null}
title={( title={(
<Text variant="s1" weight="medium" primary> <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)' }}> <span style={{ color: 'var(--tc-surface-low)' }}>
{`create ${isSpace ? 'space' : 'room'}`} {`${isSpace ? t("CreateRoom.create_space") : t("CreateRoom.create_room")}`}
</span> </span>
</Text> </Text>
)} )}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />} contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
> >
{ {

View file

@ -5,14 +5,20 @@ import './DragDrop.scss';
import RawModal from '../../atoms/modal/RawModal'; import RawModal from '../../atoms/modal/RawModal';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function DragDrop({ isOpen }) { function DragDrop({ isOpen }) {
const { t } = useTranslation();
return ( return (
<RawModal <RawModal
className="drag-drop__modal" className="drag-drop__modal"
overlayClassName="drag-drop__overlay" overlayClassName="drag-drop__overlay"
isOpen={isOpen} 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> </RawModal>
); );
} }

View file

@ -20,6 +20,10 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { accessSecretStorage } from '../settings/SecretStorageAccess'; import { accessSecretStorage } from '../settings/SecretStorageAccess';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function EmojiVerificationContent({ data, requestClose }) { function EmojiVerificationContent({ data, requestClose }) {
const [sas, setSas] = useState(null); const [sas, setSas] = useState(null);
const [process, setProcess] = useState(false); const [process, setProcess] = useState(false);
@ -28,6 +32,8 @@ function EmojiVerificationContent({ data, requestClose }) {
const mountStore = useStore(); const mountStore = useStore();
const beginStore = useStore(); const beginStore = useStore();
const { t } = useTranslation();
const beginVerification = async () => { const beginVerification = async () => {
if ( if (
isCrossVerified(mx.deviceId) isCrossVerified(mx.deviceId)
@ -94,14 +100,14 @@ function EmojiVerificationContent({ data, requestClose }) {
const renderWait = () => ( const renderWait = () => (
<> <>
<Spinner size="small" /> <Spinner size="small" />
<Text>Waiting for response from other device...</Text> <Text>{t("EmojiVerification.waiting_for_response")}</Text>
</> </>
); );
if (sas !== null) { if (sas !== null) {
return ( return (
<div className="emoji-verification__content"> <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"> <div className="emoji-verification__emojis">
{sas.sas.emoji.map((emoji, i) => ( {sas.sas.emoji.map((emoji, i) => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
@ -114,8 +120,8 @@ function EmojiVerificationContent({ data, requestClose }) {
<div className="emoji-verification__buttons"> <div className="emoji-verification__buttons">
{process ? renderWait() : ( {process ? renderWait() : (
<> <>
<Button variant="primary" onClick={sasConfirm}>They match</Button> <Button variant="primary" onClick={sasConfirm}>{t("EmojiVerification.emojis_match_button")}</Button>
<Button onClick={sasMismatch}>{'They don\'t match'}</Button> <Button onClick={sasMismatch}>{t("EmojiVerification.emojis_dont_match_button")}</Button>
</> </>
)} )}
</div> </div>
@ -126,7 +132,7 @@ function EmojiVerificationContent({ data, requestClose }) {
if (targetDevice) { if (targetDevice) {
return ( return (
<div className="emoji-verification__content"> <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"> <div className="emoji-verification__buttons">
{renderWait()} {renderWait()}
</div> </div>
@ -136,12 +142,12 @@ function EmojiVerificationContent({ data, requestClose }) {
return ( return (
<div className="emoji-verification__content"> <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"> <div className="emoji-verification__buttons">
{ {
process process
? renderWait() ? renderWait()
: <Button variant="primary" onClick={beginVerification}>Accept</Button> : <Button variant="primary" onClick={beginVerification}>{t("EmojiVerification.begin_verification_button_text")}</Button>
} }
</div> </div>
</div> </div>
@ -182,10 +188,10 @@ function EmojiVerification() {
className="emoji-verification" className="emoji-verification"
title={( title={(
<Text variant="s1" weight="medium" primary> <Text variant="s1" weight="medium" primary>
Emoji verification {t("EmojiVerification.title")}
</Text> </Text>
)} )}
contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />} contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip={t("common.close")} />}
onRequestClose={requestClose} 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 CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function InviteList({ isOpen, onRequestClose }) { function InviteList({ isOpen, onRequestClose }) {
const [procInvite, changeProcInvite] = useState(new Set()); const [procInvite, changeProcInvite] = useState(new Set());
const { t } = useTranslation();
function acceptInvite(roomId, isDM) { function acceptInvite(roomId, isDM) {
procInvite.add(roomId); procInvite.add(roomId);
changeProcInvite(new Set(Array.from(procInvite))); changeProcInvite(new Set(Array.from(procInvite)));
@ -73,8 +81,8 @@ function InviteList({ isOpen, onRequestClose }) {
? (<Spinner size="small" />) ? (<Spinner size="small" />)
: ( : (
<div className="invite-btn__container"> <div className="invite-btn__container">
<Button onClick={() => rejectInvite(myRoom.roomId)}>Reject</Button> <Button onClick={() => rejectInvite(myRoom.roomId)}>{t("InviteList.reject_invite")}</Button>
<Button onClick={() => acceptInvite(myRoom.roomId)} variant="primary">Accept</Button> <Button onClick={() => acceptInvite(myRoom.roomId)} variant="primary">{t("InviteList.accept_invite")}</Button>
</div> </div>
) )
} }
@ -85,14 +93,14 @@ function InviteList({ isOpen, onRequestClose }) {
return ( return (
<PopupWindow <PopupWindow
isOpen={isOpen} isOpen={isOpen}
title="Invites" title={t("InviteList.title")}
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />} contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
> >
<div className="invites-content"> <div className="invites-content">
{ initMatrix.roomList.inviteDirects.size !== 0 && ( { initMatrix.roomList.inviteDirects.size !== 0 && (
<div className="invites-content__subheading"> <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> </div>
)} )}
{ {
@ -110,8 +118,8 @@ function InviteList({ isOpen, onRequestClose }) {
? (<Spinner size="small" />) ? (<Spinner size="small" />)
: ( : (
<div className="invite-btn__container"> <div className="invite-btn__container">
<Button onClick={() => rejectInvite(myRoom.roomId, true)}>Reject</Button> <Button onClick={() => rejectInvite(myRoom.roomId, true)}>{t("InviteList.reject_invite")}</Button>
<Button onClick={() => acceptInvite(myRoom.roomId, true)} variant="primary">Accept</Button> <Button onClick={() => acceptInvite(myRoom.roomId, true)} variant="primary">{t("InviteList.accept_invite")}</Button>
</div> </div>
) )
} }
@ -121,14 +129,14 @@ function InviteList({ isOpen, onRequestClose }) {
} }
{ initMatrix.roomList.inviteSpaces.size !== 0 && ( { initMatrix.roomList.inviteSpaces.size !== 0 && (
<div className="invites-content__subheading"> <div className="invites-content__subheading">
<Text variant="b3" weight="bold">Spaces</Text> <Text variant="b3" weight="bold">{t("InviteList.spaces_title")}</Text>
</div> </div>
)} )}
{ Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) } { Array.from(initMatrix.roomList.inviteSpaces).map(renderRoomTile) }
{ initMatrix.roomList.inviteRooms.size !== 0 && ( { initMatrix.roomList.inviteRooms.size !== 0 && (
<div className="invites-content__subheading"> <div className="invites-content__subheading">
<Text variant="b3" weight="bold">Rooms</Text> <Text variant="b3" weight="bold">{t("InviteList.rooms_title")}</Text>
</div> </div>
)} )}
{ Array.from(initMatrix.roomList.inviteRooms).map(renderRoomTile) } { 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 CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import UserIC from '../../../../public/res/ic/outlined/user.svg'; import UserIC from '../../../../public/res/ic/outlined/user.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function InviteUser({ function InviteUser({
isOpen, roomId, searchTerm, onRequestClose, isOpen, roomId, searchTerm, onRequestClose,
}) { }) {
@ -36,6 +40,8 @@ function InviteUser({
const usernameRef = useRef(null); const usernameRef = useRef(null);
const { t } = useTranslation();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
function getMapCopy(myMap) { function getMapCopy(myMap) {
@ -82,7 +88,7 @@ function InviteUser({
avatar_url: result.avatar_url, avatar_url: result.avatar_url,
}]); }]);
} catch (e) { } catch (e) {
updateSearchQuery({ error: `${inputUsername} not found!` }); updateSearchQuery({error: t("InviteUser.user_not_found", {user_name: inputUsername})});
} }
} else { } else {
try { try {
@ -91,13 +97,13 @@ function InviteUser({
limit: 20, limit: 20,
}); });
if (result.results.length === 0) { if (result.results.length === 0) {
updateSearchQuery({ error: `No matches found for "${inputUsername}"!` }); updateSearchQuery({ error: t("InviteUser.no_matches_found", {user_name: inputUsername})});
updateIsSearching(false); updateIsSearching(false);
return; return;
} }
updateUsers(result.results); updateUsers(result.results);
} catch (e) { } catch (e) {
updateSearchQuery({ error: 'Something went wrong!' }); updateSearchQuery({ error: t("errors.generic")});
} }
} }
updateIsSearching(false); updateIsSearching(false);
@ -135,7 +141,7 @@ function InviteUser({
} catch (e) { } catch (e) {
deleteUserFromProc(userId); deleteUserFromProc(userId);
if (typeof e.message === 'string') procUserError.set(userId, e.message); 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)); updateUserProcError(getMapCopy(procUserError));
} }
} }
@ -155,7 +161,7 @@ function InviteUser({
} catch (e) { } catch (e) {
deleteUserFromProc(userId); deleteUserFromProc(userId);
if (typeof e.message === 'string') procUserError.set(userId, e.message); 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)); updateUserProcError(getMapCopy(procUserError));
} }
} }
@ -173,7 +179,7 @@ function InviteUser({
return <Button onClick={() => { selectRoom(createdDM.get(userId)); onRequestClose(); }}>Open</Button>; return <Button onClick={() => { selectRoom(createdDM.get(userId)); onRequestClose(); }}>Open</Button>;
} }
if (invitedUserIds.has(userId)) { if (invitedUserIds.has(userId)) {
return messageJSX('Invited', true); return messageJSX(t("InviteUser.invite_result.invited"), true);
} }
if (typeof roomId === 'string') { if (typeof roomId === 'string') {
const member = mx.getRoom(roomId).getMember(userId); const member = mx.getRoom(roomId).getMember(userId);
@ -181,18 +187,18 @@ function InviteUser({
const userMembership = member.membership; const userMembership = member.membership;
switch (userMembership) { switch (userMembership) {
case 'join': case 'join':
return messageJSX('Already joined', true); return messageJSX(t("InviteUser.invite_result.already_joined"), true);
case 'invite': case 'invite':
return messageJSX('Already Invited', true); return messageJSX(t("InviteUser.invite_result.already_invited"), true);
case 'ban': case 'ban':
return messageJSX('Banned', false); return messageJSX(t("InviteUser.invite_result.banned"), false);
default: default:
} }
} }
} }
return (typeof roomId === 'string') return (typeof roomId === 'string')
? <Button onClick={() => inviteToRoom(userId)} variant="primary">Invite</Button> ? <Button onClick={() => inviteToRoom(userId)} variant="primary">{t("common.invite")}</Button>
: <Button onClick={() => createDM(userId)} variant="primary">Message</Button>; : <Button onClick={() => createDM(userId)} variant="primary">{t("common.message_prompt")}</Button>;
}; };
const renderError = (userId) => { const renderError = (userId) => {
if (!procUserError.has(userId)) return null; if (!procUserError.has(userId)) return null;
@ -239,27 +245,27 @@ function InviteUser({
return ( return (
<PopupWindow <PopupWindow
isOpen={isOpen} isOpen={isOpen}
title={(typeof roomId === 'string' ? `Invite to ${mx.getRoom(roomId).name}` : 'Direct message')} 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="Close" />} contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip={t("common.close")} />}
onRequestClose={onRequestClose} onRequestClose={onRequestClose}
> >
<div className="invite-user"> <div className="invite-user">
<form className="invite-user__form" onSubmit={(e) => { e.preventDefault(); searchUser(usernameRef.current.value); }}> <form className="invite-user__form" onSubmit={(e) => { e.preventDefault(); searchUser(usernameRef.current.value); }}>
<Input value={searchTerm} forwardRef={usernameRef} label="Name or userId" /> <Input value={searchTerm} forwardRef={usernameRef} label={t("InviteUser.search_label")} />
<Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">Search</Button> <Button disabled={isSearching} iconSrc={UserIC} variant="primary" type="submit">{t("common.search")}</Button>
</form> </form>
<div className="invite-user__search-status"> <div className="invite-user__search-status">
{ {
typeof searchQuery.username !== 'undefined' && isSearching && ( typeof searchQuery.username !== 'undefined' && isSearching && (
<div className="flex--center"> <div className="flex--center">
<Spinner size="small" /> <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> </div>
) )
} }
{ {
typeof searchQuery.username !== 'undefined' && !isSearching && ( 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 { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/; const ALIAS_OR_ID_REG = /^[#|!].+:.+\..+$/;
function JoinAliasContent({ term, requestClose }) { function JoinAliasContent({ term, requestClose }) {
@ -29,6 +34,8 @@ function JoinAliasContent({ term, requestClose }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
const { t } = useTranslation();
const openRoom = (roomId) => { const openRoom = (roomId) => {
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if (!room) return; if (!room) return;
@ -54,10 +61,10 @@ function JoinAliasContent({ term, requestClose }) {
const alias = e.target.alias.value; const alias = e.target.alias.value;
if (alias?.trim() === '') return; if (alias?.trim() === '') return;
if (alias.match(ALIAS_OR_ID_REG) === null) { if (alias.match(ALIAS_OR_ID_REG) === null) {
setError('Invalid address.'); setError(t("JoinAlias.invalid_address"));
return; return;
} }
setProcess('Looking for address...'); setProcess(t("JoinAlias.looking_for_address"));
setError(undefined); setError(undefined);
let via; let via;
if (alias.startsWith('#')) { if (alias.startsWith('#')) {
@ -65,12 +72,12 @@ function JoinAliasContent({ term, requestClose }) {
const aliasData = await mx.resolveRoomAlias(alias); const aliasData = await mx.resolveRoomAlias(alias);
via = aliasData?.servers.slice(0, 3) || []; via = aliasData?.servers.slice(0, 3) || [];
if (mountStore.getItem()) { if (mountStore.getItem()) {
setProcess(`Joining ${alias}...`); setProcess(t("JoinAlias.joining_alias", {alias_name: alias}));
} }
} catch (err) { } catch (err) {
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
setProcess(false); 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 { try {
@ -81,14 +88,14 @@ function JoinAliasContent({ term, requestClose }) {
} catch { } catch {
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
setProcess(false); 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 ( return (
<form className="join-alias" onSubmit={handleSubmit}> <form className="join-alias" onSubmit={handleSubmit}>
<Input <Input
label="Address" label={t("JoinAlias.address_label")}
value={term} value={term}
name="alias" name="alias"
required required
@ -103,7 +110,7 @@ function JoinAliasContent({ term, requestClose }) {
<Text>{process}</Text> <Text>{process}</Text>
</> </>
) )
: <Button variant="primary" type="submit">Join</Button> : <Button variant="primary" type="submit">{t("common.join")}</Button>
} }
</div> </div>
</form> </form>
@ -142,9 +149,9 @@ function JoinAlias() {
<Dialog <Dialog
isOpen={data !== null} isOpen={data !== null}
title={( 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} onRequestClose={requestClose}
> >
{ data ? <JoinAliasContent term={data.term} requestClose={requestClose} /> : <div /> } { 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 ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function DrawerBreadcrumb({ spaceId }) { function DrawerBreadcrumb({ spaceId }) {
const [, forceUpdate] = useState({}); const [, forceUpdate] = useState({});
const scrollRef = useRef(null); const scrollRef = useRef(null);
@ -25,6 +30,8 @@ function DrawerBreadcrumb({ spaceId }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const spacePath = navigation.selectedSpacePath; const spacePath = navigation.selectedSpacePath;
const { t } = useTranslation();
function onNotiChanged(roomId, total, prevTotal) { function onNotiChanged(roomId, total, prevTotal) {
if (total === prevTotal) return; if (total === prevTotal) return;
if (navigation.selectedSpacePath.includes(roomId)) { if (navigation.selectedSpacePath.includes(roomId)) {
@ -109,7 +116,7 @@ function DrawerBreadcrumb({ spaceId }) {
className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''} className={index === spacePath.length - 1 ? 'drawer-breadcrumb__btn--selected' : ''}
onClick={() => selectSpace(id)} 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 && ( { noti !== null && (
<NotificationBadge <NotificationBadge
alert={noti.highlight !== 0} 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 SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.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 }) { export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const room = mx.getRoom(spaceId); const room = mx.getRoom(spaceId);
@ -35,29 +40,31 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
? room.currentState.maySendStateEvent('m.space.child', mx.getUserId()) ? room.currentState.maySendStateEvent('m.space.child', mx.getUserId())
: true; : true;
const { t } = useTranslation();
return ( return (
<> <>
<MenuHeader>Add rooms or spaces</MenuHeader> <MenuHeader>{t("DrawerHeader.add_rooms_or_spaces")}</MenuHeader>
<MenuItem <MenuItem
iconSrc={SpacePlusIC} iconSrc={SpacePlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }} onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
disabled={!canManage} disabled={!canManage}
> >
Create new space {t("DrawerHeader.create_new_space")}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
iconSrc={HashPlusIC} iconSrc={HashPlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }} onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
disabled={!canManage} disabled={!canManage}
> >
Create new room {t("DrawerHeader.create_new_room")}
</MenuItem> </MenuItem>
{ !spaceId && ( { !spaceId && (
<MenuItem <MenuItem
iconSrc={HashGlobeIC} iconSrc={HashGlobeIC}
onClick={() => { afterOptionSelect(); openPublicRooms(); }} onClick={() => { afterOptionSelect(); openPublicRooms(); }}
> >
Join public room {t("DrawerHeader.join_public_room")}
</MenuItem> </MenuItem>
)} )}
{ !spaceId && ( { !spaceId && (
@ -65,7 +72,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
iconSrc={PlusIC} iconSrc={PlusIC}
onClick={() => { afterOptionSelect(); openJoinAlias(); }} onClick={() => { afterOptionSelect(); openJoinAlias(); }}
> >
Join with address {t("DrawerHeader.join_with_address")}
</MenuItem> </MenuItem>
)} )}
{ spaceId && ( { spaceId && (
@ -74,7 +81,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }} onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
disabled={!canManage} disabled={!canManage}
> >
Add existing {t("DrawerHeader.add_existing")}
</MenuItem> </MenuItem>
)} )}
{ spaceId && ( { spaceId && (
@ -82,7 +89,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }} onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
iconSrc={HashSearchIC} iconSrc={HashSearchIC}
> >
Manage rooms {t("DrawerHeader.manage_rooms")}
</MenuItem> </MenuItem>
)} )}
</> </>
@ -98,7 +105,7 @@ HomeSpaceOptions.propTypes = {
function DrawerHeader({ selectedTab, spaceId }) { function DrawerHeader({ selectedTab, spaceId }) {
const mx = initMatrix.matrixClient; 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 isDMTab = selectedTab === cons.tabs.DIRECTS;
const room = mx.getRoom(spaceId); const room = mx.getRoom(spaceId);
@ -142,8 +149,8 @@ function DrawerHeader({ selectedTab, spaceId }) {
</TitleWrapper> </TitleWrapper>
)} )}
{ isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> } { isDMTab && <IconButton onClick={() => openInviteUser()} tooltip={t("DrawerHeader.start_dm_tooltip")} src={PlusIC} size="small" /> }
{ !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> } { !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip={t("DrawerHeader.add_rooms_spaces_tooltip")} src={PlusIC} size="small" /> }
</Header> </Header>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,10 @@ import { parseTimelineChange } from './common';
import TimelineScroll from './TimelineScroll'; import TimelineScroll from './TimelineScroll';
import EventLimit from './EventLimit'; import EventLimit from './EventLimit';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
const PAG_LIMIT = 30; const PAG_LIMIT = 30;
const MAX_MSG_DIFF_MINUTES = 5; const MAX_MSG_DIFF_MINUTES = 5;
const PLACEHOLDER_COUNT = 2; const PLACEHOLDER_COUNT = 2;
@ -55,29 +59,39 @@ function RoomIntroContainer({ event, timeline }) {
const [, nameForceUpdate] = useForceUpdate(); const [, nameForceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { roomList } = initMatrix; const { roomList } = initMatrix;
const { t } = useTranslation();
const { room } = timeline; const { room } = timeline;
const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; const roomTopic = room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
const isDM = roomList.directs.has(timeline.roomId); const isDM = roomList.directs.has(timeline.roomId);
let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop'); let avatarSrc = room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
avatarSrc = isDM ? room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc; 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 topic = twemojify(roomTopic || '', undefined, true);
const nameJsx = twemojify(room.name); const nameJsx = twemojify(room.name);
const desc = isDM const desc = isDM
? ( ? (
<> <>
This is the beginning of your direct message history with @ <Trans
<b>{nameJsx}</b> i18nKey={"RoomViewContent.beginning_dm"}
{'. '} values={{user_name: nameJsx}}
{topic} components={{bold: <b/>}}
/>
{topic == "" ? "" : " - "}
{topic }
</> </>
) )
: ( : (
<> <>
{'This is the beginning of the '} <Trans
<b>{nameJsx}</b> i18nKey={"RoomViewContent.beginning_room"}
{' room. '} values={{room_name: nameJsx}}
components={{bold: <b/>}}
/>
{topic == "" ? "" : " - "}
{topic} {topic}
</> </>
); );
@ -98,7 +112,7 @@ function RoomIntroContainer({ event, timeline }) {
name={room.name} name={room.name}
heading={twemojify(heading)} heading={twemojify(heading)}
desc={desc} 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 }) { function RoomViewContent({ eventId, roomTimeline }) {
const [throttle] = useState(new Throttle()); const [throttle] = useState(new Throttle());
const { t } = useTranslation();
const timelineSVRef = useRef(null); const timelineSVRef = useRef(null);
const timelineScrollRef = useRef(null); const timelineScrollRef = useRef(null);
const eventLimitRef = useRef(null); const eventLimitRef = useRef(null);
@ -523,7 +539,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
&& readUptoEvent.getTs() < mEvent.getTs()); && readUptoEvent.getTs() < mEvent.getTs());
if (unreadDivider) { if (unreadDivider) {
isNewEvent = true; 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; itemCountIndex += 1;
if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex; if (jumpToItemIndex === -1) jumpToItemIndex = itemCountIndex;
} }

View file

@ -3,6 +3,8 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './RoomViewFloating.scss'; import './RoomViewFloating.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import { markAsRead } from '../../../client/action/notifications'; 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 MessageUnreadIC from '../../../../public/res/ic/outlined/message-unread.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
import { getUsersActionJsx } from './common'; import { getUsersActionJsx } from './common';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
function useJumpToEvent(roomTimeline) { function useJumpToEvent(roomTimeline) {
const [eventId, setEventId] = useState(null); const [eventId, setEventId] = useState(null);
@ -86,32 +94,55 @@ function useScrollToBottom(roomTimeline) {
function RoomViewFloating({ function RoomViewFloating({
roomId, roomTimeline, roomId, roomTimeline,
}) { }) {
const { t } = useTranslation();
const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline); const [isJumpToEvent, jumpToEvent, cancelJumpToEvent] = useJumpToEvent(roomTimeline);
const [typingMembers] = useTypingMembers(roomTimeline); const [typingMembers] = useTypingMembers(roomTimeline);
const [isAtBottom, setIsAtBottom] = useScrollToBottom(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 = () => { const handleScrollToBottom = () => {
roomTimeline.emit(cons.events.roomTimeline.SCROLL_TO_LIVE); roomTimeline.emit(cons.events.roomTimeline.SCROLL_TO_LIVE);
setIsAtBottom(true); setIsAtBottom(true);
}; };
console.log(typingMembers)
let typingMemberValues = [...typingMembers];
console.log(typingMemberValues)
return ( return (
<> <>
<div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}> <div className={`room-view__unread ${isJumpToEvent ? 'room-view__unread--open' : ''}`}>
<Button iconSrc={MessageUnreadIC} onClick={jumpToEvent} variant="primary"> <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>
<Button iconSrc={TickMarkIC} onClick={cancelJumpToEvent} variant="primary"> <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> </Button>
</div> </div>
<div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}> <div className={`room-view__typing${typingMembers.size > 0 ? ' room-view__typing--open' : ''}`}>
<div className="bouncing-loader"><div /></div> <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>
<div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}> <div className={`room-view__STB${isAtBottom ? '' : ' room-view__STB--open'}`}>
<Button iconSrc={MessageIC} onClick={handleScrollToBottom}> <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> </Button>
</div> </div>
</> </>

View file

@ -29,6 +29,9 @@ import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomViewHeader({ roomId }) { function RoomViewHeader({ roomId }) {
const [, forceUpdate] = useForceUpdate(); const [, forceUpdate] = useForceUpdate();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
@ -37,6 +40,8 @@ function RoomViewHeader({ roomId }) {
avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc; avatarSrc = isDM ? mx.getRoom(roomId).getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 36, 36, 'crop') : avatarSrc;
const roomName = mx.getRoom(roomId).name; const roomName = mx.getRoom(roomId).name;
const { t } = useTranslation();
const roomHeaderBtnRef = useRef(null); const roomHeaderBtnRef = useRef(null);
useEffect(() => { useEffect(() => {
const settingsToggle = (isVisibile) => { const settingsToggle = (isVisibile) => {
@ -93,17 +98,18 @@ function RoomViewHeader({ roomId }) {
</TitleWrapper> </TitleWrapper>
<RawIcon src={ChevronBottomIC} /> <RawIcon src={ChevronBottomIC} />
</button> </button>
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} /> <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip={t("RoomViewHeader.search_tooltip")} src={SearchIC} />
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} /> <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="Members" src={UserIC} /> <IconButton className="room-header__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip={t("RoomViewHeader.members_tooltip")} src={UserIC} />
<IconButton <IconButton
onClick={openRoomOptions} onClick={openRoomOptions}
tooltip="Options" tooltip={t("common.options")}
src={VerticalMenuIC} src={VerticalMenuIC}
/> />
</Header> </Header>
); );
} }
RoomViewHeader.propTypes = { RoomViewHeader.propTypes = {
roomId: PropTypes.string.isRequired, 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 FileIC from '../../../../public/res/ic/outlined/file.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const CMD_REGEX = /(^\/|:|@)(\S*)$/; const CMD_REGEX = /(^\/|:|@)(\S*)$/;
let isTyping = false; let isTyping = false;
let isCmdActivated = false; let isCmdActivated = false;
@ -41,6 +44,8 @@ function RoomViewInput({
const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown); const [isMarkdown, setIsMarkdown] = useState(settings.isMarkdown);
const [replyTo, setReplyTo] = useState(null); const [replyTo, setReplyTo] = useState(null);
const { t } = useTranslation();
const textAreaRef = useRef(null); const textAreaRef = useRef(null);
const inputBaseRef = useRef(null); const inputBaseRef = useRef(null);
const uploadInputRef = useRef(null); const uploadInputRef = useRef(null);
@ -81,7 +86,7 @@ function RoomViewInput({
function uploadingProgress(myRoomId, { loaded, total }) { function uploadingProgress(myRoomId, { loaded, total }) {
if (myRoomId !== roomId) return; if (myRoomId !== roomId) return;
const progressPer = Math.round((loaded * 100) / total); 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}%)`; inputBaseRef.current.style.backgroundImage = `linear-gradient(90deg, var(--bg-surface-hover) ${progressPer}%, var(--bg-surface-low) ${progressPer}%)`;
} }
function clearAttachment(myRoomId) { function clearAttachment(myRoomId) {
@ -311,8 +316,8 @@ function RoomViewInput({
<Text className="room-input__alert"> <Text className="room-input__alert">
{ {
tombstoneEvent tombstoneEvent
? tombstoneEvent.getContent()?.body ?? 'This room has been replaced and is no longer active.' ? tombstoneEvent.getContent()?.body ?? t("RoomViewInput.tombstone_replaced")
: 'You do not have permission to post to this room' : t("RoomViewInput.tombstone_permission_denied")
} }
</Text> </Text>
); );
@ -321,7 +326,7 @@ function RoomViewInput({
<> <>
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}> <div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
<input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" /> <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>
<div ref={inputBaseRef} className="room-input__input-container"> <div ref={inputBaseRef} className="room-input__input-container">
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />} {roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
@ -333,7 +338,7 @@ function RoomViewInput({
onChange={handleMsgTyping} onChange={handleMsgTyping}
onPaste={handlePaste} onPaste={handlePaste}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Send a message..." placeholder={t("RoomViewInput.send_message_placeholder")}
/> />
</Text> </Text>
</ScrollView> </ScrollView>
@ -347,10 +352,10 @@ function RoomViewInput({
cords.y -= 250; cords.y -= 250;
openEmojiBoard(cords, addEmoji); openEmojiBoard(cords, addEmoji);
}} }}
tooltip="Emoji" tooltip={t("RoomViewInput.emoji_tooltip")}
src={EmojiIC} src={EmojiIC}
/> />
<IconButton onClick={sendMessage} tooltip="Send" src={SendIC} /> <IconButton onClick={sendMessage} tooltip={t("common.send")} src={SendIC} />
</div> </div>
</> </>
); );
@ -368,7 +373,7 @@ function RoomViewInput({
</div> </div>
<div className="room-attachment__info"> <div className="room-attachment__info">
<Text variant="b1">{attachment.name}</Text> <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>
</div> </div>
); );
@ -383,7 +388,7 @@ function RoomViewInput({
setReplyTo(null); setReplyTo(null);
}} }}
src={CrossIC} src={CrossIC}
tooltip="Cancel reply" tooltip={t("RoomViewInput.cancel_reply_tooltip")}
size="extra-small" size="extra-small"
/> />
<MessageReply <MessageReply

View file

@ -5,13 +5,22 @@ import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil'; import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
function getTimelineJSXMessages() { function getTimelineJSXMessages() {
return { return {
join(user) { join(user) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' joined the room'} i18nKey={"RoomCommon.user_joined"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
@ -19,118 +28,145 @@ function getTimelineJSXMessages() {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : ''; const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' left the room'} i18nKey={"RoomCommon.user_left"}
{twemojify(reasonMsg)} values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
invite(inviter, user) { invite(inviter, user) {
return ( return (
<> <>
<b>{twemojify(inviter)}</b> <Trans
{' invited '} i18nKey={"RoomCommon.user_invited"}
<b>{twemojify(user)}</b> values={{user_name: twemojify(user), inviter_name: twemojify(inviter)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
cancelInvite(inviter, user) { cancelInvite(inviter, user) {
return ( return (
<> <>
<b>{twemojify(inviter)}</b> <Trans
{' canceled '} i18nKey={"RoomCommon.invite_cancelled"}
<b>{twemojify(user)}</b> values={{user_name: twemojify(user), inviter_name: twemojify(inviter)}}
{'\'s invite'} components={{bold: <b/>}}
/>
</> </>
); );
}, },
rejectInvite(user) { rejectInvite(user) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' rejected the invitation'} i18nKey={"RoomCommon.invite_rejected"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
kick(actor, user, reason) { kick(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : ''; const reasonMsg = (typeof reason === 'string') ? `${reason}` : '';
return ( return (
<> <>
<b>{twemojify(actor)}</b> <Trans
{' kicked '} i18nKey={"RoomCommon.user_kicked"}
<b>{twemojify(user)}</b> values={{user_name: twemojify(user), actor: twemojify(actor), reason: twemojify(reasonMsg)}}
{twemojify(reasonMsg)} components={{bold: <b/>}}
/>
</> </>
); );
}, },
ban(actor, user, reason) { ban(actor, user, reason) {
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : ''; const reasonMsg = (typeof reason === 'string') ? `${reason}` : '';
return ( return (
<> <>
<b>{twemojify(actor)}</b> <Trans
{' banned '} i18nKey={"RoomCommon.user_banned"}
<b>{twemojify(user)}</b> values={{user_name: twemojify(user), actor: twemojify(actor), reason: twemojify(reasonMsg)}}
{twemojify(reasonMsg)} components={{bold: <b/>}}
/>
</> </>
); );
}, },
unban(actor, user) { unban(actor, user) {
return ( return (
<> <>
<b>{twemojify(actor)}</b> <Trans
{' unbanned '} i18nKey={"RoomCommon.user_unbanned"}
<b>{twemojify(user)}</b> values={{user_name: twemojify(user), actor: twemojify(actor)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
avatarSets(user) { avatarSets(user) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' set a avatar'} i18nKey={"RoomCommon.avatar_set"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
avatarChanged(user) { avatarChanged(user) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' changed their avatar'} i18nKey={"RoomCommon.avatar_changed"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
avatarRemoved(user) { avatarRemoved(user) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' removed their avatar'} i18nKey={"RoomCommon.avatar_removed"}
values={{user_name: twemojify(user)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
nameSets(user, newName) { nameSets(user, newName) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' set display name to '} i18nKey={"RoomCommon.name_set"}
<b>{twemojify(newName)}</b> values={{user_name: twemojify(user), new_name: twemojify(newName)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
nameChanged(user, newName) { nameChanged(user, newName) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' changed their display name to '} i18nKey={"RoomCommon.name_changed"}
<b>{twemojify(newName)}</b> values={{user_name: twemojify(user), new_name: twemojify(newName)}}
components={{bold: <b/>}}
/>
</> </>
); );
}, },
nameRemoved(user, lastName) { nameRemoved(user, lastName) {
return ( return (
<> <>
<b>{twemojify(user)}</b> <Trans
{' removed their display name '} i18nKey={"RoomCommon.name_removed"}
<b>{twemojify(lastName)}</b> 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 SearchIC from '../../../../public/res/ic/outlined/search.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function useVisiblityToggle(setResult) { function useVisiblityToggle(setResult) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -81,6 +84,8 @@ function Search() {
const searchRef = useRef(null); const searchRef = useRef(null);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { t } = useTranslation();
const handleSearchResults = (chunk, term) => { const handleSearchResults = (chunk, term) => {
setResult({ setResult({
term, term,
@ -212,7 +217,7 @@ function Search() {
<Input <Input
onChange={handleOnChange} onChange={handleOnChange}
forwardRef={searchRef} forwardRef={searchRef}
placeholder="Search" placeholder={t("common.search")}
/> />
<IconButton size="small" src={CrossIC} type="reset" onClick={handleCross} tabIndex={-1} /> <IconButton size="small" src={CrossIC} type="reset" onClick={handleCross} tabIndex={-1} />
</form> </form>
@ -224,7 +229,7 @@ function Search() {
</ScrollView> </ScrollView>
</div> </div>
<div className="search-dialog__footer"> <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>
</div> </div>
</RawModal> </RawModal>

View file

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

View file

@ -19,58 +19,67 @@ import SettingTile from '../../molecules/setting-tile/SettingTile';
import { authRequest } from './AuthRequest'; import { authRequest } from './AuthRequest';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
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>
</div>
);
openReusableDialog( import '../../i18n.jsx'
<Text variant="s1" weight="medium">Setup cross signing</Text>, import { useTranslation } from 'react-i18next';
renderFailure,
);
};
const securityKeyDialog = (key) => {
const downloadKey = () => {
const blob = new Blob([key.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'security-key.txt');
};
const copyKey = () => {
copyToClipboard(key.encodedPrivateKey);
};
const renderSecurityKey = () => (
<div className="cross-signing__key">
<Text weight="medium">Please save this security key somewhere safe.</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>
</div>
</div>
);
// Download automatically.
downloadKey();
openReusableDialog(
<Text variant="s1" weight="medium">Security Key</Text>,
() => renderSecurityKey(),
);
};
function CrossSigningSetup() { function CrossSigningSetup() {
const { t } = useTranslation();
const initialValues = { phrase: '', confirmPhrase: '' }; const initialValues = { phrase: '', confirmPhrase: '' };
const [genWithPhrase, setGenWithPhrase] = useState(undefined); const [genWithPhrase, setGenWithPhrase] = useState(undefined);
const failedDialog = () => {
const renderFailure = (requestClose) => (
<div className="cross-signing__failure">
<Text variant="h1">{twemojify('❌')}</Text>
<Text weight="medium">{t("CrossSigning.setup_failed")}</Text>
<Button onClick={requestClose}>{t("common.close")}</Button>
</div>
);
openReusableDialog(
<Text variant="s1" weight="medium">{t("CrossSigning.setup")}</Text>,
renderFailure,
);
};
const securityKeyDialog = (key) => {
const downloadKey = () => {
const blob = new Blob([key.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'security-key.txt');
};
const copyKey = () => {
copyToClipboard(key.encodedPrivateKey);
};
const renderSecurityKey = () => (
<div className="cross-signing__key">
<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)}>{t("common.copy")}</Button>
<Button onClick={() => downloadKey(key)}>{t("common.download")}</Button>
</div>
</div>
);
// Download automatically.
downloadKey();
openReusableDialog(
<Text variant="s1" weight="medium">{t("CrossSigning.security_key_dialog_title")}</Text>,
() => renderSecurityKey(),
);
};
const setup = async (securityPhrase = undefined) => { const setup = async (securityPhrase = undefined) => {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
setGenWithPhrase(typeof securityPhrase === 'string'); setGenWithPhrase(typeof securityPhrase === 'string');
@ -121,13 +130,12 @@ function CrossSigningSetup() {
<div className="cross-signing__setup"> <div className="cross-signing__setup">
<div className="cross-signing__setup-entry"> <div className="cross-signing__setup-entry">
<Text> <Text>
We will generate a <b>Security Key</b>, {t("CrossSigning.security_key_generation_message")}
which you can use to manage messages backup and session verification.
</Text> </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" />} {genWithPhrase === false && <Spinner size="small" />}
</div> </div>
<Text className="cross-signing__setup-divider">OR</Text> <Text className="cross-signing__setup-divider">{t("common.or")}</Text>
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
onSubmit={(values) => setup(values.phrase)} onSubmit={(values) => setup(values.phrase)}
@ -142,15 +150,13 @@ function CrossSigningSetup() {
disabled={genWithPhrase !== undefined} disabled={genWithPhrase !== undefined}
> >
<Text> <Text>
Alternatively you can also set a <b>Security Phrase </b> {t("CrossSigning.security_phrase_message")}
so you don't have to remember long Security Key,
and optionally save the Key as backup.
</Text> </Text>
<Input <Input
name="phrase" name="phrase"
value={values.phrase} value={values.phrase}
onChange={handleChange} onChange={handleChange}
label="Security Phrase" label={t("CrossSigning.security_phrase_label")}
type="password" type="password"
required required
disabled={genWithPhrase !== undefined} disabled={genWithPhrase !== undefined}
@ -160,13 +166,13 @@ function CrossSigningSetup() {
name="confirmPhrase" name="confirmPhrase"
value={values.confirmPhrase} value={values.confirmPhrase}
onChange={handleChange} onChange={handleChange}
label="Confirm Security Phrase" label={t("CrossSigning.security_phrase_confirm_label")}
type="password" type="password"
required required
disabled={genWithPhrase !== undefined} disabled={genWithPhrase !== undefined}
/> />
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>} {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" />} {genWithPhrase === true && <Spinner size="small" />}
</form> </form>
)} )}
@ -180,41 +186,41 @@ const setupDialog = () => {
<Text variant="s1" weight="medium">Setup cross signing</Text>, <Text variant="s1" weight="medium">Setup cross signing</Text>,
() => <CrossSigningSetup />, () => <CrossSigningSetup />,
); );
}; }
function CrossSigningReset() { function CrossSigningReset() {
const { t } = useTranslation();
return ( return (
<div className="cross-signing__reset"> <div className="cross-signing__reset">
<Text variant="h1">{twemojify('✋🧑‍🚒🤚')}</Text> <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> <Text>
Anyone you have verified with will see security alerts and your message backup will lost. {t("CrossSigning.reset_keys_message")}
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.
</Text> </Text>
<Button variant="danger" onClick={setupDialog}>Reset</Button> <Button variant="danger" onClick={setupDialog}>{t("common.reset")}</Button>
</div> </div>
); );
} }
const resetDialog = () => { const resetDialog = () => {
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">Reset cross signing</Text>, <Text variant="s1" weight="medium">Reset cross signing</Text>,
() => <CrossSigningReset />, () => <CrossSigningReset />,
); );
}; }
function CrossSignin() { function CrossSignin() {
const { t } = useTranslation();
const isCSEnabled = useCrossSigningStatus(); const isCSEnabled = useCrossSigningStatus();
return ( return (
<SettingTile <SettingTile
title="Cross signing" title={t("CrossSigning.title")}
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>} content={<Text variant="b3">{t("CrossSigning.setup_message")}</Text>}
options={( options={(
isCSEnabled isCSEnabled
? <Button variant="danger" onClick={resetDialog}>Reset</Button> ? <Button variant="danger" onClick={resetDialog}>{t("common.reset")}</Button>
: <Button variant="primary" onClick={setupDialog}>Setup</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 { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import { accessSecretStorage } from './SecretStorageAccess'; import { accessSecretStorage } from './SecretStorageAccess';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const promptDeviceName = async (deviceName) => new Promise((resolve) => { const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false; let isCompleted = false;
const { t } = useTranslation();
const renderContent = (onComplete) => { const renderContent = (onComplete) => {
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
@ -39,17 +45,17 @@ const promptDeviceName = async (deviceName) => new Promise((resolve) => {
}; };
return ( return (
<form className="device-manage__rename" onSubmit={handleSubmit}> <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"> <div className="device-manage__rename-btn">
<Button variant="primary" type="submit">Save</Button> <Button variant="primary" type="submit">{t("common.save")}</Button>
<Button onClick={() => onComplete(null)}>Cancel</Button> <Button onClick={() => onComplete(null)}>{t("common.cancel")}</Button>
</div> </div>
</form> </form>
); );
}; };
openReusableDialog( 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) => { (requestClose) => renderContent((name) => {
isCompleted = true; isCompleted = true;
resolve(name); resolve(name);
@ -76,6 +82,41 @@ function DeviceManage() {
setProcessing([]); setProcessing([]);
}, [deviceList]); }, [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 addToProcessing = (device) => {
const old = [...processing]; const old = [...processing];
old.push(device.device_id); old.push(device.device_id);
@ -91,7 +132,7 @@ function DeviceManage() {
<div className="device-manage"> <div className="device-manage">
<div className="device-manage__loading"> <div className="device-manage__loading">
<Spinner size="small" /> <Spinner size="small" />
<Text>Loading devices...</Text> <Text>{t("DeviceManage.loading_devices")}</Text>
</div> </div>
</div> </div>
); );
@ -114,14 +155,14 @@ function DeviceManage() {
const handleRemove = async (device) => { const handleRemove = async (device) => {
const isConfirmed = await confirmDialog( const isConfirmed = await confirmDialog(
`Logout ${device.display_name}`, t("DeviceManage.logout_device_title", {device: device.display_name}),
`You are about to logout "${device.display_name}" session.`, t("DeviceManage.logout_device_message", {device: device.display_name}),
'Logout', t("DeviceManage.logout_device_confirm"),
'danger', 'danger',
); );
if (!isConfirmed) return; if (!isConfirmed) return;
addToProcessing(device); 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); await mx.deleteDevice(device.device_id, auth);
}); });
@ -130,7 +171,7 @@ function DeviceManage() {
}; };
const verifyWithKey = async (device) => { const verifyWithKey = async (device) => {
const keyData = await accessSecretStorage('Session verification'); const keyData = await accessSecretStorage(t("DeviceManage.session_verification_title"));
if (!keyData) return; if (!keyData) return;
addToProcessing(device); addToProcessing(device);
await mx.checkOwnCrossSigningTrust(); await mx.checkOwnCrossSigningTrust();
@ -164,7 +205,7 @@ function DeviceManage() {
<Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}> <Text style={{ color: isVerified !== false ? '' : 'var(--tc-danger-high)' }}>
{displayName} {displayName}
<Text variant="b3" span>{`${displayName ? ' — ' : ''}${deviceId}`}</Text> <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> </Text>
)} )}
options={ options={
@ -172,9 +213,9 @@ function DeviceManage() {
? <Spinner size="small" /> ? <Spinner size="small" />
: ( : (
<> <>
{(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">Verify</Button>} {(isCSEnabled && canVerify) && <Button onClick={() => verify(deviceId, isCurrentDevice)} variant="positive">{t("DeviceManage.verify_session_button")}</Button>}
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" /> <IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip={t("DeviceManage.edit_session_name_tooltip")} />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" /> <IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip={t("DeviceManage.logout_device_tooltip")}/>
</> </>
) )
} }
@ -211,49 +252,50 @@ function DeviceManage() {
noEncryption.push(device); noEncryption.push(device);
} }
}); });
return ( return (
<div className="device-manage"> <div className="device-manage">
<div> <div>
<MenuHeader>Unverified sessions</MenuHeader> <MenuHeader>{t("DeviceManage.unverified_sessions_title")}</MenuHeader>
{!isCSEnabled && ( {!isCSEnabled && (
<div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}> <div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
<InfoCard <InfoCard
rounded rounded
variant="caution" variant="caution"
iconSrc={InfoIC} iconSrc={InfoIC}
title="Setup cross signing in case you lose all your sessions." title={t("DeviceManage.setup_cross_signing_message")}
/> />
</div> </div>
)} )}
{ {
unverified.length > 0 unverified.length > 0
? unverified.map((device) => renderDevice(device, false)) ? 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> </div>
{noEncryption.length > 0 && ( {noEncryption.length > 0 && (
<div> <div>
<MenuHeader>Sessions without encryption support</MenuHeader> <MenuHeader>{t("DeviceManage.unencrypted_sessions_title")}</MenuHeader>
{noEncryption.map((device) => renderDevice(device, null))} {noEncryption.map((device) => renderDevice(device, null))}
</div> </div>
)} )}
<div> <div>
<MenuHeader>Verified sessions</MenuHeader> <MenuHeader>{t("DeviceManage.verified_sessions_title")}</MenuHeader>
{ {
verified.length > 0 verified.length > 0
? verified.map((device, index) => { ? verified.map((device, index) => {
if (truncated && index >= TRUNCATED_COUNT) return null; if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true); 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 && ( { verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}> <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> </Button>
)} )}
{ deviceList.length > 0 && ( { 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>
</div> </div>

View file

@ -24,11 +24,17 @@ import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus'; import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function CreateKeyBackupDialog({ keyData }) { function CreateKeyBackupDialog({ keyData }) {
const [done, setDone] = useState(false); const [done, setDone] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
const { t } = useTranslation();
const doBackup = async () => { const doBackup = async () => {
setDone(false); setDone(false);
let info; let info;
@ -60,19 +66,19 @@ function CreateKeyBackupDialog({ keyData }) {
{done === false && ( {done === false && (
<div> <div>
<Spinner size="small" /> <Spinner size="small" />
<Text>Creating backup...</Text> <Text>{t("KeyBackup.creating_backup")}</Text>
</div> </div>
)} )}
{done === true && ( {done === true && (
<> <>
<Text variant="h1">{twemojify('✅')}</Text> <Text variant="h1">{twemojify('✅')}</Text>
<Text>Successfully created backup</Text> <Text>{t("KeyBackup.backup_created")}</Text>
</> </>
)} )}
{done === null && ( {done === null && (
<> <>
<Text>Failed to create backup</Text> <Text>{t("KeyBackup.backup_failed")}</Text>
<Button onClick={doBackup}>Retry</Button> <Button onClick={doBackup}>{t("common.retry")}</Button>
</> </>
)} )}
</div> </div>
@ -83,6 +89,9 @@ CreateKeyBackupDialog.propTypes = {
}; };
function RestoreKeyBackupDialog({ keyData }) { function RestoreKeyBackupDialog({ keyData }) {
const { t } = useTranslation();
const [status, setStatus] = useState(false); const [status, setStatus] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
@ -99,7 +108,7 @@ function RestoreKeyBackupDialog({ keyData }) {
meBreath = true; meBreath = true;
}, 200); }, 200);
setStatus({ message: `Restoring backup keys... (${progress.successes}/${progress.total})` }); setStatus({ message: t("KeyBackup.restoring_progress", {progress: progress.successes, total: progress.total}) });
}; };
try { try {
@ -111,14 +120,14 @@ function RestoreKeyBackupDialog({ keyData }) {
{ progressCallback }, { progressCallback },
); );
if (!mountStore.getItem()) return; 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) { } catch (e) {
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') { if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') {
deletePrivateKey(keyData.keyId); 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 { } 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) && ( {(status === false || status.message) && (
<div> <div>
<Spinner size="small" /> <Spinner size="small" />
<Text>{status.message ?? 'Restoring backup keys...'}</Text> <Text>{status.message ?? t("KeyBackup.restoring")}</Text>
</div> </div>
)} )}
{status.done && ( {status.done && (
@ -145,7 +154,7 @@ function RestoreKeyBackupDialog({ keyData }) {
{status.error && ( {status.error && (
<> <>
<Text>{status.error}</Text> <Text>{status.error}</Text>
<Button onClick={restoreBackup}>Retry</Button> <Button onClick={restoreBackup}>{t("common.retry")}}</Button>
</> </>
)} )}
</div> </div>
@ -159,6 +168,7 @@ function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const mountStore = useStore(); const mountStore = useStore();
const { t } = useTranslation();
const deleteBackup = async () => { const deleteBackup = async () => {
mountStore.setItem(true); mountStore.setItem(true);
@ -177,12 +187,12 @@ function DeleteKeyBackupDialog({ requestClose }) {
return ( return (
<div className="key-backup__delete"> <div className="key-backup__delete">
<Text variant="h1">{twemojify('🗑')}</Text> <Text variant="h1">{twemojify('🗑')}</Text>
<Text weight="medium">Deleting key backup is permanent.</Text> <Text weight="medium">{t("KeyBackup.delete_key_backup_subtitle")}</Text>
<Text>All encrypted messages keys stored on server will be deleted.</Text> <Text>{t("KeyBackup.delete_key_backup_message")}</Text>
{ {
isDeleting isDeleting
? <Spinner size="small" /> ? <Spinner size="small" />
: <Button variant="danger" onClick={deleteBackup}>Delete</Button> : <Button variant="danger" onClick={deleteBackup}>{t("common.delete")}</Button>
} }
</div> </div>
); );
@ -196,6 +206,7 @@ function KeyBackup() {
const isCSEnabled = useCrossSigningStatus(); const isCSEnabled = useCrossSigningStatus();
const [keyBackup, setKeyBackup] = useState(undefined); const [keyBackup, setKeyBackup] = useState(undefined);
const mountStore = useStore(); const mountStore = useStore();
const { t } = useTranslation();
const fetchKeyBackupVersion = async () => { const fetchKeyBackupVersion = async () => {
const info = await mx.getKeyBackupVersion(); const info = await mx.getKeyBackupVersion();
@ -220,28 +231,28 @@ function KeyBackup() {
}, [isCSEnabled]); }, [isCSEnabled]);
const openCreateKeyBackup = async () => { const openCreateKeyBackup = async () => {
const keyData = await accessSecretStorage('Create Key Backup'); const keyData = await accessSecretStorage(t('KeyBackup.create_backup_title'));
if (keyData === null) return; if (keyData === null) return;
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">Create Key Backup</Text>, <Text variant="s1" weight="medium">{t('KeyBackup.create_backup_title')}</Text>,
() => <CreateKeyBackupDialog keyData={keyData} />, () => <CreateKeyBackupDialog keyData={keyData} />,
() => fetchKeyBackupVersion(), () => fetchKeyBackupVersion(),
); );
}; };
const openRestoreKeyBackup = async () => { const openRestoreKeyBackup = async () => {
const keyData = await accessSecretStorage('Restore Key Backup'); const keyData = await accessSecretStorage(t('KeyBackup.restore_backup_title'));
if (keyData === null) return; if (keyData === null) return;
openReusableDialog( openReusableDialog(
<Text variant="s1" weight="medium">Restore Key Backup</Text>, <Text variant="s1" weight="medium">{t('KeyBackup.restore_backup_title')}</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} />, () => <RestoreKeyBackupDialog keyData={keyData} />,
); );
}; };
const openDeleteKeyBackup = () => openReusableDialog( 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) => ( (requestClose) => (
<DeleteKeyBackupDialog <DeleteKeyBackupDialog
requestClose={(isDone) => { requestClose={(isDone) => {
@ -254,28 +265,28 @@ function KeyBackup() {
const renderOptions = () => { const renderOptions = () => {
if (keyBackup === undefined) return <Spinner size="small" />; 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 ( return (
<> <>
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" /> <IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip={t('KeyBackup.restore_backup_tooltip')} />
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" /> <IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip={t('KeyBackup.delete_key_backup_tooltip')} />
</> </>
); );
}; };
return ( return (
<SettingTile <SettingTile
title="Encrypted messages backup" title={t("KeyBackup.encrypted_messages_backup_title")}
content={( 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 && ( {!isCSEnabled && (
<InfoCard <InfoCard
style={{ marginTop: 'var(--sp-ultra-tight)' }} style={{ marginTop: 'var(--sp-ultra-tight)' }}
rounded rounded
variant="caution" variant="caution"
iconSrc={InfoIC} 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 Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner'; import Spinner from '../../atoms/spinner/Spinner';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
function SecretStorageAccess({ onComplete }) { function SecretStorageAccess({ onComplete }) {
@ -24,6 +29,7 @@ function SecretStorageAccess({ onComplete }) {
const [process, setProcess] = useState(false); const [process, setProcess] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const mountStore = useStore(); const mountStore = useStore();
const { t } = useTranslation();
const toggleWithPhrase = () => setWithPhrase(!withPhrase); const toggleWithPhrase = () => setWithPhrase(!withPhrase);
@ -39,7 +45,7 @@ function SecretStorageAccess({ onComplete }) {
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
if (!isCorrect) { if (!isCorrect) {
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`); setError(t(key ? "SecretStorageAccess.incorrect_security_key" : "SecretStorageAccess.incorrect_security_phrase"));
setProcess(false); setProcess(false);
return; return;
} }
@ -51,7 +57,7 @@ function SecretStorageAccess({ onComplete }) {
}); });
} catch (e) { } catch (e) {
if (!mountStore.getItem()) return; if (!mountStore.getItem()) return;
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`); setError(t(key ? "SecretStorageAccess.incorrect_security_key" : "SecretStorageAccess.incorrect_security_phrase"));
setProcess(false); setProcess(false);
} }
}; };
@ -76,7 +82,7 @@ function SecretStorageAccess({ onComplete }) {
<form onSubmit={handleForm}> <form onSubmit={handleForm}>
<Input <Input
name="password" name="password"
label={`Security ${withPhrase ? 'Phrase' : 'Key'}`} label={t(withPhrase ? "SecretStorageAccess.security_phrase" : "SecretStorageAccess.security_key")}
type="password" type="password"
onChange={handleChange} onChange={handleChange}
required required
@ -84,8 +90,8 @@ function SecretStorageAccess({ onComplete }) {
{error && <Text variant="b3">{error}</Text>} {error && <Text variant="b3">{error}</Text>}
{!process && ( {!process && (
<div className="secret-storage-access__btn"> <div className="secret-storage-access__btn">
<Button variant="primary" type="submit">Continue</Button> <Button variant="primary" type="submit">{t("common.continue")}</Button>
{isPassphrase && <Button onClick={toggleWithPhrase}>{`Use Security ${withPhrase ? 'Key' : 'Phrase'}`}</Button>} {isPassphrase && <Button onClick={toggleWithPhrase}>{t( withPhrase ? "SecretStorageAccess.use_security_key" : "SecretStorageAccess.use_security_phrase")}</Button>}
</div> </div>
)} )}
</form> </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 CinnySVG from '../../../../public/res/svg/cinny.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function AppearanceSection() { function AppearanceSection() {
const [, updateState] = useState({}); const [, updateState] = useState({});
const { t } = useTranslation();
return ( return (
<div className="settings-appearance"> <div className="settings-appearance">
<div className="settings-appearance__card"> <div className="settings-appearance__card">
<MenuHeader>Theme</MenuHeader> <MenuHeader>Theme</MenuHeader>
<SettingTile <SettingTile
title="Follow system theme" title={t("Settings.theme.follow_system.title")}
options={( options={(
<Toggle <Toggle
isActive={settings.useSystemTheme} isActive={settings.useSystemTheme}
onToggle={() => { toggleSystemTheme(); updateState({}); }} 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 && ( {!settings.useSystemTheme && (
<SettingTile <SettingTile
title="Theme" title={t("Settings.theme.title")}
content={( content={(
<SegmentedControls <SegmentedControls
selected={settings.getThemeIndex()} selected={settings.getThemeIndex()}
segments={[ segments={[
{ text: 'Light' }, { text: t("Settings.theme.theme_light") },
{ text: 'Silver' }, { text: t("Settings.theme.theme_silver") },
{ text: 'Dark' }, { text: t("Settings.theme.theme_dark") },
{ text: 'Butter' }, { text: t("Settings.theme.theme_butter") },
]} ]}
onSelect={(index) => settings.setTheme(index)} onSelect={(index) => settings.setTheme(index)}
/> />
@ -78,34 +83,34 @@ function AppearanceSection() {
<div className="settings-appearance__card"> <div className="settings-appearance__card">
<MenuHeader>Room messages</MenuHeader> <MenuHeader>Room messages</MenuHeader>
<SettingTile <SettingTile
title="Markdown formatting" title={t("Settings.markdown.title")}
options={( options={(
<Toggle <Toggle
isActive={settings.isMarkdown} isActive={settings.isMarkdown}
onToggle={() => { toggleMarkdown(); updateState({}); }} 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 <SettingTile
title="Hide membership events" title={t("Settings.hide_membership_events.title")}
options={( options={(
<Toggle <Toggle
isActive={settings.hideMembershipEvents} isActive={settings.hideMembershipEvents}
onToggle={() => { toggleMembershipEvents(); updateState({}); }} 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 <SettingTile
title="Hide nick/avatar events" title={t("Settings.hide_nickname_avatar_events.title")}
options={( options={(
<Toggle <Toggle
isActive={settings.hideNickAvatarEvents} isActive={settings.hideNickAvatarEvents}
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }} 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>
</div> </div>
@ -117,9 +122,11 @@ function NotificationsSection() {
const [, updateState] = useState({}); const [, updateState] = useState({});
const { t } = useTranslation();
const renderOptions = () => { const renderOptions = () => {
if (window.Notification === undefined) { 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') { if (permission === 'granted') {
@ -147,51 +154,54 @@ function NotificationsSection() {
return ( return (
<div className="settings-notifications"> <div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader> <MenuHeader>{t("Settings.notifications_and_sound.title")}</MenuHeader>
<SettingTile <SettingTile
title="Desktop notification" title={t("Settings.notifications_and_sound.desktop.title")}
options={renderOptions()} 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 <SettingTile
title="Notification Sound" title={t("Settings.notifications_and_sound.sound.title")}
options={( options={(
<Toggle <Toggle
isActive={settings.isNotificationSounds} isActive={settings.isNotificationSounds}
onToggle={() => { toggleNotificationSounds(); updateState({}); }} 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> </div>
); );
} }
function SecuritySection() { function SecuritySection() {
const { t } = useTranslation();
return ( return (
<div className="settings-security"> <div className="settings-security">
<div className="settings-security__card"> <div className="settings-security__card">
<MenuHeader>Cross signing and backup</MenuHeader> <MenuHeader>{t("Settings.security.cross_signing.title")}</MenuHeader>
<CrossSigning /> <CrossSigning />
<KeyBackup /> <KeyBackup />
</div> </div>
<DeviceManage /> <DeviceManage />
<div className="settings-security__card"> <div className="settings-security__card">
<MenuHeader>Export/Import encryption keys</MenuHeader> <MenuHeader>{t("Settings.security.export_import_encryption_keys.title")}</MenuHeader>
<SettingTile <SettingTile
title="Export E2E room keys" title={t("Settings.security.export_encryption_keys.title")}
content={( 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 /> <ExportE2ERoomKeys />
</> </>
)} )}
/> />
<SettingTile <SettingTile
title="Import E2E room keys" title={t("Settings.security.import_encryption_keys.title")}
content={( 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 /> <ImportE2ERoomKeys />
</> </>
)} )}
@ -202,28 +212,31 @@ function SecuritySection() {
} }
function AboutSection() { function AboutSection() {
const { t } = useTranslation();
return ( return (
<div className="settings-about"> <div className="settings-about">
<div className="settings-about__card"> <div className="settings-about__card">
<MenuHeader>Application</MenuHeader> <MenuHeader>{t("Settings.about.application")}</MenuHeader>
<div className="settings-about__branding"> <div className="settings-about__branding">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" /> <img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
<div> <div>
<Text variant="h2" weight="medium"> <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> <span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
</Text> </Text>
<Text>Yet another matrix client</Text> <Text>{t("common.slogan")}</Text>
<div className="settings-about__btns"> <div className="settings-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button> <Button onClick={() => window.open('https://github.com/ajbura/cinny')}>{t("common.source_code")}</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button> <Button onClick={() => window.open('https://cinny.in/#sponsor')}>{t("common.sponsor")}</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="settings-about__card"> <div className="settings-about__card">
<MenuHeader>Credits</MenuHeader> <MenuHeader>{t("Settings.about.credits")}</MenuHeader>
<div className="settings-about__credits"> <div className="settings-about__credits">
<ul> <ul>
<li> <li>
@ -297,9 +310,11 @@ function Settings() {
const [selectedTab, setSelectedTab] = useState(tabItems[0]); const [selectedTab, setSelectedTab] = useState(tabItems[0]);
const [isOpen, requestClose] = useWindowToggle(setSelectedTab); const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
const { t } = useTranslation();
const handleTabChange = (tabItem) => setSelectedTab(tabItem); const handleTabChange = (tabItem) => setSelectedTab(tabItem);
const handleLogout = async () => { 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(); logout();
} }
}; };
@ -308,13 +323,13 @@ function Settings() {
<PopupWindow <PopupWindow
isOpen={isOpen} isOpen={isOpen}
className="settings-window" className="settings-window"
title={<Text variant="s1" weight="medium" primary>Settings</Text>} title={<Text variant="s1" weight="medium" primary>{t("Settings.title")}</Text>}
contentOptions={( contentOptions={(
<> <>
<Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}> <Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
Logout {t("Settings.logout.title")}
</Button> </Button>
<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" /> <IconButton src={CrossIC} onClick={requestClose} tooltip={t("common.close")} />
</> </>
)} )}
onRequestClose={requestClose} onRequestClose={requestClose}

View file

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

View file

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

View file

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

View file

@ -56,15 +56,15 @@ function ViewSource() {
const renderViewSource = () => ( const renderViewSource = () => (
<div className="view-source"> <div className="view-source">
{event.isEncrypted() && <ViewSourceBlock title={t("view_source.decrypted_source")} json={event.getEffectiveEvent()} />} {event.isEncrypted() && <ViewSourceBlock title={t("ViewSource.decrypted_source")} json={event.getEffectiveEvent()} />}
<ViewSourceBlock title={t("view_source.original_source")} json={event.event} /> <ViewSourceBlock title={t("ViewSource.original_source")} json={event.event} />
</div> </div>
); );
return ( return (
<PopupWindow <PopupWindow
isOpen={isOpen} isOpen={isOpen}
title={t("view_source.title")} title={t("ViewSource.title")}
onAfterClose={handleAfterClose} onAfterClose={handleAfterClose}
onRequestClose={() => setIsOpen(false)} onRequestClose={() => setIsOpen(false)}
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip={t("common.close")} />} 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 className="app-welcome flex--center">
<div> <div>
<img className="app-welcome__logo noselect" src={CinnySvg} alt="Cinny logo" /> <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__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__subheading" variant="s1">{t('Welcome.subheading')}</Text>
</div> </div>
</div> </div>
); );