feat: add i18n deps (#18)

This commit is contained in:
boojack
2023-04-06 10:59:45 +08:00
committed by GitHub
parent 34a72a7d56
commit 4742f8b83c
24 changed files with 300 additions and 32 deletions

View File

@ -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
View File

@ -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:

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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 &&

View File

@ -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>

View File

@ -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>

View File

@ -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)}>

View File

@ -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>

View 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;

View 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;

View File

@ -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">

View File

@ -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">

View File

@ -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
View 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
View 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
View 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": "连接设置存储在您的本地浏览器中"
}
}

View File

@ -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} />

View File

@ -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;

View File

@ -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
View 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",
}
)
);

View File

@ -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
View File

@ -0,0 +1,5 @@
export type Locale = "en" | "zh";
export interface Setting {
locale: Locale;
}