Added translation to all molecules

This commit is contained in:
Dylan 2022-07-14 19:24:19 +09:30
parent 71bc3ea41e
commit 3668342ca7
29 changed files with 717 additions and 214 deletions

View file

@ -334,6 +334,18 @@
"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?", "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?" "demoting_self_message": "You will not be able to undo this change as you are demoting yourself. Are you sure?"
}, },
"PeopleDrawer": {
"title": "People",
"members_one": "{{count}} member",
"members_other": "{{count}} members",
"joined": "Joined",
"invited": "Invited",
"banned": "Banned",
"search_no_results": "No results found",
"view_more": "View more",
"placeholder": "Search",
"invite_tooltip": "Invite"
},
"ProfileEditor": { "ProfileEditor": {
"remove_avatar": "Remove avatar", "remove_avatar": "Remove avatar",
"remove_avatar_confirmation": "Are you sure that you want to remove your avatar?", "remove_avatar_confirmation": "Are you sure that you want to remove your avatar?",
@ -439,5 +451,326 @@
"create_space": "Create space", "create_space": "Create space",
"home": "Home" "home": "Home"
} }
},
"Molecules":{
"ConfirmDialog":{
"cancel": "Cancel"
},
"ReusableDialog":
{
"close_tooltip": "Close"
},
"FollowingMembers":{
"users_following_one": "<bold>{{user_one}}</bold> is following the conversation",
"users_following_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> are following the conversation",
"users_following_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, and <bold>{{user_three}}</bold> are following the conversation",
"users_following_other": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and {{other_count}} others are following the conversation"
},
"ImageUpload":
{
"prompt": "Upload",
"cancel": "Cancel",
"remove": "Remove"
},
"ExportE2ERoomKeys":
{
"getting_keys": "Getting keys...",
"encrypting_keys": "Encrypting Keys...",
"password_does_not_match": "Password does not match.",
"export_success": "Successfully exported all keys.",
"export_failed": "Failed to export keys. Please try again.",
"button_text": "Export"
},
"ImportE2ERoomKeys":
{
"decrypting_file": "Decrypting file...",
"decrypting_messages": "Decrypting messages...",
"import_success": "Successfully imported all keys",
"import_failed": "Failed tp decrypt keys. Please try again",
"import_keys_button": "Import Keys",
"decrypt_button": "Decrypt"
},
"Media":{
"open_new_tab": "Open in new tab",
"download": "Download",
"play_audio": "Play audio",
"play_video": "Play video"
},
"Message":
{
"message_deleted": "*** This message has been deleted ***",
"unable_to_load_reply": "*** Unable to load reply ***",
"unknown_user": "** Unknown user **",
"edited": "(edited)",
"edit_placeholder": "Edit message",
"user_reacted_one": "<bold>{{user_one}}</bold> reacted with <emoji/>",
"user_reacted_two": "<bold>{{user_one}}</bold> and <bold>{{user_two}}</bold> reacted with <emoji/>",
"user_reacted_three": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold> and <bold>{{user_three}}</bold> reacted with <emoji/>",
"user_reacted_other": "<bold>{{user_one}}</bold>, <bold>{{user_two}}</bold>, <bold>{{user_three}}</bold> and {{other_count}} others reacted with <emoji/>",
"add_reaction_tooltip": "Add reaction",
"reply_tooltip": "Reply",
"edit_tooltip": "Edit",
"options_header": "Options",
"options_tooltip": "Options",
"read_receipts": "Read receipts",
"view_source": "View source",
"delete_message_prompt": "Delete message",
"delete_message_confirmation": "Are you sure that you want to delete this message?",
"delete_message_button": "Delete"
},
"PopupWindow":
{
"close_tooltip": "Back"
},
"PowerLevelSelector": {
"placeholder": "Power level"
},
"RoomAliases": {
"invalid_characters": "Invalid character: only letter, numbers and _- are allowed.",
"validating_alias": "validating {{alias}}...",
"alias_available": "{{alias}} is available.",
"alias_unavailable": "{{alias}} is unavailable.",
"deleting_alias": "Deleting...",
"set_main_alias": "Set as Main",
"publish_alias": "Publish",
"unpublish_alias": "Unpublish",
"delete_alias": "Delete",
"main_alias": "Main",
"publish_to_room_directory":
{
"title": "Publish to room directory",
"publish_room_message": "Publish this room to the {{homeserver}} publish directory?",
"publish_space_message": "Publish this space to the {{homeserver}} publish directory?"
},
"published_addresses":
{
"title": "Published addresses",
"none": "No published addresses",
"no_main_address": "No main address (select one from below)",
"message_room": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
"message_space": "Published addresses can be used by anyone on any server to join your space. To publish an address, it needs to be set as a local address first."
},
"local_addresses":
{
"title": "Local addresses",
"none": "No local addresses",
"message_room": "Set local addresses for this room so users can find this room through your homeserver.",
"message_space": "Set local addresses for this space so users can find this space through your homeserver.",
"add": "Add local address",
"add_button": "Add",
"placeholder_room": "my_room_address",
"placeholder_space": "my_space_address",
"hide": "Hide local addresses",
"view": "View local addresses"
}
},
"RoomEncryption":{
"encryption_public_room_message": "It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone should be able to read messages in them.",
"encryption_message": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly",
"encryption_cannot_be_disabled": "Once enabled, encryption cannot be disabled.",
"enable_room_encryption": "Enable room encryption",
"enable_encryption_prompt": "Enable Encryption",
"enable_encryption_button": "Enable",
"continue_button": "Continue"
},
"RoomHistoryVisibility": {
"world_readable": "Anyone (including guests)",
"shared": "Members (all messages)",
"invited": "Members (messages after invite)",
"joined": "Members (messages after join)",
"changes_only_affect_future": "Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect."
},
"RoomMembers": {
"search_title": "Search member",
"found_members_one": "Found — one member",
"found_members_other": "Found — {{count}} members",
"no_results": "No results found for {{term}}",
"no_members": "No members to display",
"view_more": "View more",
"joined": "Joined",
"invited": "Invited",
"banned": "Banned",
"search_placeholder": "Search for name"
},
"RoomNotification":
{
"default": "Global",
"all_messages": "All messages",
"mentions_and_keywords": "Mentions & Keywords",
"mute": "Mute"
},
"RoomOptions":{
"title": "Options for {{room_name}}",
"leave": {
"title": "Leave room",
"subtitle": "Are you sure you want to leave the {{room_name}} room?",
"button_text": "Leave"
},
"mark_as_read": "Mark as read",
"notifications_heading": "Notifications",
"invite": "Invite"
},
"RoomPermissions":
{
"default_role": {
"name": "Default role",
"description": "Set default role for all members"
},
"send_messages": {
"name": "Send messages",
"description": "Set minimum power level to send messages in a room"
},
"reactions":{
"name": "Send reactions",
"description": "Set minimum power level to send reactions in a room"
},
"delete": {
"name": "Delete messages sent by others",
"description": "Set minumum power level to delete messages in a room"
},
"notifications":{
"name": "Ping room",
"description": "Set minimum power level to ping room"
},
"manage_rooms":{
"name": "Manage rooms in space",
"description": "Set minimum power level to manage rooms in space"
},
"invite": {
"name": "Invite",
"description": "Set minimum power level to invite members"
},
"kick": {
"name": "Kick",
"description": "Set minimum power level to kick members"
},
"ban": {
"name": "Ban",
"description": "Set minimum power level to ban members"
},
"change_avatar": {
"name": "Change avatar",
"description": "Set minimum power level to change room/space avatar"
},
"change_name": {
"name": "Change name",
"description": "Set minimum power evel to change room/space name"
},
"change_topic": {
"name": "Change topic",
"description": "Set minimum power level to change room/space topic"
},
"change_settings": {
"name": "Change settings",
"description": "Set minimum power level to change settings"
},
"change_published_address": {
"name": "Change published address",
"description": "Set minimum power level to publish and set main address"
},
"change_permissions": {
"name": "Change permissions",
"description": "Set minimum power level to change permissions"
},
"enable_room_encryption": {
"name": "Enable room encryption",
"description": "Set minimum power level to enable room encryption"
},
"change_history_visibility": {
"name": "Change history visibility",
"description": "Set minimum power level to change room message history visibility"
},
"upgrade_room": {
"name": "Upgrade room",
"description": "Set minimum power level to upgrade room"
},
"pin_messages": {
"name": "Pin messages",
"description": "Set minimum power level to pin messages in a room"
},
"change_acls": {
"name": "Change server ACLs",
"description": "Set minimum power level to change server ACLs"
},
"modify_widgets": {
"name": "Modify widgets",
"description": "Set minimum power level to modify room widgets"
},
"groups":
{
"general": "General permissions",
"manage_members": "Manage members permissions",
"room": "Room profile permissions",
"space": "Space profile permissions",
"other": "Other permissions",
"settings": "Settings permissions"
}
},
"RoomProfile": {
"saving_room_name": "Saving room name...",
"saving_room_topic": "Saving room topic...",
"save_success": "Saved successfully",
"save_failed": "Unable to save",
"remove_avatar_title": "Remove avatar",
"remove_avatar_subtitle": "Are you sure that you want to remove room avatar?",
"remove_avatar_button": "Remove",
"permission_change_room_name": "You have permission to change room name only",
"permission_change_room_topic": "You have permission to change room topic only",
"permission_change_space_name": "You have permission to change space name only",
"permission_change_space_topic": "You have permission to change space topic only",
"name_label": "Name",
"topic_label": "Topic"
},
"RoomSearch": {
"title": "Room search",
"placeholder": "Search for keywords",
"search_button": "Search",
"searching": "Searching room messages...",
"subtitle": "Search room messages",
"failed": "Failed to search messages",
"no_results": "No results found",
"encrypted_room": "Search does not work in encrypted room",
"load_more": "Load more",
"results_one": "{{count}} result for {{term}}",
"results_other": "{{count}} results for {{term}}"
},
"RoomTile": {
"invited_by_user_zero": "Invited by {{inviter}} to {{id}}",
"invited_by_user_one": "Invited by {{inviter}} to {{id}} • {{member_count}} member",
"invited_by_user_other": "Invited by {{inviter}} to {{id}} • {{member_count}} members",
"invited_zero": "{{id}}",
"invited_one": "{{id}} • {{member_count}} member",
"invited_other": "{{id}} • {{member_count}} members"
},
"RoomVisibility": {
"private": "Private (invite only)",
"restricted": "Restricted (space members can join)",
"restricted_unsupported": "Restricted (Unsupported: room required upgrade)",
"public": "Public (anyone can join)"
},
"SpaceAddExisting": {
"adding_items_one": "Adding one item...",
"adding_items_other": "Adding {{count}} items...",
"items_selected_one": "{{count}} item selected",
"items_selected_other": "{{count}} items selected",
"search_rooms_placeholder": "Search rooms",
"no_results": "No results found",
"add_button": "Add",
"subtitle": "add existing rooms"
},
"SpaceOptions": {
"leave_space": "Leave Space",
"leave_space_confirmation": "Are you sure that you want to leave the {{space}} space?",
"leave_space_confirm": "Leave",
"invite": "Invite",
"manage_rooms":"Manage rooms",
"settings": "Settings",
"leave": "Leave"
},
"SSOButtons": {
"login_with": "Login with {{idp_name}}"
}
} }
} }

View file

