mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-09-24 16:46:05 +08:00
feat: add i18n deps (#18)
This commit is contained in:
@ -18,12 +18,14 @@
|
||||
"dayjs": "^1.11.7",
|
||||
"eventsource-parser": "^1.0.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"i18next": "^22.4.14",
|
||||
"lodash-es": "^4.17.21",
|
||||
"next": "^13.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-data-table-component": "^7.5.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-is": "^18.2.0",
|
||||
"react-loader-spinner": "^5.3.4",
|
||||
|
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@ -34,6 +34,9 @@ dependencies:
|
||||
highlight.js:
|
||||
specifier: ^11.7.0
|
||||
version: 11.7.0
|
||||
i18next:
|
||||
specifier: ^22.4.14
|
||||
version: 22.4.14
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@ -52,6 +55,9 @@ dependencies:
|
||||
react-hot-toast:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0(csstype@3.1.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
react-i18next:
|
||||
specifier: ^12.2.0
|
||||
version: 12.2.0(i18next@22.4.14)(react-dom@18.2.0)(react@18.2.0)
|
||||
react-icons:
|
||||
specifier: ^4.8.0
|
||||
version: 4.8.0(react@18.2.0)
|
||||
@ -2407,10 +2413,22 @@ packages:
|
||||
react-is: 16.13.1
|
||||
dev: false
|
||||
|
||||
/html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
dev: false
|
||||
|
||||
/hyphenate-style-name@1.0.4:
|
||||
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
|
||||
dev: false
|
||||
|
||||
/i18next@22.4.14:
|
||||
resolution: {integrity: sha512-VtLPtbdwGn0+DAeE00YkiKKXadkwg+rBUV+0v8v0ikEjwdiJ0gmYChVE4GIa9HXymY6wKapkL93vGT7xpq6aTw==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.0
|
||||
dev: false
|
||||
|
||||
/iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3714,6 +3732,26 @@ packages:
|
||||
- csstype
|
||||
dev: false
|
||||
|
||||
/react-i18next@12.2.0(i18next@22.4.14)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==}
|
||||
peerDependencies:
|
||||
i18next: '>= 19.0.0'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.0
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 22.4.14
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-icons@4.8.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==}
|
||||
peerDependencies:
|
||||
@ -4528,6 +4566,11 @@ packages:
|
||||
vfile-message: 3.1.4
|
||||
dev: false
|
||||
|
||||
/void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/which-boxed-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||
dependencies:
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Icon from "./Icon";
|
||||
|
||||
export interface ActionConfirmModalProps {
|
||||
@ -10,6 +11,7 @@ export interface ActionConfirmModalProps {
|
||||
|
||||
const ActionConfirmModal = (props: ActionConfirmModalProps) => {
|
||||
const { close, confirm, title, content, confirmButtonStyle } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="modal modal-middle modal-open">
|
||||
@ -23,7 +25,7 @@ const ActionConfirmModal = (props: ActionConfirmModalProps) => {
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-outline" onClick={close}>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${confirmButtonStyle}`}
|
||||
@ -32,7 +34,7 @@ const ActionConfirmModal = (props: ActionConfirmModalProps) => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
{t("common.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useChatStore, useConnectionStore, useMessageStore, useUserStore } from "@/store";
|
||||
import { CreatorRole } from "@/types";
|
||||
@ -13,6 +14,7 @@ interface Props {
|
||||
|
||||
const MessageTextarea = (props: Props) => {
|
||||
const { disabled, sendMessage } = props;
|
||||
const { t } = useTranslation();
|
||||
const connectionStore = useConnectionStore();
|
||||
const userStore = useUserStore();
|
||||
const chatStore = useChatStore();
|
||||
@ -75,7 +77,7 @@ const MessageTextarea = (props: Props) => {
|
||||
<TextareaAutosize
|
||||
ref={textareaRef}
|
||||
className="w-full h-full outline-none border-none bg-transparent leading-6 py-2 px-2 resize-none hide-scrollbar"
|
||||
placeholder="Type a message..."
|
||||
placeholder={t("editor.placeholder") || ""}
|
||||
rows={1}
|
||||
minRows={1}
|
||||
maxRows={5}
|
||||
|
@ -2,6 +2,7 @@ import { Menu, MenuItem } from "@mui/material";
|
||||
import dayjs from "dayjs";
|
||||
import { ReactElement, useState } from "react";
|
||||
import { ThreeDots } from "react-loader-spinner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "react-hot-toast";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@ -17,6 +18,7 @@ interface Props {
|
||||
|
||||
const MessageView = (props: Props) => {
|
||||
const message = props.message;
|
||||
const { t } = useTranslation();
|
||||
const userStore = useUserStore();
|
||||
const chatStore = useChatStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
@ -124,11 +126,11 @@ const MessageView = (props: Props) => {
|
||||
>
|
||||
<MenuItem onClick={copyMessage}>
|
||||
<Icon.BiClipboard className="w-4 h-auto mr-2 opacity-70" />
|
||||
Copy
|
||||
{t("common.copy")}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => deleteMessage(message)}>
|
||||
<Icon.BiTrash className="w-4 h-auto mr-2 opacity-70" />
|
||||
Delete
|
||||
{t("common.delete")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ClearDataConfirmModal from "./ClearDataConfirmModal";
|
||||
|
||||
const ClearDataButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const [showClearDataConfirmModal, setShowClearDataConfirmModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="btn btn-sm btn-error" onClick={() => setShowClearDataConfirmModal(true)}>
|
||||
Clear
|
||||
{t("common.clear")}
|
||||
</button>
|
||||
|
||||
{showClearDataConfirmModal &&
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Icon from "./Icon";
|
||||
|
||||
interface Props {
|
||||
@ -7,11 +8,12 @@ interface Props {
|
||||
|
||||
const ClearDataConfirmModal = (props: Props) => {
|
||||
const { close } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClearData = () => {
|
||||
window.localStorage.clear();
|
||||
close();
|
||||
toast.success("Message cleared. The page will be reloaded.");
|
||||
toast.success("Data cleared. The page will be reloaded.");
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
@ -29,10 +31,10 @@ const ClearDataConfirmModal = (props: Props) => {
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-outline" onClick={close}>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</button>
|
||||
<button className="btn btn-error" onClick={handleClearData}>
|
||||
Clear data
|
||||
{t("common.clear")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { head } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useChatStore, useConnectionStore, useLayoutStore } from "@/store";
|
||||
import { Chat, Connection } from "@/types";
|
||||
import Icon from "./Icon";
|
||||
import EngineIcon from "./EngineIcon";
|
||||
import LocaleSwitch from "./LocaleSwitch";
|
||||
import CreateConnectionModal from "./CreateConnectionModal";
|
||||
import SettingModal from "./SettingModal";
|
||||
import EditChatTitleModal from "./EditChatTitleModal";
|
||||
@ -16,6 +18,7 @@ interface State {
|
||||
}
|
||||
|
||||
const ConnectionSidebar = () => {
|
||||
const { t } = useTranslation();
|
||||
const layoutStore = useLayoutStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const chatStore = useChatStore();
|
||||
@ -159,16 +162,17 @@ const ConnectionSidebar = () => {
|
||||
))}
|
||||
<button
|
||||
className="tooltip tooltip-right w-10 h-10 mt-4 ml-2 p-2 bg-gray-100 rounded-full text-gray-500 cursor-pointer"
|
||||
data-tip="Create Connection"
|
||||
data-tip={t("connection.new")}
|
||||
onClick={() => toggleCreateConnectionModal(true)}
|
||||
>
|
||||
<Icon.AiOutlinePlus className="w-auto h-full mx-auto" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full flex flex-col justify-end items-center">
|
||||
<LocaleSwitch />
|
||||
<button
|
||||
className="tooltip tooltip-right w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100"
|
||||
data-tip="Setting"
|
||||
data-tip={t("common.setting")}
|
||||
onClick={() => toggleSettingModal(true)}
|
||||
>
|
||||
<Icon.IoMdSettings className="text-gray-600 w-6 h-auto" />
|
||||
@ -180,12 +184,12 @@ const ConnectionSidebar = () => {
|
||||
<div className="w-full grow">
|
||||
{isRequestingDatabase && (
|
||||
<div className="w-full h-12 flex flex-row justify-start items-center px-4 sticky top-0 border z-1 mb-4 mt-2 rounded-lg text-sm text-gray-600">
|
||||
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" /> Loading
|
||||
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" /> {t("common.loading")}
|
||||
</div>
|
||||
)}
|
||||
{databaseList.length > 0 && (
|
||||
<div className="w-full sticky top-0 z-1 mb-4 mt-2">
|
||||
<p className="text-sm leading-6 mb-1 text-gray-500">Select your database:</p>
|
||||
<p className="text-sm leading-6 mb-1 text-gray-500">{t("connection.select-database")}</p>
|
||||
<select
|
||||
className="w-full select select-bordered"
|
||||
value={currentConnectionCtx?.database?.name}
|
||||
@ -236,7 +240,7 @@ const ConnectionSidebar = () => {
|
||||
onClick={handleCreateChat}
|
||||
>
|
||||
<Icon.AiOutlinePlus className="w-5 h-auto mr-1" />
|
||||
New Chat
|
||||
{t("chat.new")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="sticky bottom-0 w-full flex justify-center bg-gray-100 backdrop-blur bg-opacity-60 pb-6 py-2">
|
||||
@ -246,7 +250,7 @@ const ConnectionSidebar = () => {
|
||||
target="_blank"
|
||||
>
|
||||
<Icon.BsDiscord className="w-4 h-auto mr-1" />
|
||||
Join Discord Channel
|
||||
{t("social.join-discord-channel")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import Icon from "./Icon";
|
||||
|
||||
@ -8,6 +9,7 @@ interface Props {
|
||||
|
||||
const DataStorageBanner = (props: Props) => {
|
||||
const { className, alwaysShow } = props;
|
||||
const { t } = useTranslation();
|
||||
const [hideBanner, setHideBanner] = useLocalStorage("hide-local-storage-banner", false);
|
||||
const show = alwaysShow || !hideBanner;
|
||||
|
||||
@ -19,7 +21,7 @@ const DataStorageBanner = (props: Props) => {
|
||||
>
|
||||
<span className="text-sm leading-6 pr-4">
|
||||
<Icon.IoInformationCircleOutline className="inline-block h-5 w-auto -mt-0.5 mr-0.5 opacity-80" />
|
||||
Connection settings are stored in your local browser
|
||||
{t("banner.data-storage")}
|
||||
</span>
|
||||
{!alwaysShow && (
|
||||
<button className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100" onClick={() => setHideBanner(true)}>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useChatStore } from "@/store";
|
||||
import { Chat } from "@/types";
|
||||
import Icon from "./Icon";
|
||||
@ -11,6 +12,7 @@ interface Props {
|
||||
|
||||
const EditMessageTitleModal = (props: Props) => {
|
||||
const { close, chat } = props;
|
||||
const { t } = useTranslation();
|
||||
const chatStore = useChatStore();
|
||||
const [title, setTitle] = useState(chat.title);
|
||||
const allowSave = title !== "";
|
||||
@ -31,14 +33,14 @@ const EditMessageTitleModal = (props: Props) => {
|
||||
return (
|
||||
<div className="modal modal-middle modal-open">
|
||||
<div className="modal-box relative">
|
||||
<h3 className="font-bold text-lg">Edit chat title</h3>
|
||||
<h3 className="font-bold text-lg">{t("chat.edit-title")}</h3>
|
||||
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
|
||||
<Icon.IoMdClose className="w-5 h-auto" />
|
||||
</button>
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Chat title"
|
||||
placeholder={t("chat.chat-title") || ""}
|
||||
className="input input-bordered w-full"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
@ -46,10 +48,10 @@ const EditMessageTitleModal = (props: Props) => {
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-outline" onClick={close}>
|
||||
Close
|
||||
{t("common.close")}
|
||||
</button>
|
||||
<button className="btn" disabled={!allowSave} onClick={handleSaveEdit}>
|
||||
Save
|
||||
{t("common.save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
19
src/components/LocaleSelector.tsx
Normal file
19
src/components/LocaleSelector.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { useSettingStore } from "@/store";
|
||||
|
||||
const LocaleSelector = () => {
|
||||
const settingStore = useSettingStore();
|
||||
const locale = settingStore.setting.locale;
|
||||
|
||||
const handleLocaleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
settingStore.setLocale(event.target.value as any);
|
||||
};
|
||||
|
||||
return (
|
||||
<select className="select select-bordered !h-auto !min-h-fit" value={locale} onChange={handleLocaleChange}>
|
||||
<option value="en">English</option>
|
||||
<option value="zh">简体中文</option>
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocaleSelector;
|
23
src/components/LocaleSwitch.tsx
Normal file
23
src/components/LocaleSwitch.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { useSettingStore } from "@/store";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const LocaleSwitch = () => {
|
||||
const settingStore = useSettingStore();
|
||||
const locale = settingStore.setting.locale;
|
||||
|
||||
const handleLocaleChange = () => {
|
||||
if (locale === "en") {
|
||||
settingStore.setLocale("zh");
|
||||
} else {
|
||||
settingStore.setLocale("en");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100" onClick={handleLocaleChange}>
|
||||
<Icon.IoLanguage className="text-gray-600 w-6 h-auto" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocaleSwitch;
|
@ -3,6 +3,7 @@ import { head } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import DataTable from "react-data-table-component";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useQueryStore } from "@/store";
|
||||
import { ResponseObject } from "@/types";
|
||||
@ -14,6 +15,7 @@ type RawQueryResult = {
|
||||
};
|
||||
|
||||
const QueryDrawer = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryStore = useQueryStore();
|
||||
const [rawResults, setRawResults] = useState<RawQueryResult[]>([]);
|
||||
const context = queryStore.context;
|
||||
@ -86,16 +88,16 @@ const QueryDrawer = () => {
|
||||
<button className="btn btn-sm btn-circle" onClick={close}>
|
||||
<Icon.IoMdClose className="w-5 h-auto" />
|
||||
</button>
|
||||
<h3 className="font-bold text-2xl mt-4">Execute query</h3>
|
||||
<h3 className="font-bold text-2xl mt-4">{t("execution.title")}</h3>
|
||||
{!context ? (
|
||||
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
|
||||
<Icon.BiSad className="w-7 h-auto opacity-70" />
|
||||
<span className="text-sm font-mono text-gray-500 mt-2">No connection selected.</span>
|
||||
<span className="text-sm font-mono text-gray-500 mt-2">{t("execution.message.no-connection")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-full flex flex-row justify-start items-center mt-4">
|
||||
<span className="opacity-70">Connection: </span>
|
||||
<span className="opacity-70">{t("connection.self")}: </span>
|
||||
<EngineIcon className="w-6 h-auto" engine={context.connection.engineType} />
|
||||
<span>{context.database?.name}</span>
|
||||
</div>
|
||||
@ -120,12 +122,12 @@ const QueryDrawer = () => {
|
||||
{isLoading ? (
|
||||
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
|
||||
<Icon.BiLoaderAlt className="w-7 h-auto opacity-70 animate-spin" />
|
||||
<span className="text-sm font-mono text-gray-500 mt-2">Executing</span>
|
||||
<span className="text-sm font-mono text-gray-500 mt-2">{t("execution.message.executing")}</span>
|
||||
</div>
|
||||
) : rawResults.length === 0 ? (
|
||||
<div className="w-full flex flex-col justify-center items-center py-6 pt-10">
|
||||
<Icon.BsBox2 className="w-7 h-auto opacity-70" />
|
||||
<span className="text-sm font-mono text-gray-500 mt-2">No data return.</span>
|
||||
<span className="text-sm font-mono text-gray-500 mt-2">{t("execution.message.no-data")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Icon from "./Icon";
|
||||
import WeChatQRCodeView from "./WeChatQRCodeView";
|
||||
import ClearDataButton from "./ClearDataButton";
|
||||
import LocaleSelector from "./LocaleSelector";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
@ -9,6 +11,7 @@ interface Props {
|
||||
|
||||
const SettingModal = (props: Props) => {
|
||||
const { show, close } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`modal modal-middle ${show && "modal-open"}`}>
|
||||
@ -25,11 +28,18 @@ const SettingModal = (props: Props) => {
|
||||
target="_blank"
|
||||
>
|
||||
<Icon.BsDiscord className="w-4 h-auto mr-1" />
|
||||
Join Discord Channel
|
||||
{t("social.join-discord-channel")}
|
||||
</a>
|
||||
<WeChatQRCodeView />
|
||||
</div>
|
||||
|
||||
<div className="w-full border border-gray-200 p-4 rounded-lg">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>Language</span>
|
||||
<LocaleSelector />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Danger Zone</h3>
|
||||
<div className="w-full border border-red-200 p-4 rounded-lg">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Popover } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Icon from "./Icon";
|
||||
|
||||
const WeChatQRCodeView = () => {
|
||||
const { t } = useTranslation();
|
||||
const [wechatAnchorEl, setWeChatAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const openWeChatQrCodePopover = Boolean(wechatAnchorEl);
|
||||
|
||||
@ -13,7 +15,7 @@ const WeChatQRCodeView = () => {
|
||||
onClick={(e) => setWeChatAnchorEl(e.currentTarget)}
|
||||
>
|
||||
<Icon.BsWechat className="w-4 h-auto mr-1" />
|
||||
Join WeChat Group
|
||||
{t("social.join-wechat-group")}
|
||||
</div>
|
||||
<Popover
|
||||
open={openWeChatQrCodePopover}
|
||||
|
40
src/locales/en.json
Normal file
40
src/locales/en.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"common": {
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"save": "Save",
|
||||
"loading": "Loading",
|
||||
"setting": "Setting",
|
||||
"copy": "Copy",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"chat": {
|
||||
"new": "New Chat",
|
||||
"chat-title": "Chat title",
|
||||
"edit-title": "Edit chat title"
|
||||
},
|
||||
"connection": {
|
||||
"self": "Connection",
|
||||
"new": "Create Connection",
|
||||
"select-database": "Select your database:"
|
||||
},
|
||||
"execution": {
|
||||
"title": "Execute query",
|
||||
"message": {
|
||||
"executing": "Executing query...",
|
||||
"no-connection": "No connection selected",
|
||||
"no-data": "No data returned"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"placeholder": "Enter your question here..."
|
||||
},
|
||||
"social": {
|
||||
"join-discord-channel": "Join Discord Channel",
|
||||
"join-wechat-group": "Join WeChat Group"
|
||||
},
|
||||
"banner": {
|
||||
"data-storage": "Connection settings are stored in your local browser"
|
||||
}
|
||||
}
|
18
src/locales/i18n.ts
Normal file
18
src/locales/i18n.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import enLocale from "./en.json";
|
||||
import zhLocale from "./zh.json";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enLocale,
|
||||
},
|
||||
zh: {
|
||||
translation: zhLocale,
|
||||
},
|
||||
},
|
||||
fallbackLng: "en",
|
||||
});
|
||||
|
||||
export default i18n;
|
40
src/locales/zh.json
Normal file
40
src/locales/zh.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"common": {
|
||||
"clear": "清除",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"save": "保存",
|
||||
"loading": "加载中",
|
||||
"setting": "设置",
|
||||
"copy": "复制",
|
||||
"delete": "删除"
|
||||
},
|
||||
"chat": {
|
||||
"new": "新建会话",
|
||||
"chat-title": "会话标题",
|
||||
"edit-title": "编辑会话标题"
|
||||
},
|
||||
"connection": {
|
||||
"self": "连接",
|
||||
"new": "创建连接",
|
||||
"select-database": "选择您的数据库:"
|
||||
},
|
||||
"execution": {
|
||||
"title": "执行查询",
|
||||
"message": {
|
||||
"executing": "正在执行查询...",
|
||||
"no-connection": "未选择连接",
|
||||
"no-data": "未返回数据"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"placeholder": "在此输入您的问题..."
|
||||
},
|
||||
"social": {
|
||||
"join-discord-channel": "加入 Discord 频道",
|
||||
"join-wechat-group": "加入微信群"
|
||||
},
|
||||
"banner": {
|
||||
"data-storage": "连接设置存储在您的本地浏览器中"
|
||||
}
|
||||
}
|
@ -1,16 +1,26 @@
|
||||
import { AppProps } from "next/app";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { AppProps } from "next/app";
|
||||
import React, { useEffect } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
dayjs.extend(localizedFormat);
|
||||
import { useSettingStore } from "@/store";
|
||||
|
||||
import "@/locales/i18n";
|
||||
import "@/styles/tailwind.css";
|
||||
import "@/styles/global.css";
|
||||
import "@/styles/data-table.css";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(settingStore.setting.locale);
|
||||
}, [settingStore.setting.locale]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component {...pageProps} />
|
||||
|
@ -17,7 +17,7 @@ const QueryDrawer = dynamic(() => import("@/components/QueryDrawer"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ChatPage: NextPage = () => {
|
||||
const IndexPage: NextPage = () => {
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
useEffect(() => {
|
||||
@ -69,4 +69,4 @@ const ChatPage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPage;
|
||||
export default IndexPage;
|
||||
|
@ -5,3 +5,4 @@ export * from "./chat";
|
||||
export * from "./message";
|
||||
export * from "./layout";
|
||||
export * from "./query";
|
||||
export * from "./setting";
|
||||
|
34
src/store/setting.ts
Normal file
34
src/store/setting.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { Setting } from "@/types";
|
||||
|
||||
const getDefaultSetting = (): Setting => {
|
||||
return {
|
||||
locale: "en",
|
||||
};
|
||||
};
|
||||
|
||||
interface SettingState {
|
||||
setting: Setting;
|
||||
setLocale: (locale: Setting["locale"]) => void;
|
||||
}
|
||||
|
||||
export const useSettingStore = create<SettingState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
getState: () => get(),
|
||||
setting: getDefaultSetting(),
|
||||
setLocale: (locale: Setting["locale"]) => {
|
||||
set({
|
||||
setting: {
|
||||
...get().setting,
|
||||
locale,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "setting-storage",
|
||||
}
|
||||
)
|
||||
);
|
@ -4,4 +4,5 @@ export * from "./connection";
|
||||
export * from "./database";
|
||||
export * from "./chat";
|
||||
export * from "./message";
|
||||
export * from "./setting";
|
||||
export * from "./api";
|
||||
|
5
src/types/setting.ts
Normal file
5
src/types/setting.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type Locale = "en" | "zh";
|
||||
|
||||
export interface Setting {
|
||||
locale: Locale;
|
||||
}
|
Reference in New Issue
Block a user