feat: i18n

This commit is contained in:
Tristan Yang
2022-11-23 19:38:45 +08:00
parent 9b3dc2f740
commit 48ffdcc1b5
97 changed files with 1470 additions and 652 deletions
+6 -4
View File
@@ -8,6 +8,7 @@ import ChannelIcon from "./ChannelIcon";
import ChannelModal from "./ChannelModal";
import UsersModal from "./UsersModal";
import InviteModal from "./InviteModal";
import { useTranslation } from "react-i18next";
const Styled = styled.ul`
z-index: 999;
@@ -42,6 +43,7 @@ const Styled = styled.ul`
}
`;
export default function AddEntriesMenu() {
const { t } = useTranslation();
const currentUser = useAppSelector((store) => store.authData.user);
const [isPrivate, setIsPrivate] = useState(false);
const [inviteModalVisible, setInviteModalVisible] = useState(false);
@@ -79,20 +81,20 @@ export default function AddEntriesMenu() {
{currentUser?.is_admin && (
<li className="item" onClick={handleOpenChannelModal.bind(null, false)}>
<ChannelIcon className="icon" />
New Channel
{t("action.new_channel")}
</li>
)}
<li className="item" onClick={handleOpenChannelModal.bind(null, true)}>
<ChannelIcon personal={true} className="icon" />
New Private Channel
{t("action.new_private_channel")}
</li>
<li className="item" onClick={toggleUsersModalVisible}>
<IconMention className="icon" />
New Message
{t("action.new_msg")}
</li>
<li className="item" onClick={toggleInviteModalVisible}>
<IconInvite className="icon" />
Invite People
{t("action.invite_people")}
</li>
</Styled>
{channelModalVisible && <ChannelModal personal={isPrivate} closeModal={handleCloseModal} />}
+3 -1
View File
@@ -2,6 +2,7 @@ import { ChangeEvent, FC, useState } from "react";
import styled from "styled-components";
import Avatar from "./Avatar";
import uploadIcon from "../../assets/icons/upload.image.svg?url";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
width: 96px;
@@ -75,6 +76,7 @@ const AvatarUploader: FC<Props> = ({
uploadImage,
disabled = false
}) => {
const { t } = useTranslation();
const [uploading, setUploading] = useState(false);
const handleUpload = async (evt: ChangeEvent<HTMLInputElement>) => {
if (uploading) return;
@@ -91,7 +93,7 @@ const AvatarUploader: FC<Props> = ({
<Avatar type={type} url={url} name={name} />
{!disabled && (
<>
<div className="tip">{uploading ? `Uploading` : `Change Avatar`}</div>
<div className="tip">{uploading ? t("status.uploading") : t("action.change_avatar")}</div>
<input
multiple={false}
onChange={handleUpload}
+9 -8
View File
@@ -7,6 +7,7 @@ import IconInvite from "../../assets/icons/placeholder.invite.svg";
import IconDownload from "../../assets/icons/placeholder.download.svg";
import UsersModal from "./UsersModal";
import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next";
interface Props {
@@ -18,6 +19,7 @@ const classes = {
boxTip: "px-5 text-sm text-[#475467] font-bold text-center"
};
const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
const { t } = useTranslation("welcome");
const server = useAppSelector((store) => store.server);
const [inviteModalVisible, setInviteModalVisible] = useState(false);
const [createChannelVisible, setCreateChannelVisible] = useState(false);
@@ -32,22 +34,21 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
setInviteModalVisible((prev) => !prev);
};
const chatTip =
type == "chat" ? "Create a Channel to Start a Conversation" : "Send a Direct Message";
type == "chat" ? t("start_by_channel") : t("start_by_dm");
const chatHandler = type == "chat" ? toggleChannelModalVisible : toggleUserListVisible;
return (
<>
<div className="flex flex-col gap-8 -mt-[50px]">
<div className="flex flex-col gap-2 items-center">
<h2 className="text-3xl text-[#344054] font-bold">Welcome to {server.name} server</h2>
<h2 className="text-3xl text-[#344054] font-bold">{t("title", { name: server.name })}</h2>
<p className="text-sm text-[#98a2b3] max-w-[424px] text-center">
Here are some steps to help you get started. For more, check out our Getting Started
guide
{t("desc")}
</p>
</div>
<div className="grid grid-cols-2 grid-rows-2 gap-6">
<div className={classes.box} onClick={toggleInviteModalVisible}>
<IconInvite className={classes.boxIcon} />
<div className={classes.boxTip}>Invite your friends or teammates</div>
<div className={classes.boxTip}>{t("invite")}</div>
</div>
<button onClick={chatHandler} className={classes.box} >
<IconChat className={classes.boxIcon} />
@@ -55,11 +56,11 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
</button>
<a href={"https://voce.chat#download"} target={"_blank"} rel="noreferrer" className={classes.box} >
<IconDownload className={classes.boxIcon} />
<div className={classes.boxTip}>Download Mobile apps</div>
<div className={classes.boxTip}>{t("download")}</div>
</a>
<a href={"https://voce.chat"} target={"_blank"} rel="noreferrer" className={classes.box} >
<a href={"https://doc.voce.chat"} target={"_blank"} rel="noreferrer" className={classes.box} >
<IconAsk className={classes.boxIcon} />
<div className={classes.boxTip}>Got questions? Visit our help center</div>
<div className={classes.boxTip}>{t("help")}</div>
</a>
</div>
</div>
+10 -8
View File
@@ -12,6 +12,7 @@ import useFilteredUsers from "../../hook/useFilteredUsers";
import { useCreateChannelMutation } from "../../../app/services/channel";
import { useAppSelector } from "../../../app/store";
import { CreateChannelDTO } from "../../../types/channel";
import { useTranslation } from "react-i18next";
interface Props {
personal?: boolean;
@@ -19,6 +20,7 @@ interface Props {
}
const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
const { t } = useTranslation("chat");
const { usersData, loginUid } = useAppSelector((store) => {
return { usersData: store.users.byId, loginUid: store.authData.user?.uid };
});
@@ -98,7 +100,7 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
<input
value={input}
onChange={handleInputChange}
placeholder="Type Username to search"
placeholder={t("search_user_placeholder") || ""}
/>
</div>
{users && (
@@ -129,21 +131,21 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
</div>
)}
<div className={`right ${!is_public ? "private" : ""}`}>
<h3 className="title">Create New Channel</h3>
<h3 className="title">{t("create_channel")}</h3>
<p className="desc normal">
{!is_public
? "This is a private channel, only select members will be able to join."
: "This is a public channel, everyone in this team can see it."}
? t("create_private_channel_desc")
: t("create_channel_desc")}
</p>
<div className="name">
<span className="label normal">Channel Name</span>
<span className="label normal">{t("channel_name")}</span>
<div className="input">
<input onChange={handleNameInput} value={name} placeholder="new channel" />
<ChannelIcon personal={!is_public} className="icon" />
</div>
</div>
<div className="private">
<span className="txt normal">Private Channel</span>
<span className="txt normal">{t("private_channel")}</span>
<StyledToggle
data-checked={!is_public}
data-disabled={!loginUser?.is_admin}
@@ -152,10 +154,10 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
</div>
<div className="btns">
<Button onClick={closeModal} className="normal cancel">
Cancel
{t("action.cancel", { ns: "common" })}
</Button>
<Button disabled={isLoading} onClick={handleCreate} className="normal">
Create
{t("action.create", { ns: "common" })}
</Button>
</div>
</div>
@@ -50,6 +50,7 @@ const StyledWrapper = styled.div`
}
}
.right {
min-width: 400px;
display: flex;
flex-direction: column;
align-items: center;
+5 -3
View File
@@ -1,13 +1,15 @@
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { useGetServerVersionQuery } from "../../app/services/server";
type Props = {};
const FAQ: FC<Props> = () => {
const { t } = useTranslation("setting", { keyPrefix: "faq" });
const { data: serverVersion } = useGetServerVersionQuery();
return (
<div className="flex flex-col gap-3">
<div className="item">Client Version: {process.env.VERSION}</div>
<div className="item">Server Version: {serverVersion}</div>
<div className="item">Build Timestamp: {process.env.REACT_APP_BUILD_TIME}</div>
<div className="item">{t("client_version")}: {process.env.VERSION}</div>
<div className="item">{t("server_version")}: {serverVersion}</div>
<div className="item">{t("build_time")}: {process.env.REACT_APP_BUILD_TIME}</div>
</div>
);
};
+3 -1
View File
@@ -2,6 +2,7 @@ import { FC, useEffect } from "react";
import IconGithub from "../../assets/icons/github.svg";
import styled from "styled-components";
import Button from "./styled/Button";
import { useTranslation } from "react-i18next";
const StyledSocialButton = styled(Button)`
width: 100%;
@@ -25,6 +26,7 @@ type Props = {
};
const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", client_id }) => {
const { t } = useTranslation("auth");
useEffect(() => {
const handleGithubLoginStatusChange = (evt: StorageEvent) => {
const { key, newValue } = evt;
@@ -60,7 +62,7 @@ const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", clien
return (
<StyledSocialButton onClick={handleGithubLogin}>
<IconGithub className="icon" />
{` ${type === "login" ? "Sign in" : "Sign up"} with Github`}
{` ${type === "login" ? t("login.github") : t("reg.github")}`}
</StyledSocialButton>
);
};
+3 -1
View File
@@ -6,6 +6,7 @@ import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
import IconGoogle from "../../assets/icons/google.svg";
import Button from "./styled/Button";
import { useLoginMutation } from "../../app/services/auth";
import { useTranslation } from "react-i18next";
const StyledSocialButton = styled(Button)`
position: relative;
@@ -59,6 +60,7 @@ interface Props {
}
const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
const { t } = useTranslation("auth");
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
//拿本地存的magic token
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
@@ -91,7 +93,7 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
{loadError
? "Script Load Error!"
: loaded
? `${type === "login" ? "Sign in" : "Sign up"} with Google`
? `${type === "login" ? t("login.google") : t("reg.google")}`
: `Initializing`}
</div>
<div className="hide">
+6 -4
View File
@@ -3,6 +3,7 @@ import useInviteLink from "../hook/useInviteLink";
import Input from "./styled/Input";
import Button from "./styled/Button";
import { FC } from "react";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
display: flex;
@@ -40,6 +41,7 @@ const StyledWrapper = styled.div`
`;
type Props = {};
const InviteLink: FC<Props> = () => {
const { t } = useTranslation("chat");
const { generating, link, linkCopied, copyLink, generateNewLink } = useInviteLink();
const handleNewLink = () => {
generateNewLink();
@@ -47,16 +49,16 @@ const InviteLink: FC<Props> = () => {
return (
<StyledWrapper>
<span className="tip">Share this link to invite people to this server.</span>
<span className="tip">{t("share_invite_link")}</span>
<div className="link">
<Input readOnly className={"large"} placeholder="Generating" value={link} />
<Button onClick={copyLink} className="ghost small border_less">
{linkCopied ? "Copied" : `Copy`}
{linkCopied ? "Copied" : t("action.copy", { ns: "common" })}
</Button>
</div>
<span className="sub_tip">Invite link expires in 7 days.</span>
<span className="sub_tip">{t("invite_link_expire")}</span>
<Button className="ghost" disabled={generating} onClick={handleNewLink}>
{generating ? `Generating` : `Generate New Link`}
{generating ? `Generating` : t("generate_new_link")}
</Button>
</StyledWrapper>
);
@@ -1,5 +1,6 @@
import { useEffect, useState, ChangeEvent, FC } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import useInviteLink from "../../hook/useInviteLink";
import Button from "../styled/Button";
@@ -59,6 +60,7 @@ const Styled = styled.div`
font-size: 12px;
line-height: 18px;
button {
margin-left: 4px;
background: none;
border: none;
color: #22ccee;
@@ -71,6 +73,7 @@ interface Props {
}
const InviteByEmail: FC<Props> = ({ cid }) => {
const { t } = useTranslation("chat");
const [email, setEmail] = useState("");
const { enableSMTP, linkCopied, link, copyLink, generateNewLink, generating } =
useInviteLink(cid);
@@ -87,33 +90,33 @@ const InviteByEmail: FC<Props> = ({ cid }) => {
return (
<Styled>
<div className="invite">
<label htmlFor="">Invite by Email</label>
<label htmlFor="">{t("invite_by_email")}</label>
<div className="input">
<Input
value={email}
onChange={handleEmail}
disabled={!enableSMTP}
type="email"
placeholder={enableSMTP ? "Enter Email" : "Enable SMTP First"}
placeholder={enableSMTP ? "Enter Email" : t("enable_smtp") || ""}
/>
<Button disabled={!enableSMTP || !email} className="send">
Send
{t("action.send", { ns: "common" })}
</Button>
</div>
</div>
<div className="link">
<label htmlFor="">Or Send invite link to your friends</label>
<label htmlFor="">{t("send_invite_link")}</label>
<div className="input">
<Input readOnly className="invite" placeholder="Generating" value={link} />
<button className="copy" onClick={copyLink}>
Copy
{t("action.copy", { ns: "common" })}
</button>
</div>
</div>
<div className="tip">
Invite link expires in 7 days.{" "}
{t("invite_link_expire")}
<button disabled={generating} className="new" onClick={() => generateNewLink()}>
Generate New Link
{t("generate_new_link")}
</button>
</div>
</Styled>
+3 -1
View File
@@ -5,6 +5,7 @@ import CloseIcon from "../../../assets/icons/close.svg";
import Modal from "../Modal";
import { useAppSelector } from "../../../app/store";
import { FC } from "react";
import { useTranslation } from "react-i18next";
const Styled = styled.div`
display: flex;
@@ -37,6 +38,7 @@ interface Props {
}
const InviteModal: FC<Props> = ({ type = "server", cid, title = "", closeModal }) => {
const { t } = useTranslation("chat");
const { channel, server } = useAppSelector((store) => {
return {
channel: cid ? store.channels.byId[cid] : undefined,
@@ -48,7 +50,7 @@ const InviteModal: FC<Props> = ({ type = "server", cid, title = "", closeModal }
<Modal>
<Styled>
<h2 className="title">
Add friends to {finalTitle}
{t("invite_title", { name: finalTitle })}
<CloseIcon className="close" onClick={closeModal} />
</h2>
{!channel?.is_public && <AddMembers cid={cid} closeModal={closeModal} />}
@@ -5,6 +5,7 @@ import useLeaveChannel from "../../hook/useLeaveChannel";
import Modal from "../Modal";
import StyledModal from "../styled/Modal";
import Button from "../styled/Button";
import { useTranslation } from "react-i18next";
interface Props {
id: number;
@@ -13,6 +14,7 @@ interface Props {
}
const LeaveConfirmModal: FC<Props> = ({ id, closeModal, handleNextStep }) => {
const { t } = useTranslation("setting");
const navigateTo = useNavigate();
const { isOwner, leaving, leaveChannel, leaveSuccess } = useLeaveChannel(id);
@@ -28,16 +30,16 @@ const LeaveConfirmModal: FC<Props> = ({ id, closeModal, handleNextStep }) => {
<Modal id="modal-modal">
<StyledModal
className="compact"
title="Leave Channel"
title={t("channel.leave") || ""}
description={
isOwner
? "You need to transfer your channel ownership to someone else before leaving the channel."
: "Are you sure want to leave this channel?"
? t("channel.transfer_desc") || ""
: t("channel.leave_desc") || ""
}
buttons={
<>
<Button onClick={closeModal.bind(null, undefined)} className="cancel">
Cancel
{t("action.cancel", { ns: "common" })}
</Button>
{isOwner ? (
<Button onClick={handleNextStep} className="main">
@@ -7,6 +7,7 @@ import useLeaveChannel from "../../hook/useLeaveChannel";
import StyledModal from "../styled/Modal";
import Button from "../styled/Button";
import User from "../User";
import { useTranslation } from "react-i18next";
const UserList = styled.ul`
display: flex;
@@ -37,6 +38,7 @@ interface Props {
}
const TransferOwnerModal: FC<Props> = ({ id, closeModal, withLeave = true }) => {
const { t } = useTranslation();
const {
transferOwner,
otherMembers,
@@ -80,7 +82,7 @@ const TransferOwnerModal: FC<Props> = ({ id, closeModal, withLeave = true }) =>
buttons={
<>
<Button onClick={closeModal.bind(null, undefined)} className="cancel">
Cancel
{t("action.cancel")}
</Button>
<Button disabled={!uid} onClick={handleTransferAndLeave} className="danger">
{operating ? "Assigning" : `Assign and Leave`}
+10 -8
View File
@@ -13,11 +13,13 @@ import IconArrowDown from "../../../assets/icons/arrow.down.mini.svg";
import IconCheck from "../../../assets/icons/check.sign.svg";
import useUserOperation from "../../hook/useUserOperation";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
interface Props {
cid?: number;
}
const ManageMembers: FC<Props> = ({ cid }) => {
const { t } = useTranslation(["member", "common"]);
const { users, channels, loginUser } = useAppSelector((store) => {
return {
users: store.users,
@@ -54,9 +56,9 @@ const ManageMembers: FC<Props> = ({ cid }) => {
<StyledWrapper>
{loginUser?.is_admin && <InviteLink />}
<div className="intro">
<h4 className="title">Manage Members</h4>
<h4 className="title">{t("manage_members")}</h4>
<p className="desc">
Disabling your account means you can recover it at any time after taking this action.
{t("manage_tip")}
</p>
</div>
@@ -84,7 +86,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
</div>
<div className="right">
<span className="role">
{is_admin ? "Admin" : "User"}
{is_admin ? t("admin") : t("user")}
{switchRoleVisible && (
<Tippy
interactive
@@ -100,7 +102,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
isAdmin: true
})}
>
Admin
{t("admin")}
{is_admin && <IconCheck className="icon" />}
</li>
<li
@@ -111,7 +113,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
isAdmin: false
})}
>
User
{t("user")}
{!is_admin && <IconCheck className="icon" />}
</li>
</StyledMenu>
@@ -130,17 +132,17 @@ const ManageMembers: FC<Props> = ({ cid }) => {
<StyledMenu className="menu">
{email && (
<li className="item" onClick={copyEmail.bind(null, email)}>
Copy Email
{t("action.copy_email", { ns: "common" })}
</li>
)}
{canRemoveFromChannel && (
<li className="item danger" onClick={removeFromChannel.bind(null, uid)}>
Remove From Channel
{t("remove_from_channel")}
</li>
)}
{canRemove && (
<li className="item danger" onClick={removeUser.bind(null, uid)}>
Remove
{t("action.remove", { ns: "common" })}
</li>
)}
</StyledMenu>
+11 -9
View File
@@ -20,6 +20,7 @@ import IconSelect from "../../../assets/icons/select.svg";
import IconDelete from "../../../assets/icons/delete.svg";
import moreIcon from "../../../assets/icons/more.svg?url";
import useMessageOperation from "./useMessageOperation";
import { useTranslation } from "react-i18next";
const StyledCmds = styled.ul`
z-index: 999;
@@ -68,6 +69,7 @@ type Props = {
toggleEditMessage: () => void;
};
const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleEditMessage }) => {
const { t } = useTranslation();
const {
canDelete,
canReply,
@@ -134,27 +136,27 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
content={<ReactionPicker mid={mid} hidePicker={hideAll} />}
>
<li className="cmd">
<Tooltip placement="top" tip="Add Reaction">
<Tooltip placement="top" tip={t("action.add_reaction")}>
<img src={reactIcon} className="toggler" alt="icon emoji" />
</Tooltip>
</li>
</Tippy>
{canEdit && (
<li className="cmd" onClick={toggleEditMessage}>
<Tooltip placement="top" tip="Edit">
<Tooltip placement="top" tip={t("action.edit")}>
<img src={editIcon} alt="icon edit" />
</Tooltip>
</li>
)}
{canReply && (
<li className="cmd" onClick={handleReply}>
<Tooltip placement="top" tip="Reply">
<Tooltip placement="top" tip={t("action.reply")}>
<img src={replyIcon} alt="icon reply" />
</Tooltip>
</li>
)}
<li className="cmd fav" onClick={handleAddFav}>
<Tooltip placement="top" tip="Add to Favorites">
<Tooltip placement="top" tip={t("action.add_to_fav")}>
<IconBookmark />
</Tooltip>
</li>
@@ -169,22 +171,22 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
items={
[
canPin && {
title: pinned ? `Unpin Message` : `Pin Message`,
title: pinned ? t("action.unpin") : t("action.pin"),
icon: <IconPin className="icon" />,
handler: pinned ? handleUnpin : togglePinModal
},
{
title: "Forward",
title: t("action.forward"),
icon: <IconForward className="icon" />,
handler: toggleForwardModal
},
{
title: "Select",
title: t("action.select"),
icon: <IconSelect className="icon" />,
handler: handleSelect.bind(null, mid)
},
canDelete && {
title: " Delete",
title: t("action.remove"),
danger: true,
icon: <IconDelete className="icon" />,
handler: toggleDeleteModal
@@ -195,7 +197,7 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
}
>
<li className="cmd">
<Tooltip placement="top" tip="More">
<Tooltip placement="top" tip={t("more")}>
<img src={moreIcon} alt="icon more" />
</Tooltip>
</li>
+9 -7
View File
@@ -12,6 +12,7 @@ import IconSelect from "../../../assets/icons/select.svg";
import { updateSelectMessages } from "../../../app/slices/ui";
import useSendMessage from "../../hook/useSendMessage";
import useMessageOperation from "./useMessageOperation";
import { useTranslation } from "react-i18next";
type Props = {
context: "user" | "channel";
contextId: number;
@@ -30,6 +31,7 @@ const MessageContextMenu: FC<Props> = ({
editMessage,
children
}) => {
const { t } = useTranslation();
const {
copyContent,
canEdit,
@@ -58,37 +60,37 @@ const MessageContextMenu: FC<Props> = ({
};
const items = [
canEdit && {
title: "Edit Message",
title: t("action.edit_msg"),
icon: <IconEdit className="icon" />,
handler: editMessage
},
canReply && {
title: "Reply",
title: t("action.reply"),
icon: <IconReply className="icon" />,
handler: handleReply
},
canCopy && {
title: "Copy",
title: t("action.copy"),
icon: <IconCopy className="icon" />,
handler: copyContent
},
canPin && {
title: pinned ? "Unpin" : "Pin",
title: pinned ? t("action.unpin") : t("action.pin"),
icon: <IconPin className="icon" />,
handler: pinned ? unPin.bind(null, mid) : togglePinModal
},
{
title: "Forward",
title: t("action.forward"),
icon: <IconForward className="icon" />,
handler: toggleForwardModal
},
{
title: "Select",
title: t("action.select"),
icon: <IconSelect className="icon" />,
handler: handleSelect
},
canDelete && {
title: "Delete",
title: t("action.remove"),
danger: true,
icon: <IconDelete className="icon" />,
handler: toggleDeleteModal
@@ -5,6 +5,7 @@ import renderContent from "./renderContent";
import Avatar from "../Avatar";
import IconForward from "../../../assets/icons/forward.svg";
import useNormalizeMessage from "../../hook/useNormalizeMessage";
import { useTranslation } from "react-i18next";
const StyledForward = styled.div`
display: flex;
@@ -36,6 +37,7 @@ type Props = {
id: string;
};
const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
const { t } = useTranslation();
const { normalizeMessage, messages } = useNormalizeMessage();
const [forwards, setForwards] = useState<ReactElement | null>(null);
useEffect(() => {
@@ -52,7 +54,7 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
<StyledForward data-forwarded-mids={forward_mids.join(",")}>
<h4 className="tip">
<IconForward className="icon" />
Forwarded
{t("action.forward")}
</h4>
<div className="list">
{messages.map((msg, idx) => {
@@ -7,6 +7,7 @@ import StyledModal from "../styled/Modal";
import Button from "../styled/Button";
import Modal from "../Modal";
import PreviewMessage from "./PreviewMessage";
import { useTranslation } from "react-i18next";
const StyledPinModal = styled(StyledModal)`
min-width: 406px;
@@ -30,6 +31,7 @@ interface Props {
}
const PinMessageModal: FC<Props> = ({ closeModal, mid = 0, gid = 0 }) => {
const { t } = useTranslation();
const { channel, pinMessage, isPining, isSuccess } = usePinMessage(gid);
const handlePin = () => {
pinMessage(mid);
@@ -51,14 +53,14 @@ const PinMessageModal: FC<Props> = ({ closeModal, mid = 0, gid = 0 }) => {
buttons={
<>
<Button onClick={closeModal} className="cancel">
Cancel
{t("action.cancel")}
</Button>
<Button disabled={isPining} onClick={handlePin} className="main">
{isPining ? "Pining" : `Pin It`}
{isPining ? "Pining" : t("action.pin")}
</Button>
</>
}
title="Pin It"
title={t("action.pin") || ""}
description={`Do you want to pin this message to #${channel?.name}`}
>
<PreviewMessage mid={mid} />
+9 -7
View File
@@ -9,6 +9,7 @@ import StyledWrapper from "./styled";
import StyledMenu from "../styled/Menu";
import useUserOperation from "../../hook/useUserOperation";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
interface Props {
uid: number;
@@ -17,6 +18,7 @@ interface Props {
}
const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
const { t } = useTranslation(["member", "common"]);
const {
canCall,
call,
@@ -56,14 +58,14 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
<NavLink to={`/chat/dm/${uid}`}>
<li className="icon chat">
<IconMessage />
<span className="txt">Message</span>
<span className="txt">{t("send_msg")}</span>
</li>
</NavLink>
{/* <NavLink to={`#`}> */}
{enableCall && (
<li className="icon call" onClick={call}>
<IconCall />
<span className="txt">Call</span>
<span className="txt">{t("call")}</span>
</li>
)}
{/* </NavLink> */}
@@ -79,22 +81,22 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
{enableCall && (
<li className="item" onClick={call}>
{/* <IconCall className="icon" /> */}
Call
{t("call")}
</li>
)}
{canCopyEmail && (
<li className="item" onClick={copyEmail.bind(undefined, email)}>
Copy Email
{t("copy_email")}
</li>
)}
{canRemoveFromChannel && (
<li className="item danger" onClick={removeFromChannel.bind(null, uid)}>
Remove from Channel
{t("remove_from_channel")}
</li>
)}
{canRemoveFromServer && (
<li className="item danger" onClick={removeUser.bind(null, uid)}>
Remove from Server
{t("remove")}
</li>
)}
</StyledMenu>
@@ -102,7 +104,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
>
<li className={`icon more ${hasMore ? "" : "disabled"}`}>
<IconMore />
<span className="txt">More</span>
<span className="txt">{t("more", { ns: "common" })}</span>
</li>
</Tippy>
</ul>
+5 -3
View File
@@ -1,4 +1,5 @@
import { FC, MouseEvent } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
const StyledWrapper = styled.div`
@@ -54,15 +55,16 @@ interface Props {
}
const SaveTip: FC<Props> = ({ saveHandler, resetHandler }) => {
const { t } = useTranslation("setting");
return (
<StyledWrapper className="animate__animated animate__flipInX animate__faster">
<span className="txt">You have unsaved changes!</span>
<span className="txt">{t('save_tip')}</span>
<div className="btns">
<button className="btn reset" onClick={resetHandler}>
Reset
{t('reset')}
</button>
<button className="btn" onClick={saveHandler}>
Save Changes
{t('save_change')}
</button>
</div>
</StyledWrapper>
+4 -2
View File
@@ -5,6 +5,7 @@ import addIcon from "../../assets/icons/add.svg?url";
import AddEntriesMenu from "./AddEntriesMenu";
import Tooltip from "./Tooltip";
import { FC } from "react";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
position: relative;
@@ -35,13 +36,14 @@ const StyledWrapper = styled.div`
`;
type Props = {};
const Search: FC<Props> = () => {
const { t } = useTranslation();
return (
<StyledWrapper>
<div className="search">
<img src={searchIcon} alt="search icon" />
<input placeholder="Search..." className="input" />
<input placeholder={`${t("action.search")}...`} className="input" />
</div>
<Tooltip tip="More" placement="bottom">
<Tooltip tip={t("more")} placement="bottom">
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
<img src={addIcon} alt="add icon" className="add" />
</Tippy>
+3 -1
View File
@@ -6,6 +6,7 @@ import MarkdownIcon from "../../../assets/icons/markdown.svg";
import FullscreenIcon from "../../../assets/icons/fullscreen.svg";
import ExitFullscreenIcon from "../../../assets/icons/fullscreen.exit.svg";
import useUploadFile from "../../hook/useUploadFile";
import { useTranslation } from "react-i18next";
const Styled = styled.div`
display: flex;
@@ -58,6 +59,7 @@ const Toolbar: FC<Props> = ({
to,
context
}) => {
const { t } = useTranslation();
const { addStageFile } = useUploadFile({ context, id: to });
const fileInputRef = useRef<HTMLInputElement>(null);
const handleUpload = (evt: ChangeEvent<HTMLInputElement>) => {
@@ -94,7 +96,7 @@ const Toolbar: FC<Props> = ({
</Tooltip>
))}
</div>
<Tooltip placement="top" tip="Upload">
<Tooltip placement="top" tip={t("action.upload")}>
<div className="add">
<AddIcon />
<label htmlFor="file">
+5 -4
View File
@@ -15,6 +15,7 @@ import MixedInput, { useMixedEditor } from "../MixedInput";
import useDraft from "../../hook/useDraft";
import useUploadFile from "../../hook/useUploadFile";
import { useAppDispatch, useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
const Modes = {
text: "text",
@@ -29,6 +30,7 @@ const Send: FC<IProps> = ({
context = "channel",
id
}) => {
const { t } = useTranslation("chat");
const { resetStageFiles } = useUploadFile({ context, id });
const { getDraft, getUpdateDraft } = useDraft({ context, id });
const editor = useMixedEditor(`${context}_${id}`);
@@ -130,14 +132,13 @@ const Send: FC<IProps> = ({
setMarkdownFullscreen((prev) => !prev);
};
const name = context == "channel" ? channelsData[id]?.name : usersData[id]?.name;
const placeholder = `Send to ${ChatPrefixes[context]}${name} `;
const placeholder = `${t("send_to")} ${ChatPrefixes[context]}${name} `;
const members =
context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : [];
return (
<StyledSend
className={`send ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${
replying_mid ? "reply" : ""
} ${context}`}
className={`send ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${replying_mid ? "reply" : ""
} ${context}`}
>
{replying_mid && <Replying context={context} mid={replying_mid} id={id} />}
{mode == Modes.text && <UploadFileList context={context} id={id} />}
+5 -3
View File
@@ -5,6 +5,7 @@ import addIcon from "../../assets/icons/add.svg?url";
import Tooltip from "./Tooltip";
import AddEntriesMenu from "./AddEntriesMenu";
import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
min-height: 56px;
@@ -54,6 +55,7 @@ type Props = {
readonly?: boolean;
};
export default function Server({ readonly = false }: Props) {
const { t } = useTranslation();
const { pathname } = useLocation();
const { server, userCount } = useAppSelector((store) => {
return {
@@ -74,7 +76,7 @@ export default function Server({ readonly = false }: Props) {
<h3 className="name" title={description}>
{name}
</h3>
<span className="desc">{userCount} members</span>
<span className="desc">{userCount} {t("members")}</span>
</div>
</div>
</StyledWrapper>
@@ -91,12 +93,12 @@ export default function Server({ readonly = false }: Props) {
<h3 className="name" title={description}>
{name}
</h3>
<span className="desc">{userCount} members</span>
<span className="desc">{userCount} {t("members")}</span>
</div>
</div>
</NavLink>
<Tooltip tip="More" placement="bottom">
<Tooltip tip={t("more")} placement="bottom">
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
<img src={addIcon} alt="add icon" className="add" />
</Tippy>
+7 -5
View File
@@ -2,6 +2,7 @@ import { FC, ReactElement } from "react";
import Tippy from "@tippyjs/react";
import useUserOperation from "../../hook/useUserOperation";
import ContextMenu, { Item } from "../ContextMenu";
import { useTranslation } from "react-i18next";
interface Props {
enable?: boolean;
@@ -13,6 +14,7 @@ interface Props {
}
const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, children }) => {
const { t } = useTranslation("member");
const {
canCall,
call,
@@ -43,25 +45,25 @@ const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, c
items={
[
{
title: "Message",
title: t("send_msg"),
handler: startChat
},
canCall && {
title: "Call",
title: t("call"),
handler: call
},
canCopyEmail && {
title: "Copy Email",
title: t("copy_email"),
handler: copyEmail
},
canRemoveFromChannel && {
danger: true,
title: "Remove From Channel",
title: t("remove_from_channel"),
handler: removeFromChannel
},
canRemove && {
danger: true,
title: "Remove",
title: t("remove"),
handler: removeUser
}
].filter(Boolean) as Item[]
+3 -1
View File
@@ -6,6 +6,7 @@ import useFilteredUsers from "../hook/useFilteredUsers";
import User from "./User";
import Modal from "./Modal";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
display: flex;
@@ -56,6 +57,7 @@ interface Props {
}
const UsersModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation("chat");
const wrapperRef = useRef<HTMLDivElement>(null);
const { users, updateInput, input } = useFilteredUsers();
useOutsideClick(wrapperRef, closeModal);
@@ -68,7 +70,7 @@ const UsersModal: FC<Props> = ({ closeModal }) => {
<Modal>
<StyledWrapper ref={wrapperRef}>
<div className="search">
<input value={input} onChange={handleSearch} placeholder="Type Username to search" />
<input value={input} onChange={handleSearch} placeholder={t("search_user_placeholder") || ""} />
</div>
{users && (
<ul className="users">
+1
View File
@@ -12,6 +12,7 @@ const StyledButton = styled.button`
line-height: 20px;
color: #fff;
background-color: #22ccee;
word-break: keep-all;
&.flex {
width: 100%;
}
+3 -1
View File
@@ -4,6 +4,7 @@ import Tippy from "@tippyjs/react";
import IconSelect from "../../../assets/icons/check.sign.svg";
import IconArrow from "../../../assets/icons/arrow.down.svg";
import Menu from "./Menu";
import { useTranslation } from "react-i18next";
const Styled = styled.div`
user-select: none;
@@ -41,6 +42,7 @@ interface Props {
}
const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }) => {
const { t } = useTranslation();
const [optionsVisible, setOptionsVisible] = useState(false);
const [curr, setCurr] = useState<Partial<Option> | null>(null);
const toggleVisible = () => {
@@ -79,7 +81,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
}
>
<Styled onClick={toggleVisible}>
<span className="txt">{(current !== null ? current : curr)?.title || "Select"}</span>
<span className="txt">{(current !== null ? current : curr)?.title || t("action.select")}</span>
<IconArrow className="icon" />
</Styled>
</Tippy>