@ -7,15 +7,20 @@ import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ConfirmDialog({ function ConfirmDialog({
desc, actionTitle, actionType, onComplete, desc, actionTitle, actionType, onComplete,
}) { }) {
const { t } = useTranslation();
return ( return (
<div className="confirm-dialog"> <div className="confirm-dialog">
<Text>{desc}</Text> <Text>{desc}</Text>
<div className="confirm-dialog__btn"> <div className="confirm-dialog__btn">
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button> <Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
<Button onClick={() => onComplete(false)}>Cancel</Button> <Button onClick={() => onComplete(false)}>{t("Molecules.ConfirmDialog.cancel")}</Button>
</div> </div>
</div> </div>
); );

View file

@ -8,10 +8,15 @@ import Dialog from './Dialog';
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 ReusableDialog() { function ReusableDialog() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null); const [data, setData] = useState(null);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const handleOpen = (title, render, afterClose) => { const handleOpen = (title, render, afterClose) => {
setIsOpen(true); setIsOpen(true);
@ -38,7 +43,7 @@ function ReusableDialog() {
title={data?.title || ''} title={data?.title || ''}
onAfterClose={handleAfterClose} onAfterClose={handleAfterClose}
onRequestClose={handleRequestClose} onRequestClose={handleRequestClose}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />} contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip={t("Molecules.ReusableDialog.close_tooltip")} />}
invisibleScroll invisibleScroll
> >
{data?.render(handleRequestClose) || <div />} {data?.render(handleRequestClose) || <div />}

View file

@ -13,12 +13,22 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import { getUsersActionJsx } from '../../organisms/room/common'; import { getUsersActionJsx } from '../../organisms/room/common';
import { twemojify } from '../../../util/twemojify';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import { getUserDisplayName } from '../../../util/matrixUtil';
function FollowingMembers({ roomTimeline }) { function FollowingMembers({ roomTimeline }) {
const [followingMembers, setFollowingMembers] = useState([]); const [followingMembers, setFollowingMembers] = useState([]);
const { roomId } = roomTimeline; const { roomId } = roomTimeline;
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { roomsInput } = initMatrix; const { roomsInput } = initMatrix;
const myUserId = mx.getUserId(); const myUserId = mx.getUserId();
const room = mx.getRoom(roomId);
const handleOnMessageSent = () => setFollowingMembers([]); const handleOnMessageSent = () => setFollowingMembers([]);
@ -47,7 +57,19 @@ function FollowingMembers({ roomTimeline }) {
size="extra-small" size="extra-small"
src={TickMarkIC} src={TickMarkIC}
/> />
<Text variant="b2">{getUsersActionJsx(roomId, filteredM, 'following the conversation.')}</Text> <Text variant="b2">
<Trans
i18nKey="Molecules.FollowingMembers.users_following"
values={{
count: filteredM.length,
user_one: twemojify(getUserDisplayName(room, filteredM?.[0])),
user_two: twemojify(getUserDisplayName(room, filteredM?.[1])),
user_three: twemojify(getUserDisplayName(room, filteredM?.[2])),
other_count: filteredM.length - 3
}}
components={{bold: <b/>}}
/>
</Text>
</button> </button>
); );
} }

View file

@ -8,12 +8,17 @@ import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar'; import Avatar from '../../atoms/avatar/Avatar';
import Spinner from '../../atoms/spinner/Spinner'; import Spinner from '../../atoms/spinner/Spinner';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ImageUpload({ function ImageUpload({
text, bgColor, imageSrc, onUpload, onRequestRemove, text, bgColor, imageSrc, onUpload, onRequestRemove,
}) { }) {
const [uploadPromise, setUploadPromise] = useState(null); const [uploadPromise, setUploadPromise] = useState(null);
const uploadImageRef = useRef(null); const uploadImageRef = useRef(null);
const { t } = useTranslation();
async function uploadImage(e) { async function uploadImage(e) {
const file = e.target.files.item(0); const file = e.target.files.item(0);
if (file === null) return; if (file === null) return;
@ -53,7 +58,7 @@ function ImageUpload({
size="large" size="large"
/> />
<div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}> <div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
{uploadPromise === null && <Text variant="b3" weight="bold">Upload</Text>} {uploadPromise === null && <Text variant="b3" weight="bold">{t("Molecules.ImageUpload.prompt")}</Text>}
{uploadPromise !== null && <Spinner size="small" />} {uploadPromise !== null && <Spinner size="small" />}
</div> </div>
</button> </button>
@ -63,7 +68,7 @@ function ImageUpload({
type="button" type="button"
onClick={uploadPromise === null ? onRequestRemove : cancelUpload} onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
> >
<Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text> <Text variant="b3">{uploadPromise ? t("Molecules.ImageUpload.cancel") : t("Molecules.ImageUpload.remove")}</Text>
</button> </button>
)} )}
<input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" /> <input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" />

View file

@ -14,6 +14,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';
function ExportE2ERoomKeys() { function ExportE2ERoomKeys() {
const isMountStore = useStore(); const isMountStore = useStore();
const [status, setStatus] = useState({ const [status, setStatus] = useState({
@ -24,19 +27,21 @@ function ExportE2ERoomKeys() {
const passwordRef = useRef(null); const passwordRef = useRef(null);
const confirmPasswordRef = useRef(null); const confirmPasswordRef = useRef(null);
const { t } = useTranslation();
const exportE2ERoomKeys = async () => { const exportE2ERoomKeys = async () => {
const password = passwordRef.current.value; const password = passwordRef.current.value;
if (password !== confirmPasswordRef.current.value) { if (password !== confirmPasswordRef.current.value) {
setStatus({ setStatus({
isOngoing: false, isOngoing: false,
msg: 'Password does not match.', msg: t("Molecules.ExportE2ERoomKeys.password_does_not_match"),
type: cons.status.ERROR, type: cons.status.ERROR,
}); });
return; return;
} }
setStatus({ setStatus({
isOngoing: true, isOngoing: true,
msg: 'Getting keys...', msg: t("Molecules.ExportE2ERoomKeys.getting_keys"),
type: cons.status.IN_FLIGHT, type: cons.status.IN_FLIGHT,
}); });
try { try {
@ -44,7 +49,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: true, isOngoing: true,
msg: 'Encrypting keys...', msg: t("Molecules.ExportE2ERoomKeys.encrypting_keys"),
type: cons.status.IN_FLIGHT, type: cons.status.IN_FLIGHT,
}); });
} }
@ -56,7 +61,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: false, isOngoing: false,
msg: 'Successfully exported all keys.', msg: t("Molecules.ExportE2ERoomKeys.export_success"),
type: cons.status.SUCCESS, type: cons.status.SUCCESS,
}); });
} }
@ -64,7 +69,7 @@ function ExportE2ERoomKeys() {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: false, isOngoing: false,
msg: e.friendlyText || 'Failed to export keys. Please try again.', msg: e.friendlyText || t("Molecules.ExportE2ERoomKeys.export_failed"),
type: cons.status.ERROR, type: cons.status.ERROR,
}); });
} }
@ -83,7 +88,7 @@ function ExportE2ERoomKeys() {
<form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}> <form className="export-e2e-room-keys__form" onSubmit={(e) => { e.preventDefault(); exportE2ERoomKeys(); }}>
<Input forwardRef={passwordRef} type="password" placeholder="Password" required /> <Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required /> <Input forwardRef={confirmPasswordRef} type="password" placeholder="Confirm password" required />
<Button disabled={status.isOngoing} variant="primary" type="submit">Export</Button> <Button disabled={status.isOngoing} variant="primary" type="submit">{t("Molecules.ExportE2ERoomKeys.button_text")}</Button>
</form> </form>
{ status.type === cons.status.IN_FLIGHT && ( { status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process"> <div className="import-e2e-room-keys__process">

View file

@ -15,6 +15,9 @@ import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function ImportE2ERoomKeys() { function ImportE2ERoomKeys() {
const isMountStore = useStore(); const isMountStore = useStore();
const [keyFile, setKeyFile] = useState(null); const [keyFile, setKeyFile] = useState(null);
@ -26,13 +29,15 @@ function ImportE2ERoomKeys() {
const inputRef = useRef(null); const inputRef = useRef(null);
const passwordRef = useRef(null); const passwordRef = useRef(null);
const { t } = useTranslation();
async function tryDecrypt(file, password) { async function tryDecrypt(file, password) {
try { try {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: true, isOngoing: true,
msg: 'Decrypting file...', msg: t("Molecules.ImportE2ERoomKeys.decrypting_file"),
type: cons.status.IN_FLIGHT, type: cons.status.IN_FLIGHT,
}); });
} }
@ -41,7 +46,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: true, isOngoing: true,
msg: 'Decrypting messages...', msg: t("Molecules.ImportE2ERoomKeys.decrypting_messages"),
type: cons.status.IN_FLIGHT, type: cons.status.IN_FLIGHT,
}); });
} }
@ -49,7 +54,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: false, isOngoing: false,
msg: 'Successfully imported all keys.', msg: t("Molecules.ImportE2ERoomKeys.import_success"),
type: cons.status.SUCCESS, type: cons.status.SUCCESS,
}); });
inputRef.current.value = null; inputRef.current.value = null;
@ -59,7 +64,7 @@ function ImportE2ERoomKeys() {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
isOngoing: false, isOngoing: false,
msg: e.friendlyText || 'Failed to decrypt keys. Please try again.', msg: e.friendlyText || t("Molecules.ImportE2ERoomKeys.import_failed"),
type: cons.status.ERROR, type: cons.status.ERROR,
}); });
} }
@ -114,9 +119,9 @@ function ImportE2ERoomKeys() {
<Text>{keyFile.name}</Text> <Text>{keyFile.name}</Text>
</div> </div>
)} )}
{keyFile === null && <Button onClick={() => inputRef.current.click()}>Import keys</Button>} {keyFile === null && <Button onClick={() => inputRef.current.click()}>{t("Molecules.ImportE2ERoomKeys.import_keys_button")}</Button>}
<Input forwardRef={passwordRef} type="password" placeholder="Password" required /> <Input forwardRef={passwordRef} type="password" placeholder="Password" required />
<Button disabled={status.isOngoing} variant="primary" type="submit">Decrypt</Button> <Button disabled={status.isOngoing} variant="primary" type="submit">{t("Molecules.ImportE2ERoomKeys.decrypt_button")}</Button>
</form> </form>
{ status.type === cons.status.IN_FLIGHT && ( { status.type === cons.status.IN_FLIGHT && (
<div className="import-e2e-room-keys__process"> <div className="import-e2e-room-keys__process">

View file

@ -12,6 +12,9 @@ import DownloadSVG from '../../../../public/res/ic/outlined/download.svg';
import ExternalSVG from '../../../../public/res/ic/outlined/external.svg'; import ExternalSVG from '../../../../public/res/ic/outlined/external.svg';
import PlaySVG from '../../../../public/res/ic/outlined/play.svg'; import PlaySVG from '../../../../public/res/ic/outlined/play.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
// https://github.com/matrix-org/matrix-react-sdk/blob/a9e28db33058d1893d964ec96cd247ecc3d92fc3/src/utils/blobs.ts#L73 // https://github.com/matrix-org/matrix-react-sdk/blob/a9e28db33058d1893d964ec96cd247ecc3d92fc3/src/utils/blobs.ts#L73
const ALLOWED_BLOB_MIMETYPES = [ const ALLOWED_BLOB_MIMETYPES = [
'image/jpeg', 'image/jpeg',
@ -73,6 +76,8 @@ function FileHeader({
}) { }) {
const [url, setUrl] = useState(null); const [url, setUrl] = useState(null);
const { t } = useTranslation();
async function getFile() { async function getFile() {
const myUrl = await getUrl(link, type, file); const myUrl = await getUrl(link, type, file);
setUrl(myUrl); setUrl(myUrl);
@ -94,7 +99,7 @@ function FileHeader({
external && ( external && (
<IconButton <IconButton
size="extra-small" size="extra-small"
tooltip="Open in new tab" tooltip={t("Molecules.Media.open_new_tab")}
src={ExternalSVG} src={ExternalSVG}
onClick={() => window.open(url || link)} onClick={() => window.open(url || link)}
/> />
@ -103,7 +108,7 @@ function FileHeader({
<a href={url || link} download={name} target="_blank" rel="noreferrer"> <a href={url || link} download={name} target="_blank" rel="noreferrer">
<IconButton <IconButton
size="extra-small" size="extra-small"
tooltip="Download" tooltip= {t("Molecules.Media.download")}
src={DownloadSVG} src={DownloadSVG}
onClick={handleDownload} onClick={handleDownload}
/> />
@ -151,6 +156,7 @@ function Image({
}) { }) {
const [url, setUrl] = useState(null); const [url, setUrl] = useState(null);
useEffect(() => { useEffect(() => {
let unmounted = false; let unmounted = false;
async function fetchUrl() { async function fetchUrl() {
@ -204,12 +210,14 @@ function Audio({
loadAudio(); loadAudio();
} }
const { t } = useTranslation();
return ( return (
<div className="file-container"> <div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external /> <FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
<div className="audio-container"> <div className="audio-container">
{ url === null && isLoading && <Spinner size="small" /> } { url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip="Play audio" src={PlaySVG} />} { url === null && !isLoading && <IconButton onClick={handlePlayAudio} tooltip={t("Molecules.Media.play_audio")} src={PlaySVG} />}
{ url !== null && ( { url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */ /* eslint-disable-next-line jsx-a11y/media-has-caption */
<audio autoPlay controls> <audio autoPlay controls>
@ -263,6 +271,8 @@ function Video({
loadVideo(); loadVideo();
} }
const { t } = useTranslation();
return ( return (
<div className="file-container"> <div className="file-container">
<FileHeader name={name} link={file !== null ? url : url || link} type={type} external /> <FileHeader name={name} link={file !== null ? url : url || link} type={type} external />
@ -274,7 +284,7 @@ function Video({
className="video-container" className="video-container"
> >
{ url === null && isLoading && <Spinner size="small" /> } { url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />} { url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip={t("Molecules.Media.play_video")} src={PlaySVG} />}
{ url !== null && ( { url !== null && (
/* eslint-disable-next-line jsx-a11y/media-has-caption */ /* eslint-disable-next-line jsx-a11y/media-has-caption */
<video autoPlay controls poster={thumbUrl}> <video autoPlay controls poster={thumbUrl}>

View file

@ -38,6 +38,11 @@ import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next';
import { t } from 'i18next';
function PlaceholderMessage() { function PlaceholderMessage() {
return ( return (
<div className="ph-msg"> <div className="ph-msg">
@ -118,6 +123,7 @@ const MessageReplyWrapper = React.memo(({ roomTimeline, eventId }) => {
useEffect(() => { useEffect(() => {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const timelineSet = roomTimeline.getUnfilteredTimelineSet(); const timelineSet = roomTimeline.getUnfilteredTimelineSet();
const loadReply = async () => { const loadReply = async () => {
try { try {
const eTimeline = await mx.getEventTimeline(timelineSet, eventId); const eTimeline = await mx.getEventTimeline(timelineSet, eventId);
@ -250,7 +256,7 @@ const MessageBody = React.memo(({
)} )}
{ content } { content }
</div> </div>
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>} { isEdited && <Text className="message__body-edited" variant="b3"><Trans i18nKey={"Molecules.Message.edited"}/></Text>}
</div> </div>
); );
}); });
@ -270,6 +276,8 @@ MessageBody.propTypes = {
function MessageEdit({ body, onSave, onCancel }) { function MessageEdit({ body, onSave, onCancel }) {
const editInputRef = useRef(null); const editInputRef = useRef(null);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
// makes the cursor end up at the end of the line instead of the beginning // makes the cursor end up at the end of the line instead of the beginning
editInputRef.current.value = ''; editInputRef.current.value = '';
@ -289,14 +297,14 @@ function MessageEdit({ body, onSave, onCancel }) {
forwardRef={editInputRef} forwardRef={editInputRef}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
value={body} value={body}
placeholder="Edit message" placeholder={t("Molecules.Message.edit_placeholder")}
required required
resizable resizable
autoFocus autoFocus
/> />
<div className="message__edit-btns"> <div className="message__edit-btns">
<Button type="submit" variant="primary">Save</Button> <Button type="submit" variant="primary">{t("common.save")}</Button>
<Button onClick={onCancel}>Cancel</Button> <Button onClick={onCancel}>{t("common.cancel")}</Button>
</div> </div>
</form> </form>
); );
@ -341,20 +349,21 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
} }
function genReactionMsg(userIds, reaction) { function genReactionMsg(userIds, reaction) {
console.log(reaction);
return ( return (
<> <>
{userIds.map((userId, index) => ( <Trans
<React.Fragment key={userId}> i18nKey="Molecules.Message.user_reacted"
{twemojify(getUsername(userId))} values={{
{index < userIds.length - 1 && ( count: userIds.length,
<span style={{ opacity: '.6' }}> user_one: getUsername(userIds?.[0]),
{index === userIds.length - 2 ? ' and ' : ', '} user_two: getUsername(userIds?.[1]),
</span> user_three: getUsername(userIds?.[2]),
)} other_count: userIds.length - 3,
</React.Fragment> }}
))} components={{bold: <b/>, emoji: reaction}}
<span style={{ opacity: '.6' }}>{' reacted with '}</span>
{twemojify(reaction, { className: 'react-emoji' })} />
</> </>
); );
} }
@ -518,38 +527,38 @@ const MessageOptions = React.memo(({
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)} onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
src={EmojiAddIC} src={EmojiAddIC}
size="extra-small" size="extra-small"
tooltip="Add reaction" tooltip={t("Molecules.Message.add_reaction_tooltip")}
/> />
)} )}
<IconButton <IconButton
onClick={() => reply()} onClick={() => reply()}
src={ReplyArrowIC} src={ReplyArrowIC}
size="extra-small" size="extra-small"
tooltip="Reply" tooltip={t("Molecules.Message.reply_tooltip")}
/> />
{(senderId === mx.getUserId() && !isMedia(mEvent)) && ( {(senderId === mx.getUserId() && !isMedia(mEvent)) && (
<IconButton <IconButton
onClick={() => edit(true)} onClick={() => edit(true)}
src={PencilIC} src={PencilIC}
size="extra-small" size="extra-small"
tooltip="Edit" tooltip={t("Molecules.Message.edit_tooltip")}
/> />
)} )}
<ContextMenu <ContextMenu
content={() => ( content={() => (
<> <>
<MenuHeader>Options</MenuHeader> <MenuHeader>{t("Molecules.Message.options_header")}</MenuHeader>
<MenuItem <MenuItem
iconSrc={TickMarkIC} iconSrc={TickMarkIC}
onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))} onClick={() => openReadReceipts(roomId, roomTimeline.getEventReaders(mEvent))}
> >
Read receipts {t("Molecules.Message.read_receipts")}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
iconSrc={CmdIC} iconSrc={CmdIC}
onClick={() => handleOpenViewSource(mEvent, roomTimeline)} onClick={() => handleOpenViewSource(mEvent, roomTimeline)}
> >
View source {t("Molecules.Message.view_source")}
</MenuItem> </MenuItem>
{(canIRedact || senderId === mx.getUserId()) && ( {(canIRedact || senderId === mx.getUserId()) && (
<> <>
@ -559,9 +568,9 @@ const MessageOptions = React.memo(({
iconSrc={BinIC} iconSrc={BinIC}
onClick={async () => { onClick={async () => {
const isConfirmed = await confirmDialog( const isConfirmed = await confirmDialog(
'Delete message', t("Molecules.Message.delete_message_prompt"),
'Are you sure that you want to delete this message?', t("Molecules.Message.delete_message_confirmation"),
'Delete', t("Molecules.Message.delete_message_button"),
'danger', 'danger',
); );
if (!isConfirmed) return; if (!isConfirmed) return;
@ -579,7 +588,7 @@ const MessageOptions = React.memo(({
onClick={toggleMenu} onClick={toggleMenu}
src={VerticalMenuIC} src={VerticalMenuIC}
size="extra-small" size="extra-small"
tooltip="Options" tooltip={t("Molecules.Message.options_tooltip")}
/> />
)} )}
/> />

View file

@ -13,6 +13,9 @@ import RawModal from '../../atoms/modal/RawModal';
import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg'; import ChevronLeftIC from '../../../../public/res/ic/outlined/chevron-left.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function PWContentSelector({ function PWContentSelector({
selected, variant, iconSrc, selected, variant, iconSrc,
type, onClick, children, type, onClick, children,
@ -56,6 +59,8 @@ function PopupWindow({
const haveDrawer = drawer !== null; const haveDrawer = drawer !== null;
const cTitle = contentTitle !== null ? contentTitle : title; const cTitle = contentTitle !== null ? contentTitle : title;
const { t } = useTranslation();
return ( return (
<RawModal <RawModal
className={`${className === null ? '' : `${className} `}pw-modal`} className={`${className === null ? '' : `${className} `}pw-modal`}
@ -69,7 +74,7 @@ function PopupWindow({
{haveDrawer && ( {haveDrawer && (
<div className="pw__drawer"> <div className="pw__drawer">
<Header> <Header>
<IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip="Back" /> <IconButton size="small" src={ChevronLeftIC} onClick={onRequestClose} tooltip={t("Molecules.PopupWindow.close_tooltip")}/>
<TitleWrapper> <TitleWrapper>
{ {
typeof title === 'string' typeof title === 'string'

View file

@ -7,6 +7,9 @@ import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import CheckIC from '../../../../public/res/ic/outlined/check.svg'; import CheckIC from '../../../../public/res/ic/outlined/check.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function PowerLevelSelector({ function PowerLevelSelector({
value, max, onSelect, value, max, onSelect,
}) { }) {
@ -16,6 +19,8 @@ function PowerLevelSelector({
onSelect(Number(powerLevel)); onSelect(Number(powerLevel));
}; };
const { t } = useTranslation();
return ( return (
<div className="power-level-selector"> <div className="power-level-selector">
<MenuHeader>Power level selector</MenuHeader> <MenuHeader>Power level selector</MenuHeader>
@ -25,7 +30,7 @@ function PowerLevelSelector({
defaultValue={value} defaultValue={value}
type="number" type="number"
name="power-level" name="power-level"
placeholder="Power level" placeholder={t("Molecules.PowerLevelIndicator.placeholder")}
max={max} max={max}
autoComplete="off" autoComplete="off"
required required

View file

@ -17,10 +17,16 @@ import SettingTile from '../setting-tile/SettingTile';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function useValidate(hsString) { function useValidate(hsString) {
const [debounce] = useState(new Debounce()); const [debounce] = useState(new Debounce());
const [validate, setValidate] = useState({ alias: null, status: cons.status.PRE_FLIGHT }); const [validate, setValidate] = useState({ alias: null, status: cons.status.PRE_FLIGHT });
const { t } = useTranslation();
const setValidateToDefault = () => { const setValidateToDefault = () => {
setValidate({ setValidate({
alias: null, alias: null,
@ -37,7 +43,7 @@ function useValidate(hsString) {
setValidate({ setValidate({
alias: null, alias: null,
status: cons.status.ERROR, status: cons.status.ERROR,
msg: 'Invalid character: only letter, numbers and _- are allowed.', msg: t("Molecules.RoomAliases.invalid_characters")
}); });
return false; return false;
} }
@ -59,7 +65,7 @@ function useValidate(hsString) {
setValidate({ setValidate({
alias, alias,
status: cons.status.IN_FLIGHT, status: cons.status.IN_FLIGHT,
msg: `validating ${alias}...`, msg: t("Molecules.RoomAliases.validating_alias", {alias: alias}),
}); });
const isValid = await isRoomAliasAvailable(alias); const isValid = await isRoomAliasAvailable(alias);
@ -70,7 +76,7 @@ function useValidate(hsString) {
return { return {
alias, alias,
status: isValid ? cons.status.SUCCESS : cons.status.ERROR, status: isValid ? cons.status.SUCCESS : cons.status.ERROR,
msg: isValid ? `${alias} is available.` : `${alias} is already in use.`, msg: t( isValid ? "Molecules.RoomAliases.alias_available": "Molecules.RoomAliases.alias_unavailable", {alias: alias}),
}; };
}); });
}, 600)(); }, 600)();
@ -110,6 +116,8 @@ function RoomAliases({ roomId }) {
const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId); const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
const { t } = useTranslation();
useEffect(() => isMountedStore.setItem(true), []); useEffect(() => isMountedStore.setItem(true), []);
useEffect(() => { useEffect(() => {
@ -225,7 +233,7 @@ function RoomAliases({ roomId }) {
const handleDeleteAlias = async (alias) => { const handleDeleteAlias = async (alias) => {
try { try {
setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: 'deleting...' }); setDeleteAlias({ alias, status: cons.status.IN_FLIGHT, msg: t("Molecules.RoomAliases.deleting_alias")});
await mx.deleteAlias(alias); await mx.deleteAlias(alias);
let { main, published, local } = aliases; let { main, published, local } = aliases;
if (published.includes(alias)) { if (published.includes(alias)) {
@ -259,10 +267,10 @@ function RoomAliases({ roomId }) {
return ( return (
<div className="room-aliases__item-btns"> <div className="room-aliases__item-btns">
{canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">Set as Main</Button>} {canPublishAlias && !isMain && <Button onClick={() => handleSetMainAlias(alias)} variant="primary">{t("Molecules.RoomAliases.set_main_alias")}</Button>}
{!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">Publish</Button>} {!isPublished && canPublishAlias && <Button onClick={() => handlePublishAlias(alias)} variant="positive">{t("Molecules.RoomAliases.publish_alias")}</Button>}
{isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">Un-Publish</Button>} {isPublished && canPublishAlias && <Button onClick={() => handleUnPublishAlias(alias)} variant="caution">{t("Molecules.RoomAliases.unpublish_alias")}</Button>}
<Button onClick={() => handleDeleteAlias(alias)} variant="danger">Delete</Button> <Button onClick={() => handleDeleteAlias(alias)} variant="danger">{t("Molecules.RoomAliases.delete_alias")}</Button>
</div> </div>
); );
}; };
@ -278,7 +286,7 @@ function RoomAliases({ roomId }) {
<Checkbox variant="positive" disabled={disabled} isActive={isActive} onToggle={() => handleAliasSelect(alias)} /> <Checkbox variant="positive" disabled={disabled} isActive={isActive} onToggle={() => handleAliasSelect(alias)} />
<Text> <Text>
{alias} {alias}
{isMain && <span>Main</span>} {isMain && <span>{t("Molecules.RoomAliases.main_alias")}</span>}
</Text> </Text>
</div> </div>
{isActive && renderAliasBtns(alias)} {isActive && renderAliasBtns(alias)}
@ -292,8 +300,8 @@ function RoomAliases({ roomId }) {
return ( return (
<div className="room-aliases"> <div className="room-aliases">
<SettingTile <SettingTile
title="Publish to room directory" title={t("Molecules.RoomAliases.publish_to_room_directory.title")}
content={<Text variant="b3">{`Publish this ${room.isSpaceRoom() ? 'space' : 'room'} to the ${hsString}'s public room directory?`}</Text>} content={<Text variant="b3">{t(room.isSpaceRoom() ? "Molecules.RoomAliases.publish_to_room_directory.publish_space_message" : "Molecules.RoomAliases.publish_to_room_directory.publish_room_message", {homeserver: hsString})}</Text>}
options={( options={(
<Toggle <Toggle
isActive={isPublic} isActive={isPublic}
@ -304,35 +312,35 @@ function RoomAliases({ roomId }) {
/> />
<div className="room-aliases__content"> <div className="room-aliases__content">
<MenuHeader>Published addresses</MenuHeader> <MenuHeader>{t("Molecules.RoomAliases.published_addresses.title")}</MenuHeader>
{(aliases.published.length === 0) && <Text className="room-aliases__message">No published addresses</Text>} {(aliases.published.length === 0) && <Text className="room-aliases__message">{t("Molecules.RoomAliases.published_addresses.none")}</Text>}
{(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">No Main address (select one from below)</Text>} {(aliases.published.length > 0 && !aliases.main) && <Text className="room-aliases__message">{t("Molecules.RoomAliases.published_addresses.no_main_address")}</Text>}
{aliases.published.map(renderAlias)} {aliases.published.map(renderAlias)}
<Text className="room-aliases__message" variant="b3"> <Text className="room-aliases__message" variant="b3">
{`Published addresses can be used by anyone on any server to join your ${room.isSpaceRoom() ? 'space' : 'room'}. To publish an address, it needs to be set as a local address first.`} {t(room.isSpaceRoom() ? "Molecules.RoomAliases.published_addresses.message_space" : "Molecules.RoomAliases.published_addresses.message_room")}
</Text> </Text>
</div> </div>
{ isLocalVisible && ( { isLocalVisible && (
<div className="room-aliases__content"> <div className="room-aliases__content">
<MenuHeader>Local addresses</MenuHeader> <MenuHeader>{t("Molecules.RoomAliases.local_addresses.title")}</MenuHeader>
{(aliases.local.length === 0) && <Text className="room-aliases__message">No local addresses</Text>} {(aliases.local.length === 0) && <Text className="room-aliases__message">{t("Molecules.RoomAliases.local_addresses.none")}</Text>}
{aliases.local.map(renderAlias)} {aliases.local.map(renderAlias)}
<Text className="room-aliases__message" variant="b3"> <Text className="room-aliases__message" variant="b3">
{`Set local addresses for this ${room.isSpaceRoom() ? 'space' : 'room'} so users can find this ${room.isSpaceRoom() ? 'space' : 'room'} through your homeserver.`} {t(room.isSpaceRoom() ? "Molecules.RoomAliases.local_addresses.message_space" : "Molecules.RoomAliases.local_addresses.message_room" )}
</Text> </Text>
<Text className="room-aliases__form-label" variant="b2">Add local address</Text> <Text className="room-aliases__form-label" variant="b2">{t("Molecules.RoomAliases.local_addresses.add")}</Text>
<form className="room-aliases__form" onSubmit={handleAliasSubmit}> <form className="room-aliases__form" onSubmit={handleAliasSubmit}>
<div className="room-aliases__input-wrapper"> <div className="room-aliases__input-wrapper">
<Input <Input
name="alias-input" name="alias-input"
state={inputState} state={inputState}
onChange={handleAliasChange} onChange={handleAliasChange}
placeholder={`my_${room.isSpaceRoom() ? 'space' : 'room'}_address`} placeholder={t(room.isSpaceRoom() ? "Molecules.RoomAliases.local_addresses.placeholder_space" : "Molecules.RoomAliases.local_addresses.placeholder_room")}
required required
/> />
</div> </div>
<Button variant="primary" type="submit">Add</Button> <Button variant="primary" type="submit">{t("Molecules.RoomAliases.local_addresses.add_button")}</Button>
</form> </form>
<div className="room-aliases__input-status"> <div className="room-aliases__input-status">
{validate.status === cons.status.SUCCESS && <Text className="room-aliases__valid" variant="b2">{validate.msg}</Text>} {validate.status === cons.status.SUCCESS && <Text className="room-aliases__valid" variant="b2">{validate.msg}</Text>}
@ -342,7 +350,7 @@ function RoomAliases({ roomId }) {
)} )}
<div className="room-aliases__content"> <div className="room-aliases__content">
<Button onClick={() => setIsLocalVisible(!isLocalVisible)}> <Button onClick={() => setIsLocalVisible(!isLocalVisible)}>
{`${isLocalVisible ? 'Hide' : 'Add / View'} local address`} {t(isLocalVisible ? "Molecules.RoomAliases.local_addresses.hide" : "Molecules.RoomAliases.local_addresses.view" )}
</Button> </Button>
</div> </div>
</div> </div>

View file

@ -10,6 +10,9 @@ import SettingTile from '../setting-tile/SettingTile';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomEncryption({ roomId }) { function RoomEncryption({ roomId }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@ -17,16 +20,18 @@ function RoomEncryption({ roomId }) {
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0); const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId()); const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
const { t } = useTranslation();
const handleEncryptionEnable = async () => { const handleEncryptionEnable = async () => {
const joinRule = room.getJoinRule(); const joinRule = room.getJoinRule();
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.'; const confirmMsg1 = t("Molecules.RoomEncryption.encryption_public_room_message");
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly'; const confirmMsg2 = t("Molecules.RoomEncryption.encryption_message");
const isConfirmed1 = (joinRule === 'public') const isConfirmed1 = (joinRule === 'public')
? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution') ? await confirmDialog(t("Molecules.RoomEncryption.enable_encryption_prompt"), confirmMsg1, t("Molecules.RoomEncryption.continue_button"), 'caution')
: true; : true;
if (!isConfirmed1) return; if (!isConfirmed1) return;
if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) { if (await confirmDialog(t("Molecules.RoomEncryption.enable_encryption_prompt"), confirmMsg2, t("Molecules.RoomEncryption.enable_encryption_button"), 'caution')) {
setIsEncrypted(true); setIsEncrypted(true);
mx.sendStateEvent(roomId, 'm.room.encryption', { mx.sendStateEvent(roomId, 'm.room.encryption', {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: 'm.megolm.v1.aes-sha2',
@ -37,9 +42,9 @@ function RoomEncryption({ roomId }) {
return ( return (
<div className="room-encryption"> <div className="room-encryption">
<SettingTile <SettingTile
title="Enable room encryption" title={t("Molecules.RoomEncryption.enable_room_encryption")}
content={( content={(
<Text variant="b3">Once enabled, encryption cannot be disabled.</Text> <Text variant="b3">{t("Molecules.RoomEncryption.encryption_cannot_be_disabled")}</Text>
)} )}
options={( options={(
<Toggle <Toggle

View file

@ -8,6 +8,9 @@ import Text from '../../atoms/text/Text';
import RadioButton from '../../atoms/button/RadioButton'; import RadioButton from '../../atoms/button/RadioButton';
import { MenuItem } from '../../atoms/context-menu/ContextMenu'; import { MenuItem } from '../../atoms/context-menu/ContextMenu';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const visibility = { const visibility = {
WORLD_READABLE: 'world_readable', WORLD_READABLE: 'world_readable',
SHARED: 'shared', SHARED: 'shared',
@ -17,19 +20,19 @@ const visibility = {
const items = [{ const items = [{
iconSrc: null, iconSrc: null,
text: 'Anyone (including guests)', text: 'Molecules.RoomHistoryVisibility.world_readable',
type: visibility.WORLD_READABLE, type: visibility.WORLD_READABLE,
}, { }, {
iconSrc: null, iconSrc: null,
text: 'Members (all messages)', text: 'Molecules.RoomHistoryVisibility.shared',
type: visibility.SHARED, type: visibility.SHARED,
}, { }, {
iconSrc: null, iconSrc: null,
text: 'Members (messages after invite)', text: 'Molecules.RoomHistoryVisibility.invited',
type: visibility.INVITED, type: visibility.INVITED,
}, { }, {
iconSrc: null, iconSrc: null,
text: 'Members (messages after join)', text: 'Molecules.RoomHistoryVisibility.joined',
type: visibility.JOINED, type: visibility.JOINED,
}]; }];
@ -69,6 +72,8 @@ function RoomHistoryVisibility({ roomId }) {
const canChange = currentState.maySendStateEvent('m.room.history_visibility', userId); const canChange = currentState.maySendStateEvent('m.room.history_visibility', userId);
const { t } = useTranslation();
return ( return (
<div className="room-history-visibility"> <div className="room-history-visibility">
{ {
@ -81,13 +86,13 @@ function RoomHistoryVisibility({ roomId }) {
disabled={(!canChange)} disabled={(!canChange)}
> >
<Text varient="b1"> <Text varient="b1">
<span>{item.text}</span> <span>{t(item.text)}</span>
<RadioButton isActive={activeType === item.type} /> <RadioButton isActive={activeType === item.type} />
</Text> </Text>
</MenuItem> </MenuItem>
)) ))
} }
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text> <Text variant="b3">{t("Molecules.RoomHistoryVisibility.changes_only_affect_future")}</Text>
</div> </div>
); );
} }

View file

@ -18,6 +18,9 @@ import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls'; import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
import PeopleSelector from '../people-selector/PeopleSelector'; import PeopleSelector from '../people-selector/PeopleSelector';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const PER_PAGE_MEMBER = 50; const PER_PAGE_MEMBER = 50;
function normalizeMembers(members) { function normalizeMembers(members) {
@ -113,6 +116,8 @@ function RoomMembers({ roomId }) {
const [members] = useMemberOfMembership(roomId, membership); const [members] = useMemberOfMembership(roomId, membership);
const [searchMembers, handleSearch] = useSearchMembers(members); const [searchMembers, handleSearch] = useSearchMembers(members);
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
setItemCount(PER_PAGE_MEMBER); setItemCount(PER_PAGE_MEMBER);
}, [searchMembers]); }, [searchMembers]);
@ -124,14 +129,14 @@ function RoomMembers({ roomId }) {
const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount); const mList = searchMembers ? searchMembers.data : members.slice(0, itemCount);
return ( return (
<div className="room-members"> <div className="room-members">
<MenuHeader>Search member</MenuHeader> <MenuHeader>{t("Molecules.RoomMembers.search_title")}</MenuHeader>
<Input <Input
onChange={handleSearch} onChange={handleSearch}
placeholder="Search for name" placeholder={t("Molecules.RoomMembers.search_placeholder")}
autoFocus autoFocus
/> />
<div className="room-members__header"> <div className="room-members__header">
<MenuHeader>{`${searchMembers ? `Found — ${mList.length}` : members.length} members`}</MenuHeader> <MenuHeader>{t("Molecules.RoomMembers.found_members", {count: mList.length})}</MenuHeader>
<SegmentedControls <SegmentedControls
selected={ selected={
(() => { (() => {
@ -139,7 +144,7 @@ function RoomMembers({ roomId }) {
return getSegmentIndex[membership]; return getSegmentIndex[membership];
})() })()
} }
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]} segments={[{ text: t("Molecules.RoomMembers.joined") }, { text: t("Molecules.RoomMembers.invited") }, { text: t("Molecules.RoomMembers.banned") }]}
onSelect={(index) => { onSelect={(index) => {
const memberships = ['join', 'invite', 'ban']; const memberships = ['join', 'invite', 'ban'];
setMembership(memberships[index]); setMembership(memberships[index]);
@ -162,7 +167,7 @@ function RoomMembers({ roomId }) {
&& ( && (
<div className="room-members__status"> <div className="room-members__status">
<Text variant="b2"> <Text variant="b2">
{searchMembers ? `No results found for "${searchMembers.term}"` : 'No members to display'} {searchMembers ? t("Molecules.RoomMembers.invited", {term: searchMembers.term}) : t("Molecules.RoomMembers.no_members")}
</Text> </Text>
</div> </div>
) )
@ -171,7 +176,7 @@ function RoomMembers({ roomId }) {
mList.length !== 0 mList.length !== 0
&& members.length > itemCount && members.length > itemCount
&& searchMembers === null && searchMembers === null
&& <Button onClick={loadMorePeople}>View more</Button> && <Button onClick={loadMorePeople}>{t("Molecules.RoomMembers.view_more")}</Button>
} }
</div> </div>
</div> </div>

View file

@ -14,21 +14,24 @@ import BellRingIC from '../../../../public/res/ic/outlined/bell-ring.svg';
import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg'; import BellPingIC from '../../../../public/res/ic/outlined/bell-ping.svg';
import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg'; import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const items = [{ const items = [{
iconSrc: BellIC, iconSrc: BellIC,
text: 'Global', text: 'Molecules.RoomNotification.default',
type: cons.notifs.DEFAULT, type: cons.notifs.DEFAULT,
}, { }, {
iconSrc: BellRingIC, iconSrc: BellRingIC,
text: 'All messages', text: 'Molecules.RoomNotification.all_messages',
type: cons.notifs.ALL_MESSAGES, type: cons.notifs.ALL_MESSAGES,
}, { }, {
iconSrc: BellPingIC, iconSrc: BellPingIC,
text: 'Mentions & Keywords', text: 'Molecules.RoomNotification.mentions_and_keywords',
type: cons.notifs.MENTIONS_AND_KEYWORDS, type: cons.notifs.MENTIONS_AND_KEYWORDS,
}, { }, {
iconSrc: BellOffIC, iconSrc: BellOffIC,
text: 'Mute', text: 'Molecules.RoomNotification.mute',
type: cons.notifs.MUTE, type: cons.notifs.MUTE,
}]; }];
@ -114,6 +117,8 @@ function useNotifications(roomId) {
} }
function RoomNotification({ roomId }) { function RoomNotification({ roomId }) {
const { t } = useTranslation();
const [activeType, setNotification] = useNotifications(roomId); const [activeType, setNotification] = useNotifications(roomId);
return ( return (
@ -127,7 +132,7 @@ function RoomNotification({ roomId }) {
onClick={() => setNotification(item)} onClick={() => setNotification(item)}
> >
<Text varient="b1"> <Text varient="b1">
<span>{item.text}</span> <span>{t(item.text)}</span>
<RadioButton isActive={activeType === item.type} /> <RadioButton isActive={activeType === item.type} />
</Text> </Text>
</MenuItem> </MenuItem>

View file

@ -17,11 +17,16 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomOptions({ roomId, afterOptionSelect }) { function RoomOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
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();
const handleMarkAsRead = () => { const handleMarkAsRead = () => {
markAsRead(roomId); markAsRead(roomId);
afterOptionSelect(); afterOptionSelect();
@ -34,9 +39,9 @@ function RoomOptions({ roomId, afterOptionSelect }) {
const handleLeaveClick = async () => { const handleLeaveClick = async () => {
afterOptionSelect(); afterOptionSelect();
const isConfirmed = await confirmDialog( const isConfirmed = await confirmDialog(
'Leave room', t("Molecules.RoomOptions.leave.title"),
`Are you sure that you want to leave "${room.name}" room?`, t("Molecules.RoomOptions.leave.subtitle", {room_name: room.name}),
'Leave', t("Molecules.RoomOptions.leave.button_text"),
'danger', 'danger',
); );
if (!isConfirmed) return; if (!isConfirmed) return;
@ -45,17 +50,17 @@ function RoomOptions({ roomId, afterOptionSelect }) {
return ( return (
<div style={{ maxWidth: '256px' }}> <div style={{ maxWidth: '256px' }}>
<MenuHeader>{twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)}</MenuHeader> <MenuHeader>{twemojify(t("Molecules.RoomOptions.title", {room_name: initMatrix.matrixClient.getRoom(roomId)?.name}))}</MenuHeader>
<MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>Mark as read</MenuItem> <MenuItem iconSrc={TickMarkIC} onClick={handleMarkAsRead}>{t("Molecules.RoomOptions.mark_as_read")}</MenuItem>
<MenuItem <MenuItem
iconSrc={AddUserIC} iconSrc={AddUserIC}
onClick={handleInviteClick} onClick={handleInviteClick}
disabled={!canInvite} disabled={!canInvite}
> >
Invite {t("Molecules.RoomOptions.invite")}
</MenuItem> </MenuItem>
<MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>Leave</MenuItem> <MenuItem iconSrc={LeaveArrowIC} variant="danger" onClick={handleLeaveClick}>{t("Molecules.RoomOptions.leave.button_text")}</MenuItem>
<MenuHeader>Notification</MenuHeader> <MenuHeader>{t("Molecules.RoomOptions.notifications_heading")}</MenuHeader>
<RoomNotification roomId={roomId} /> <RoomNotification roomId={roomId} />
</div> </div>
); );

View file

@ -17,142 +17,145 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const permissionsInfo = { const permissionsInfo = {
users_default: { users_default: {
name: 'Default role', name: 'Molecules.RoomPermissions.default_role.name',
description: 'Set default role for all members.', description: 'Molecules.RoomPermissions.default_role.description',
default: 0, default: 0,
}, },
events_default: { events_default: {
name: 'Send messages', name: 'Molecules.RoomPermissions.send_messages.name',
description: 'Set minimum power level to send messages in room.', description: 'Molecules.RoomPermissions.send_messages.description',
default: 0, default: 0,
}, },
'm.reaction': { 'm.reaction': {
parent: 'events', parent: 'events',
name: 'Send reactions', name: 'Molecules.RoomPermissions.reactions.name',
description: 'Set minimum power level to send reactions in room.', description: 'Molecules.RoomPermissions.reactions.description',
default: 0, default: 0,
}, },
redact: { redact: {
name: 'Delete messages sent by others', name: 'Molecules.RoomPermissions.delete.name',
description: 'Set minimum power level to delete messages in room.', description: 'Molecules.RoomPermissions.delete.description',
default: 50, default: 50,
}, },
notifications: { notifications: {
name: 'Ping room', name: 'Molecules.RoomPermissions.notifications.name',
description: 'Set minimum power level to ping room.', description: 'Molecules.RoomPermissions.notifications.description',
default: { default: {
room: 50, room: 50,
}, },
}, },
'm.space.child': { 'm.space.child': {
parent: 'events', parent: 'events',
name: 'Manage rooms in space', name: 'Molecules.RoomPermissions.manage_rooms.name',
description: 'Set minimum power level to manage rooms in space.', description: 'Molecules.RoomPermissions.manage_rooms.description',
default: 50, default: 50,
}, },
invite: { invite: {
name: 'Invite', name: 'Molecules.RoomPermissions.invite.name',
description: 'Set minimum power level to invite members.', description: 'Molecules.RoomPermissions.invite.description',
default: 50, default: 50,
}, },
kick: { kick: {
name: 'Kick', name: 'Molecules.RoomPermissions.kick.name',
description: 'Set minimum power level to kick members.', description: 'Molecules.RoomPermissions.kick.description',
default: 50, default: 50,
}, },
ban: { ban: {
name: 'Ban', name: 'Molecules.RoomPermissions.ban.name',
description: 'Set minimum power level to ban members.', description: 'Molecules.RoomPermissions.ban.description',
default: 50, default: 50,
}, },
'm.room.avatar': { 'm.room.avatar': {
parent: 'events', parent: 'events',
name: 'Change avatar', name: 'Molecules.RoomPermissions.change_avatar.name',
description: 'Set minimum power level to change room/space avatar.', description: 'Molecules.RoomPermissions.change_avatar.description',
default: 50, default: 50,
}, },
'm.room.name': { 'm.room.name': {
parent: 'events', parent: 'events',
name: 'Change name', name: 'Molecules.RoomPermissions.change_name.name',
description: 'Set minimum power level to change room/space name.', description: 'Molecules.RoomPermissions.change_name.description',
default: 50, default: 50,
}, },
'm.room.topic': { 'm.room.topic': {
parent: 'events', parent: 'events',
name: 'Change topic', name: 'Molecules.RoomPermissions.change_topic.name',
description: 'Set minimum power level to change room/space topic.', description: 'Molecules.RoomPermissions.change_topic.description',
default: 50, default: 50,
}, },
state_default: { state_default: {
name: 'Change settings', name: 'Molecules.RoomPermissions.change_settings.name',
description: 'Set minimum power level to change settings.', description: 'Molecules.RoomPermissions.change_settings.description',
default: 50, default: 50,
}, },
'm.room.canonical_alias': { 'm.room.canonical_alias': {
parent: 'events', parent: 'events',
name: 'Change published address', name: 'Molecules.RoomPermissions.change_published_address.name',
description: 'Set minimum power level to publish and set main address.', description: 'Molecules.RoomPermissions.change_published_address.description',
default: 50, default: 50,
}, },
'm.room.power_levels': { 'm.room.power_levels': {
parent: 'events', parent: 'events',
name: 'Change permissions', name: 'Molecules.RoomPermissions.change_permissions.name',
description: 'Set minimum power level to change permissions.', description: 'Molecules.RoomPermissions.change_permissions.description',
default: 50, default: 50,
}, },
'm.room.encryption': { 'm.room.encryption': {
parent: 'events', parent: 'events',
name: 'Enable room encryption', name: 'Molecules.RoomPermissions.enable_room_encryption.name',
description: 'Set minimum power level to enable room encryption.', description: 'Molecules.RoomPermissions.enable_room_encryption.description',
default: 50, default: 50,
}, },
'm.room.history_visibility': { 'm.room.history_visibility': {
parent: 'events', parent: 'events',
name: 'Change history visibility', name: 'Molecules.RoomPermissions.change_history_visibility.name',
description: 'Set minimum power level to change room messages history visibility.', description: 'Molecules.RoomPermissions.change_history_visibility.description',
default: 50, default: 50,
}, },
'm.room.tombstone': { 'm.room.tombstone': {
parent: 'events', parent: 'events',
name: 'Upgrade room', name: 'Molecules.RoomPermissions.upgrade_room.name',
description: 'Set minimum power level to upgrade room.', description: 'Molecules.RoomPermissions.upgrade_room.description',
default: 50, default: 50,
}, },
'm.room.pinned_events': { 'm.room.pinned_events': {
parent: 'events', parent: 'events',
name: 'Pin messages', name: 'Molecules.RoomPermissions.pin_messages.name',
description: 'Set minimum power level to pin messages in room.', description: 'Molecules.RoomPermissions.pin_messages.description',
default: 50, default: 50,
}, },
'm.room.server_acl': { 'm.room.server_acl': {
parent: 'events', parent: 'events',
name: 'Change server ACLs', name: 'Molecules.RoomPermissions.change_acls.name',
description: 'Set minimum power level to change server ACLs.', description: 'Molecules.RoomPermissions.change_acls.description',
default: 50, default: 50,
}, },
'im.vector.modular.widgets': { 'im.vector.modular.widgets': {
parent: 'events', parent: 'events',
name: 'Modify widgets', name: 'Molecules.RoomPermissions.modify_widgets.name',
description: 'Set minimum power level to modify room widgets.', description: 'Molecules.RoomPermissions.modify_widgets.description',
default: 50, default: 50,
}, },
}; };
const roomPermsGroups = { const roomPermsGroups = {
'General Permissions': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'], 'Molecules.RoomPermissions.groups.general': ['users_default', 'events_default', 'm.reaction', 'redact', 'notifications'],
'Manage members permissions': ['invite', 'kick', 'ban'], 'Molecules.RoomPermissions.groups.manage_members': ['invite', 'kick', 'ban'],
'Room profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'], 'Molecules.RoomPermissions.groups.room': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'], 'Molecules.RoomPermissions.groups.settings': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels', 'm.room.encryption', 'm.room.history_visibility'],
'Other permissions': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'], 'Molecules.RoomPermissions.groups.other': ['m.room.tombstone', 'm.room.pinned_events', 'm.room.server_acl', 'im.vector.modular.widgets'],
}; };
const spacePermsGroups = { const spacePermsGroups = {
'General Permissions': ['users_default', 'm.space.child'], 'Molecules.RoomPermissions.groups.general': ['users_default', 'm.space.child'],
'Manage members permissions': ['invite', 'kick', 'ban'], 'Molecules.RoomPermissions.groups.manage_members': ['invite', 'kick', 'ban'],
'Space profile permissions': ['m.room.avatar', 'm.room.name', 'm.room.topic'], 'Molecules.RoomPermissions.groups.space': ['m.room.avatar', 'm.room.name', 'm.room.topic'],
'Settings permissions': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'], 'Molecules.RoomPermissions.groups.settings': ['state_default', 'm.room.canonical_alias', 'm.room.power_levels'],
}; };
function useRoomStateUpdate(roomId) { function useRoomStateUpdate(roomId) {
@ -181,6 +184,8 @@ function RoomPermissions({ roomId }) {
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId()); const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100; const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
const { t } = useTranslation();
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => { const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
const handlePowerLevelChange = (newPowerLevel) => { const handlePowerLevelChange = (newPowerLevel) => {
if (powerLevel === newPowerLevel) return; if (powerLevel === newPowerLevel) return;
@ -227,7 +232,7 @@ function RoomPermissions({ roomId }) {
const groupedPermKeys = permsGroups[groupKey]; const groupedPermKeys = permsGroups[groupKey];
return ( return (
<div className="room-permissions__card" key={groupKey}> <div className="room-permissions__card" key={groupKey}>
<MenuHeader>{groupKey}</MenuHeader> <MenuHeader>{t(groupKey)}</MenuHeader>
{ {
groupedPermKeys.map((permKey) => { groupedPermKeys.map((permKey) => {
const permInfo = permissionsInfo[permKey]; const permInfo = permissionsInfo[permKey];
@ -247,8 +252,8 @@ function RoomPermissions({ roomId }) {
return ( return (
<SettingTile <SettingTile
key={permKey} key={permKey}
title={permInfo.name} title={t(permInfo.name)}
content={<Text variant="b3">{permInfo.description}</Text>} content={<Text variant="b3">{t(permInfo.description)}</Text>}
options={( options={(
<Button <Button
onClick={ onClick={

View file

@ -21,6 +21,9 @@ import { useStore } from '../../hooks/useStore';
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function RoomProfile({ roomId }) { function RoomProfile({ roomId }) {
const isMountStore = useStore(); const isMountStore = useStore();
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
@ -30,6 +33,8 @@ function RoomProfile({ roomId }) {
type: cons.status.PRE_FLIGHT, type: cons.status.PRE_FLIGHT,
}); });
const { t } = useTranslation();
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const isDM = initMatrix.roomList.directs.has(roomId); const isDM = initMatrix.roomList.directs.has(roomId);
let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop'); let avatarSrc = mx.getRoom(roomId).getAvatarUrl(mx.baseUrl, 36, 36, 'crop');
@ -76,7 +81,7 @@ function RoomProfile({ roomId }) {
const newName = roomNameInput.value; const newName = roomNameInput.value;
if (newName !== roomName && roomName.trim() !== '') { if (newName !== roomName && roomName.trim() !== '') {
setStatus({ setStatus({
msg: 'Saving room name...', msg: t("Molecules.RoomProfile.saving_room_name"),
type: cons.status.IN_FLIGHT, type: cons.status.IN_FLIGHT,
}); });
await mx.setRoomName(roomId, newName); await mx.setRoomName(roomId, newName);
@ -87,7 +92,7 @@ function RoomProfile({ roomId }) {
if (newTopic !== roomTopic) { if (newTopic !== roomTopic) {
if (isMountStore.getItem()) { if (isMountStore.getItem()) {
setStatus({ setStatus({
msg: 'Saving room topic...', msg: t("Molecules.RoomProfile.saving_room_topic"),
type: cons.status.IN_FLIGHT, type: cons.status.IN_FLIGHT,
}); });
} }
@ -96,13 +101,13 @@ function RoomProfile({ roomId }) {
} }
if (!isMountStore.getItem()) return; if (!isMountStore.getItem()) return;
setStatus({ setStatus({
msg: 'Saved successfully', msg: t("Molecules.RoomProfile.save_success"),
type: cons.status.SUCCESS, type: cons.status.SUCCESS,
}); });
} catch (err) { } catch (err) {
if (!isMountStore.getItem()) return; if (!isMountStore.getItem()) return;
setStatus({ setStatus({
msg: err.message || 'Unable to save.', msg: err.message || t("Molecules.RoomProfile.save_failed"),
type: cons.status.ERROR, type: cons.status.ERROR,
}); });
} }
@ -119,9 +124,9 @@ function RoomProfile({ roomId }) {
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("Molecules.RoomProfile.remove_avatar_title"),
'Are you sure that you want to remove room avatar?', t("Molecules.RoomProfile.remove_avatar_subtitle"),
'Remove', t("Molecules.RoomProfile.remove_avatar_button"),
'caution', 'caution',
); );
if (isConfirmed) { if (isConfirmed) {
@ -132,16 +137,20 @@ function RoomProfile({ roomId }) {
const renderEditNameAndTopic = () => ( const renderEditNameAndTopic = () => (
<form className="room-profile__edit-form" onSubmit={handleOnSubmit}> <form className="room-profile__edit-form" onSubmit={handleOnSubmit}>
{canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label="Name" required />} {canChangeName && <Input value={roomName} name="room-name" disabled={status.type === cons.status.IN_FLIGHT} label={t("Molecules.RoomProfile.name_label")} required />}
{canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label="Topic" />} {canChangeTopic && <Input value={roomTopic} name="room-topic" disabled={status.type === cons.status.IN_FLIGHT} minHeight={100} resizable label={t("Molecules.RoomProfile.topic_label")} />}
{(!canChangeName || !canChangeTopic) && <Text variant="b3">{`You have permission to change ${room.isSpaceRoom() ? 'space' : 'room'} ${canChangeName ? 'name' : 'topic'} only.`}</Text>} {(!canChangeName || !canChangeTopic) && <Text variant="b3">{
room.isSpaceRoom() ?
canChangeName ? "Molecules.RoomProfile.permission_change_space_name" : "Molecules.RoomProfile.permission_change_space_topic" :
canChangeName ? "Molecules.RoomProfile.permission_change_room_name": "Molecules.RoomProfile.permission_change_room_topic"
}</Text>}
{ status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>} { status.type === cons.status.IN_FLIGHT && <Text variant="b2">{status.msg}</Text>}
{ status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>} { status.type === cons.status.SUCCESS && <Text style={{ color: 'var(--tc-positive-high)' }} variant="b2">{status.msg}</Text>}
{ status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>} { status.type === cons.status.ERROR && <Text style={{ color: 'var(--tc-danger-high)' }} variant="b2">{status.msg}</Text>}
{ status.type !== cons.status.IN_FLIGHT && ( { status.type !== cons.status.IN_FLIGHT && (
<div> <div>
<Button type="submit" variant="primary">Save</Button> <Button type="submit" variant="primary">{t("common.save")}</Button>
<Button onClick={handleCancelEditing}>Cancel</Button> <Button onClick={handleCancelEditing}>{t("common.cancel")}</Button>
</div> </div>
)} )}
</form> </form>
@ -155,7 +164,7 @@ function RoomProfile({ roomId }) {
<IconButton <IconButton
src={PencilIC} src={PencilIC}
size="extra-small" size="extra-small"
tooltip="Edit" tooltip={t("common.edit")}
onClick={() => setIsEditing(true)} onClick={() => setIsEditing(true)}
/> />
)} )}

View file

@ -20,6 +20,9 @@ import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const roomIdToBackup = new Map(); const roomIdToBackup = new Map();
function useRoomSearch(roomId) { function useRoomSearch(roomId) {
@ -117,6 +120,8 @@ function RoomSearch({ roomId }) {
search(term); search(term);
}; };
const { t } = useTranslation();
const renderTimeline = (timeline) => ( const renderTimeline = (timeline) => (
<div className="room-search__result-item" key={timeline[0].getId()}> <div className="room-search__result-item" key={timeline[0].getId()}>
{ timeline.map((mEvent) => { { timeline.map((mEvent) => {
@ -139,37 +144,37 @@ function RoomSearch({ roomId }) {
return ( return (
<div className="room-search"> <div className="room-search">
<form className="room-search__form" onSubmit={handleSearch}> <form className="room-search__form" onSubmit={handleSearch}>
<MenuHeader>Room search</MenuHeader> <MenuHeader>{t("Molecules.RoomSearch.title")}</MenuHeader>
<div> <div>
<Input <Input
placeholder="Search for keywords" placeholder={t("Molecules.RoomSearch.placeholder")}
name="room-search-input" name="room-search-input"
disabled={isRoomEncrypted} disabled={isRoomEncrypted}
autoFocus autoFocus
/> />
<Button iconSrc={SearchIC} variant="primary" type="submit">Search</Button> <Button iconSrc={SearchIC} variant="primary" type="submit">{t("Molecules.RoomSearch.search_button")}</Button>
</div> </div>
{searchData?.results.length > 0 && ( {searchData?.results.length > 0 && (
<Text>{`${searchData.count} results for "${searchTerm}"`}</Text> <Text>{t("Molecules.RoomSearch.results", {count: searchData.count, term: searchTerm})}</Text>
)} )}
{!isRoomEncrypted && searchData === null && ( {!isRoomEncrypted && searchData === null && (
<div className="room-search__help"> <div className="room-search__help">
{status.type === cons.status.IN_FLIGHT && <Spinner />} {status.type === cons.status.IN_FLIGHT && <Spinner />}
{status.type === cons.status.IN_FLIGHT && <Text>Searching room messages...</Text>} {status.type === cons.status.IN_FLIGHT && <Text>{t("Molecules.RoomSearch.searching")}</Text>}
{status.type === cons.status.PRE_FLIGHT && <RawIcon src={SearchIC} size="large" />} {status.type === cons.status.PRE_FLIGHT && <RawIcon src={SearchIC} size="large" />}
{status.type === cons.status.PRE_FLIGHT && <Text>Search room messages</Text>} {status.type === cons.status.PRE_FLIGHT && <Text>{t("Molecules.RoomSearch.subtitle")}</Text>}
{status.type === cons.status.ERROR && <Text>Failed to search messages</Text>} {status.type === cons.status.ERROR && <Text>{t("Molecules.RoomSearch.failed")}</Text>}
</div> </div>
)} )}
{!isRoomEncrypted && searchData?.results.length === 0 && ( {!isRoomEncrypted && searchData?.results.length === 0 && (
<div className="room-search__help"> <div className="room-search__help">
<Text>No results found</Text> <Text>{t("Molecules.RoomSearch.no_results")}</Text>
</div> </div>
)} )}
{isRoomEncrypted && ( {isRoomEncrypted && (
<div className="room-search__help"> <div className="room-search__help">
<Text>Search does not work in encrypted room</Text> <Text>{t("Molecules.RoomSearch.encrypted_room")}</Text>
</div> </div>
)} )}
</form> </form>
@ -184,7 +189,7 @@ function RoomSearch({ roomId }) {
{searchData?.next_batch && ( {searchData?.next_batch && (
<div className="room-search__more"> <div className="room-search__more">
{status.type !== cons.status.IN_FLIGHT && ( {status.type !== cons.status.IN_FLIGHT && (
<Button onClick={paginate}>Load more</Button> <Button onClick={paginate}>{t("Molecules.RoomSearch.load_more")}</Button>
)} )}
{status.type === cons.status.IN_FLIGHT && <Spinner />} {status.type === cons.status.IN_FLIGHT && <Spinner />}
</div> </div>

View file

@ -8,6 +8,7 @@ import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text'; import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar'; import Avatar from '../../atoms/avatar/Avatar';
import { t } from 'i18next';
function RoomTile({ function RoomTile({
avatarSrc, name, id, avatarSrc, name, id,
@ -27,8 +28,8 @@ function RoomTile({
<Text variant="b3"> <Text variant="b3">
{ {
inviterName !== null inviterName !== null
? `Invited by ${inviterName} to ${id}${memberCount === null ? '' : `${memberCount} members`}` ? t("Molecules.RoomTile.invited_by_user", {inviter: inviterName, count: memberCount || 0, id: id})
: id + (memberCount === null ? '' : `${memberCount} members`) : t("Molecules.RoomTile.invited", {count: memberCount || 0, id: id})
} }
</Text> </Text>
{ {

View file

@ -15,6 +15,9 @@ import SpaceIC from '../../../../public/res/ic/outlined/space.svg';
import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg'; import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg';
import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg'; import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
const visibility = { const visibility = {
INVITE: 'invite', INVITE: 'invite',
RESTRICTED: 'restricted', RESTRICTED: 'restricted',
@ -75,19 +78,21 @@ function RoomVisibility({ roomId }) {
const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0; const myPowerlevel = room.getMember(mx.getUserId())?.powerLevel || 0;
const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel); const canChange = room.currentState.hasSufficientPowerLevelFor('state_default', myPowerlevel);
const { t } = useTranslation();
const items = [{ const items = [{
iconSrc: isSpace ? SpaceLockIC : HashLockIC, iconSrc: isSpace ? SpaceLockIC : HashLockIC,
text: 'Private (invite only)', text: 'Molecules.RoomVisibility.private',
type: visibility.INVITE, type: visibility.INVITE,
unsupported: false, unsupported: false,
}, { }, {
iconSrc: isSpace ? SpaceIC : HashIC, iconSrc: isSpace ? SpaceIC : HashIC,
text: roomVersion < 8 ? 'Restricted (unsupported: required room upgrade)' : 'Restricted (space member can join)', text: roomVersion < 8 ? 'Molecules.RoomVisibility.restricted_unsupported' : 'Molecules.RoomVisibility.restricted',
type: visibility.RESTRICTED, type: visibility.RESTRICTED,
unsupported: roomVersion < 8 || noSpaceParent, unsupported: roomVersion < 8 || noSpaceParent,
}, { }, {
iconSrc: isSpace ? SpaceGlobeIC : HashGlobeIC, iconSrc: isSpace ? SpaceGlobeIC : HashGlobeIC,
text: 'Public (anyone can join)', text: 'Molecules.RoomVisibility.public',
type: visibility.PUBLIC, type: visibility.PUBLIC,
unsupported: false, unsupported: false,
}]; }];
@ -104,7 +109,7 @@ function RoomVisibility({ roomId }) {
disabled={(!canChange || item.unsupported)} disabled={(!canChange || item.unsupported)}
> >
<Text varient="b1"> <Text varient="b1">
<span>{item.text}</span> <span>{t(item.text)}</span>
<RadioButton isActive={activeType === item.type} /> <RadioButton isActive={activeType === item.type} />
</Text> </Text>
</MenuItem> </MenuItem>

View file

@ -25,6 +25,10 @@ import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
import { t } from 'i18next';
function SpaceAddExistingContent({ roomId }) { function SpaceAddExistingContent({ roomId }) {
const mountStore = useStore(roomId); const mountStore = useStore(roomId);
const [debounce] = useState(new Debounce()); const [debounce] = useState(new Debounce());
@ -37,6 +41,9 @@ function SpaceAddExistingContent({ roomId }) {
spaces, rooms, directs, roomIdToParents, spaces, rooms, directs, roomIdToParents,
} = initMatrix.roomList; } = initMatrix.roomList;
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
const allIds = [...spaces, ...rooms, ...directs].filter((rId) => ( const allIds = [...spaces, ...rooms, ...directs].filter((rId) => (
rId !== roomId && !roomIdToParents.get(rId)?.has(roomId) rId !== roomId && !roomIdToParents.get(rId)?.has(roomId)
@ -59,7 +66,7 @@ function SpaceAddExistingContent({ roomId }) {
}; };
const handleAdd = async () => { const handleAdd = async () => {
setProcess(`Adding ${selected.length} items...`); setProcess(t("Molecules.SpaceAddExisting.adding_items", {count: selected.length}));
const promises = selected.map((rId) => { const promises = selected.map((rId) => {
const room = mx.getRoom(rId); const room = mx.getRoom(rId);
@ -119,12 +126,12 @@ function SpaceAddExistingContent({ roomId }) {
<Input <Input
name="searchInput" name="searchInput"
onChange={handleSearch} onChange={handleSearch}
placeholder="Search room" placeholder={t("Molecules.SpaceAddExisting.search_rooms_placeholder")}
autoFocus autoFocus
/> />
<IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} /> <IconButton size="small" type="button" onClick={handleSearchClear} src={CrossIC} />
</form> </form>
{searchIds?.length === 0 && <Text>No results found</Text>} {searchIds?.length === 0 && <Text>{t("Molecules.SpaceAddExisting.no_results")}</Text>}
{ {
(searchIds || allRoomIds).map((rId) => { (searchIds || allRoomIds).map((rId) => {
const room = mx.getRoom(rId); const room = mx.getRoom(rId);
@ -171,9 +178,9 @@ function SpaceAddExistingContent({ roomId }) {
{selected.length !== 0 && ( {selected.length !== 0 && (
<div className="space-add-existing__footer"> <div className="space-add-existing__footer">
{process && <Spinner size="small" />} {process && <Spinner size="small" />}
<Text weight="medium">{process || `${selected.length} item selected`}</Text> <Text weight="medium">{t("Molecules.SpaceAddExisting.items_selected", {count: selected.length})}</Text>
{ !process && ( { !process && (
<Button onClick={handleAdd} variant="primary">Add</Button> <Button onClick={handleAdd} variant="primary">{t("Molecules.SpaceAddExisting.add_button")}</Button>
)} )}
</div> </div>
)} )}
@ -212,10 +219,10 @@ function SpaceAddExisting() {
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)' }}> add existing rooms</span> <span style={{ color: 'var(--tc-surface-low)' }}> {t("Molecules.SpaceAddExisting.subtitle")}</span>
</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

@ -26,6 +26,9 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog'; import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function SpaceOptions({ roomId, afterOptionSelect }) { function SpaceOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
@ -33,6 +36,8 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId); const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId); const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
const { t } = useTranslation();
const handleInviteClick = () => { const handleInviteClick = () => {
openInviteUser(roomId); openInviteUser(roomId);
afterOptionSelect(); afterOptionSelect();
@ -59,9 +64,9 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
const handleLeaveClick = async () => { const handleLeaveClick = async () => {
afterOptionSelect(); afterOptionSelect();
const isConfirmed = await confirmDialog( const isConfirmed = await confirmDialog(
'Leave space', t("Molecules.SpaceOptions.leave_space"),
`Are you sure that you want to leave "${room.name}" space?`, t("Molecules.SpaceOptions.leave_space_confirmation", {space: room.name}),
'Leave', t("Molecules.SpaceOptions.leave_space_confirmation"),
'danger', 'danger',
); );
if (!isConfirmed) return; if (!isConfirmed) return;
@ -75,29 +80,29 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
onClick={handleCategorizeClick} onClick={handleCategorizeClick}
iconSrc={isCategorized ? CategoryFilledIC : CategoryIC} iconSrc={isCategorized ? CategoryFilledIC : CategoryIC}
> >
{isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'} {isCategorized ? t("Organisms.SpaceSettings.uncategorize_subspaces") : t("Organisms.SpaceSettings.categorize_subspaces")}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={handlePinClick} onClick={handlePinClick}
iconSrc={isPinned ? PinFilledIC : PinIC} iconSrc={isPinned ? PinFilledIC : PinIC}
> >
{isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'} {isPinned ? t("Organisms.SpaceSettings.unpin_sidebar") : t("Organisms.SpaceSettings.pin_sidebar")}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
iconSrc={AddUserIC} iconSrc={AddUserIC}
onClick={handleInviteClick} onClick={handleInviteClick}
disabled={!canInvite} disabled={!canInvite}
> >
Invite {t("Molecules.SpaceOptions.invite")}
</MenuItem> </MenuItem>
<MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>Manage rooms</MenuItem> <MenuItem onClick={handleManageRoom} iconSrc={HashSearchIC}>{t("Molecules.SpaceOptions.manage_rooms")}</MenuItem>
<MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>Settings</MenuItem> <MenuItem onClick={handleSettingsClick} iconSrc={SettingsIC}>{t("Molecules.SpaceOptions.settings")}</MenuItem>
<MenuItem <MenuItem
variant="danger" variant="danger"
onClick={handleLeaveClick} onClick={handleLeaveClick}
iconSrc={LeaveArrowIC} iconSrc={LeaveArrowIC}
> >
Leave {t("Molecules.SpaceOptions.leave")}
</MenuItem> </MenuItem>
</div> </div>
); );

View file

@ -6,8 +6,12 @@ import { createTemporaryClient, startSsoLogin } from '../../../client/action/aut
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function SSOButtons({ type, identityProviders, baseUrl }) { function SSOButtons({ type, identityProviders, baseUrl }) {
const tempClient = createTemporaryClient(baseUrl); const tempClient = createTemporaryClient(baseUrl);
const { t } = useTranslation();
function handleClick(id) { function handleClick(id) {
startSsoLogin(baseUrl, type, id); startSsoLogin(baseUrl, type, id);
} }
@ -24,7 +28,7 @@ function SSOButtons({ type, identityProviders, baseUrl }) {
<button key={idp.id} type="button" className="sso-btn" onClick={() => handleClick(idp.id)}> <button key={idp.id} type="button" className="sso-btn" onClick={() => handleClick(idp.id)}>
<img className="sso-btn__img" src={tempClient.mxcUrlToHttp(idp.icon)} alt={idp.name} /> <img className="sso-btn__img" src={tempClient.mxcUrlToHttp(idp.icon)} alt={idp.name} />
</button> </button>
) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{`Login with ${idp.name}`}</Button> ) : <Button key={idp.id} className="sso-btn__text-only" onClick={() => handleClick(idp.id)}>{t("Molecules.SSOButtons.login_with", {idp_name: idp.name})}</Button>
))} ))}
</div> </div>
); );

View file

@ -18,6 +18,9 @@ import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg
import { useForceUpdate } from '../../hooks/useForceUpdate'; import { useForceUpdate } from '../../hooks/useForceUpdate';
import '../../i18n.jsx'
import { useTranslation } from 'react-i18next';
function Selector({ function Selector({
roomId, isDM, drawerPostie, onClick, roomId, isDM, drawerPostie, onClick,
}) { }) {
@ -30,6 +33,8 @@ function Selector({
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE; const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
const { t } = useTranslation();
const [, forceUpdate] = useForceUpdate(); const [, forceUpdate] = useForceUpdate();
useEffect(() => { useEffect(() => {
@ -69,7 +74,7 @@ function Selector({
options={( options={(
<IconButton <IconButton
size="extra-small" size="extra-small"
tooltip="Options" tooltip={t("common.options")}
tooltipPlacement="right" tooltipPlacement="right"
src={VerticalMenuIC} src={VerticalMenuIC}
onClick={openOptions} onClick={openOptions}

View file

@ -25,6 +25,9 @@ import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
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 simplyfiMembers(members) { function simplyfiMembers(members) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
return members.map((member) => ({ return members.map((member) => ({
@ -50,6 +53,8 @@ function PeopleDrawer({ roomId }) {
const [searchedMembers, setSearchedMembers] = useState(null); const [searchedMembers, setSearchedMembers] = useState(null);
const searchRef = useRef(null); const searchRef = useRef(null);
const { t } = useTranslation();
const getMembersWithMembership = useCallback( const getMembersWithMembership = useCallback(
(mship) => room.getMembersWithMembership(mship), (mship) => room.getMembersWithMembership(mship),
[roomId, membership], [roomId, membership],
@ -129,11 +134,11 @@ function PeopleDrawer({ roomId }) {
<Header> <Header>
<TitleWrapper> <TitleWrapper>
<Text variant="s1" primary> <Text variant="s1" primary>
People {t("Organisms.PeopleDrawer.title")}
<Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text> <Text className="people-drawer__member-count" variant="b3">{t("Organisms.PeopleDrawer.members", {count: room.getJoinedMemberCount()})}</Text>
</Text> </Text>
</TitleWrapper> </TitleWrapper>
<IconButton onClick={() => openInviteUser(roomId)} tooltip="Invite" src={AddUserIC} disabled={!canInvite} /> <IconButton onClick={() => openInviteUser(roomId)} tooltip={t("Organisms.PeopleDrawer.invite_tooltip")} src={AddUserIC} disabled={!canInvite} />
</Header> </Header>
<div className="people-drawer__content-wrapper"> <div className="people-drawer__content-wrapper">
<div className="people-drawer__scrollable"> <div className="people-drawer__scrollable">
@ -150,7 +155,7 @@ function PeopleDrawer({ roomId }) {
return getSegmentIndex[membership]; return getSegmentIndex[membership];
})() })()
} }
segments={[{ text: 'Joined' }, { text: 'Invited' }, { text: 'Banned' }]} segments={[{ text: t("Organisms.PeopleDrawer.joined")}, { text: t("Organisms.PeopleDrawer.invited") }, { text: t("Organisms.PeopleDrawer.banned") }]}
onSelect={(index) => { onSelect={(index) => {
const selectSegment = [ const selectSegment = [
() => setMembership('join'), () => setMembership('join'),
@ -176,7 +181,7 @@ function PeopleDrawer({ roomId }) {
(searchedMembers?.data.length === 0 || memberList.length === 0) (searchedMembers?.data.length === 0 || memberList.length === 0)
&& ( && (
<div className="people-drawer__noresult"> <div className="people-drawer__noresult">
<Text variant="b2">No results found!</Text> <Text variant="b2">{t("Organisms.PeopleDrawer.search_no_results")}</Text>
</div> </div>
) )
} }
@ -186,7 +191,7 @@ function PeopleDrawer({ roomId }) {
&& memberList.length > itemCount && memberList.length > itemCount
&& searchedMembers === null && searchedMembers === null
&& ( && (
<Button onClick={loadMorePeople}>View more</Button> <Button onClick={loadMorePeople}>{t("Organisms.PeopleDrawer.view_more")}</Button>
) )
} }
</div> </div>
@ -196,7 +201,7 @@ function PeopleDrawer({ roomId }) {
<div className="people-drawer__sticky"> <div className="people-drawer__sticky">
<form onSubmit={(e) => e.preventDefault()} className="people-search"> <form onSubmit={(e) => e.preventDefault()} className="people-search">
<RawIcon size="small" src={SearchIC} /> <RawIcon size="small" src={SearchIC} />
<Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder="Search" required /> <Input forwardRef={searchRef} type="text" onChange={handleSearch} placeholder={t("Organisms.PeopleDrawer.placeholder")} required />
{ {
searchedMembers !== null searchedMembers !== null
&& <IconButton onClick={handleSearch} size="small" src={CrossIC} /> && <IconButton onClick={handleSearch} size="small" src={CrossIC} />

View file

@ -104,7 +104,6 @@ function RoomViewFloating({
const room = initMatrix.matrixClient.getRoom(roomId) const room = initMatrix.matrixClient.getRoom(roomId)
const getUserDisplayName = (userId) => { const getUserDisplayName = (userId) => {
console.log(userId);
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId)); if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
return getUsername(userId); return getUsername(userId);
}; };
@ -135,7 +134,13 @@ function RoomViewFloating({
<Text variant="b2"> <Text variant="b2">
<Trans <Trans
i18nKey="Organisms.RoomViewFloating.user_typing" i18nKey="Organisms.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])}} values={{
count: typingMembers.size,
user_one: twemojify(getUserDisplayName(typingMemberValues?.[0])),
user_two: twemojify(getUserDisplayName(typingMemberValues?.[1])),
user_three: twemojify(getUserDisplayName(typingMemberValues?.[2])),
user_four: twemojify(getUserDisplayName(typingMemberValues?.[3]))
}}
components={{bold: <b/>}} components={{bold: <b/>}}
/> />
</Text> </Text>

View file

@ -39,6 +39,11 @@ export function getUsernameOfRoomMember(roomMember) {
return roomMember.name || roomMember.userId; return roomMember.name || roomMember.userId;
} }
export function getUserDisplayName(room, userId){
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
return getUsername(userId);
}
export async function isRoomAliasAvailable(alias) { export async function isRoomAliasAvailable(alias) {
try { try {
const result = await initMatrix.matrixClient.resolveRoomAlias(alias); const result = await initMatrix.matrixClient.resolveRoomAlias(alias);