From 4742f8b83cfa3ec9027fc4763701a21388a6c241 Mon Sep 17 00:00:00 2001 From: boojack Date: Thu, 6 Apr 2023 10:59:45 +0800 Subject: [PATCH] feat: add i18n deps (#18) --- package.json | 2 + pnpm-lock.yaml | 43 +++++++++++++++++++++ src/components/ActionConfirmModal.tsx | 6 ++- src/components/ChatView/MessageTextarea.tsx | 4 +- src/components/ChatView/MessageView.tsx | 6 ++- src/components/ClearDataButton.tsx | 4 +- src/components/ClearDataConfirmModal.tsx | 8 ++-- src/components/ConnectionSidebar.tsx | 16 +++++--- src/components/DataStorageBanner.tsx | 4 +- src/components/EditChatTitleModal.tsx | 10 +++-- src/components/LocaleSelector.tsx | 19 +++++++++ src/components/LocaleSwitch.tsx | 23 +++++++++++ src/components/QueryDrawer.tsx | 12 +++--- src/components/SettingModal.tsx | 12 +++++- src/components/WeChatQRCodeView.tsx | 4 +- src/locales/en.json | 40 +++++++++++++++++++ src/locales/i18n.ts | 18 +++++++++ src/locales/zh.json | 40 +++++++++++++++++++ src/pages/_app.tsx | 16 ++++++-- src/pages/index.tsx | 4 +- src/store/index.ts | 1 + src/store/setting.ts | 34 ++++++++++++++++ src/types/index.ts | 1 + src/types/setting.ts | 5 +++ 24 files changed, 300 insertions(+), 32 deletions(-) create mode 100644 src/components/LocaleSelector.tsx create mode 100644 src/components/LocaleSwitch.tsx create mode 100644 src/locales/en.json create mode 100644 src/locales/i18n.ts create mode 100644 src/locales/zh.json create mode 100644 src/store/setting.ts create mode 100644 src/types/setting.ts diff --git a/package.json b/package.json index cf0c04b..0a50fc5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f08015..10e0054 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/ActionConfirmModal.tsx b/src/components/ActionConfirmModal.tsx index 5ab9d83..b6d1e1e 100644 --- a/src/components/ActionConfirmModal.tsx +++ b/src/components/ActionConfirmModal.tsx @@ -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 (
@@ -23,7 +25,7 @@ const ActionConfirmModal = (props: ActionConfirmModalProps) => {
diff --git a/src/components/ChatView/MessageTextarea.tsx b/src/components/ChatView/MessageTextarea.tsx index c82bd8c..29563fd 100644 --- a/src/components/ChatView/MessageTextarea.tsx +++ b/src/components/ChatView/MessageTextarea.tsx @@ -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) => { { const message = props.message; + const { t } = useTranslation(); const userStore = useUserStore(); const chatStore = useChatStore(); const connectionStore = useConnectionStore(); @@ -124,11 +126,11 @@ const MessageView = (props: Props) => { > - Copy + {t("common.copy")} deleteMessage(message)}> - Delete + {t("common.delete")} diff --git a/src/components/ClearDataButton.tsx b/src/components/ClearDataButton.tsx index a062653..d81d3b7 100644 --- a/src/components/ClearDataButton.tsx +++ b/src/components/ClearDataButton.tsx @@ -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 ( <> {showClearDataConfirmModal && diff --git a/src/components/ClearDataConfirmModal.tsx b/src/components/ClearDataConfirmModal.tsx index e034a0b..c885f6f 100644 --- a/src/components/ClearDataConfirmModal.tsx +++ b/src/components/ClearDataConfirmModal.tsx @@ -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) => {
diff --git a/src/components/ConnectionSidebar.tsx b/src/components/ConnectionSidebar.tsx index ad6bb2b..fce71eb 100644 --- a/src/components/ConnectionSidebar.tsx +++ b/src/components/ConnectionSidebar.tsx @@ -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 = () => { ))}
+
diff --git a/src/components/LocaleSelector.tsx b/src/components/LocaleSelector.tsx new file mode 100644 index 0000000..5b976cb --- /dev/null +++ b/src/components/LocaleSelector.tsx @@ -0,0 +1,19 @@ +import { useSettingStore } from "@/store"; + +const LocaleSelector = () => { + const settingStore = useSettingStore(); + const locale = settingStore.setting.locale; + + const handleLocaleChange = (event: React.ChangeEvent) => { + settingStore.setLocale(event.target.value as any); + }; + + return ( + + ); +}; + +export default LocaleSelector; diff --git a/src/components/LocaleSwitch.tsx b/src/components/LocaleSwitch.tsx new file mode 100644 index 0000000..eee2458 --- /dev/null +++ b/src/components/LocaleSwitch.tsx @@ -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 ( + + ); +}; + +export default LocaleSwitch; diff --git a/src/components/QueryDrawer.tsx b/src/components/QueryDrawer.tsx index 2edbbb4..d28b4b0 100644 --- a/src/components/QueryDrawer.tsx +++ b/src/components/QueryDrawer.tsx @@ -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([]); const context = queryStore.context; @@ -86,16 +88,16 @@ const QueryDrawer = () => { -

Execute query

+

{t("execution.title")}

{!context ? (
- No connection selected. + {t("execution.message.no-connection")}
) : ( <>
- Connection: + {t("connection.self")}: {context.database?.name}
@@ -120,12 +122,12 @@ const QueryDrawer = () => { {isLoading ? (
- Executing + {t("execution.message.executing")}
) : rawResults.length === 0 ? (
- No data return. + {t("execution.message.no-data")}
) : (
diff --git a/src/components/SettingModal.tsx b/src/components/SettingModal.tsx index 01925e2..d24b42f 100644 --- a/src/components/SettingModal.tsx +++ b/src/components/SettingModal.tsx @@ -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 (
@@ -25,11 +28,18 @@ const SettingModal = (props: Props) => { target="_blank" > - Join Discord Channel + {t("social.join-discord-channel")}
+
+
+ Language + +
+
+

Danger Zone

diff --git a/src/components/WeChatQRCodeView.tsx b/src/components/WeChatQRCodeView.tsx index da91030..c45af22 100644 --- a/src/components/WeChatQRCodeView.tsx +++ b/src/components/WeChatQRCodeView.tsx @@ -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(null); const openWeChatQrCodePopover = Boolean(wechatAnchorEl); @@ -13,7 +15,7 @@ const WeChatQRCodeView = () => { onClick={(e) => setWeChatAnchorEl(e.currentTarget)} > - Join WeChat Group + {t("social.join-wechat-group")}
{ + i18n.changeLanguage(settingStore.setting.locale); + }, [settingStore.setting.locale]); + return ( <> diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 69707b4..af2427f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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; diff --git a/src/store/index.ts b/src/store/index.ts index e3cfd45..cbd4eab 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -5,3 +5,4 @@ export * from "./chat"; export * from "./message"; export * from "./layout"; export * from "./query"; +export * from "./setting"; diff --git a/src/store/setting.ts b/src/store/setting.ts new file mode 100644 index 0000000..88c5f5e --- /dev/null +++ b/src/store/setting.ts @@ -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()( + persist( + (set, get) => ({ + getState: () => get(), + setting: getDefaultSetting(), + setLocale: (locale: Setting["locale"]) => { + set({ + setting: { + ...get().setting, + locale, + }, + }); + }, + }), + { + name: "setting-storage", + } + ) +); diff --git a/src/types/index.ts b/src/types/index.ts index 99fcea9..69d3454 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,4 +4,5 @@ export * from "./connection"; export * from "./database"; export * from "./chat"; export * from "./message"; +export * from "./setting"; export * from "./api"; diff --git a/src/types/setting.ts b/src/types/setting.ts new file mode 100644 index 0000000..e65ce64 --- /dev/null +++ b/src/types/setting.ts @@ -0,0 +1,5 @@ +export type Locale = "en" | "zh"; + +export interface Setting { + locale: Locale; +}