diff --git a/package.json b/package.json index 3035dab..200016c 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ "axios": "^1.7.2", "echarts": "^5.5.0", "echarts-for-react": "^3.0.2", + "i18next": "^23.11.5", "js-md5": "^0.8.3", "mobx": "^6.12.4", "mobx-react": "^9.1.1", "qrcode": "^1.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^14.1.2", "react-icons": "^5.2.1", "react-router-dom": "^6.23.1", "react-scripts": "5.0.1", @@ -56,4 +58,4 @@ "devDependencies": { "@types/qrcode": "^1.5.5" } -} \ No newline at end of file +} diff --git a/src/assets/en.jpg b/src/assets/en.jpg new file mode 100644 index 0000000..405cd07 Binary files /dev/null and b/src/assets/en.jpg differ diff --git a/src/assets/zh.jpg b/src/assets/zh.jpg new file mode 100644 index 0000000..75c02f6 Binary files /dev/null and b/src/assets/zh.jpg differ diff --git a/src/components/CountdownTimer.tsx b/src/components/CountdownTimer.tsx index bf95ab0..29a9d1d 100644 --- a/src/components/CountdownTimer.tsx +++ b/src/components/CountdownTimer.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { useTranslation } from 'react-i18next'; interface CountdownTimerProps { initialSeconds: number, @@ -15,10 +16,10 @@ export interface CountdownTimerRef { const CountdownTimer = forwardRef((props, ref) => { const { initialSeconds, onComplete } = props - + const { t } = useTranslation() const [seconds, setSeconds] = useState(initialSeconds); const [isActive, setIsActive] = useState(false); - const [title, setTitle] = useState('发送验证码') + const [title, setTitle] = useState(t('Send verification code')) useImperativeHandle(ref, () => ({ handleStop, @@ -29,7 +30,7 @@ const CountdownTimer = forwardRef((props const handleStop = () => { setIsActive(false); - setTitle('发送验证码') + setTitle(t('Send verification code')) }; const handleStart = () => { @@ -54,11 +55,10 @@ const CountdownTimer = forwardRef((props }, 1000); } else { clearInterval(interval) - seconds <= 0 && setTitle('重新发送') + seconds <= 0 && setTitle(t('Resend')) setIsActive(false) // onComplete && onComplete(seconds) } - console.log(1); return () => clearInterval(interval); }, [isActive, seconds, onComplete]); diff --git a/src/components/LanguageMenu.tsx b/src/components/LanguageMenu.tsx new file mode 100644 index 0000000..0a84d1a --- /dev/null +++ b/src/components/LanguageMenu.tsx @@ -0,0 +1,69 @@ +import { useState } from "react" +import { TRADER_LANGUAGE } from "../language" +import { ConfigProvider, Popover } from "antd" +import { useTranslation } from "react-i18next" + +interface LanguageMenuProps { + width?: number +} + +const LanguageMenu = (props: LanguageMenuProps) => { + + const { width = 50 } = props + const [open, setOpen] = useState(false); + const { i18n } = useTranslation() + + const [lang, setLang] = useState(window.localStorage.getItem(TRADER_LANGUAGE) || 'zh') + const [lns, setLns] = useState([ + { + key: 'zh', + title: '中文简体' + }, + { + key: 'en', + title: 'English' + } + ]) + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + }; + + const changeLanguage = (key: string) => { + setLang(key) + window.localStorage.setItem(TRADER_LANGUAGE, key) + i18n.changeLanguage(key) + handleOpenChange(false) + } + + return ( +
+ + + { + lns.map((item, index) => ( +
changeLanguage(item.key)}>{item.title}
+ )) + } +
} + trigger="click" + > + + + + + ) +} + +export default LanguageMenu \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 5f4fa73..cfeb1fc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,7 @@ import App from './App'; import reportWebVitals from './reportWebVitals'; import './styles/global.scss' import { ConfigProvider } from 'antd'; +import './language' const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement diff --git a/src/language/index.ts b/src/language/index.ts new file mode 100644 index 0000000..b73b4f2 --- /dev/null +++ b/src/language/index.ts @@ -0,0 +1,21 @@ +import zh from "./zh.json"; +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +export const TRADER_LANGUAGE = "TRADER_LANGUAGE"; + +const resources = { + zh: { + translation: { ...zh }, + }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: window.sessionStorage.getItem(TRADER_LANGUAGE) || "zh", + interpolation: { + escapeValue: false, + }, +}); + +export default i18n; \ No newline at end of file diff --git a/src/language/zh.json b/src/language/zh.json new file mode 100644 index 0000000..fb1822d --- /dev/null +++ b/src/language/zh.json @@ -0,0 +1,24 @@ +{ + "account": "账号", + "password": "密码", + "Verification Code": "验证码", + "Sign in": "登录", + "Forget password?": "忘记密码?", + "Create a proxy account": "创建代理账户", + "Invalid account": "无效的账号", + "login successful": "登录成功", + "Password sent to email": "密码发送至邮箱", + "Retrieve immediately": "立即找回", + "Already have an account? Sign in": "已有账号,登录", + "name": "姓名", + "invitation code": "邀请码", + "email": "邮箱", + "invalid email": "无效的邮箱", + "Login password, (letters + numbers) length 8-32": "登录密码,(字母+数字)长度8-32", + "Register now": "立即注册", + "Send verification code": "发送验证码", + "Resend": "重新发送", + "Register Successful": "注册成功", + "Verification code sent successfully": "验证码发送成功", + "Failed to send verification code": "验证码发送失败" +} \ No newline at end of file diff --git a/src/pages/create-proxy/index.tsx b/src/pages/create-proxy/index.tsx index 6d3d768..8a50d8a 100644 --- a/src/pages/create-proxy/index.tsx +++ b/src/pages/create-proxy/index.tsx @@ -5,6 +5,7 @@ import { useRouter } from '../../hooks/useRouter' import { http_register, http_send_email } from '../../http/api'; import CountdownTimer, { CountdownTimerRef } from '../../components/CountdownTimer'; import { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; interface FieldType { name: string; @@ -19,14 +20,14 @@ const CreateProxy = () => { const { push, location } = useRouter() const [form] = Form.useForm(); const countdownRef = useRef(null) - console.log(location.search); + const { t } = useTranslation() const onFinish: FormProps['onFinish'] = async (values) => { try { const res: any = await http_register(values) if (res.code === 0) { notification.success({ - message: '注册成功' + message: t('Register Successful') }) push('/login', null, true) } @@ -37,11 +38,11 @@ const CreateProxy = () => { const validatePassword = (_: any, value: string) => { if (!value) { - return Promise.reject(new Error("请输入密码")); + return Promise.reject(new Error(t('password'))); } const regex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,32}$/; if (!regex.test(value)) { - return Promise.reject(new Error("密码必须包含字母和数字,长度为8-32位")); + return Promise.reject(new Error(t('Login password, (letters + numbers) length 8-32'))); } return Promise.resolve(); }; @@ -57,11 +58,11 @@ const CreateProxy = () => { }) if (res.code === 0) { notification.success({ - message: '验证码发送成功' + message: t('Verification code sent successfully') }) } else { notification.error({ - message: '验证码发送失败' + message: t('Failed to send verification code') }) countdownRef.current?.handleStop() } @@ -86,30 +87,30 @@ const CreateProxy = () => {
-
创建代理账户
+
{t('Create a proxy account')}
- - + + - - + + - - + + + }} placeholder={t('Login password, (letters + numbers) length 8-32')} /> - +
- + @@ -117,11 +118,11 @@ const CreateProxy = () => { - + - +
diff --git a/src/pages/forget/index.tsx b/src/pages/forget/index.tsx index 0e3aae5..d5b67d4 100644 --- a/src/pages/forget/index.tsx +++ b/src/pages/forget/index.tsx @@ -4,6 +4,7 @@ import Button from '../../components/Button' import { useRouter } from '../../hooks/useRouter' import { useEffect, useState } from 'react'; import { http_code, http_forget } from '../../http/api'; +import { useTranslation } from 'react-i18next'; interface FieldType { email: string; @@ -16,6 +17,7 @@ const ForGet = () => { const [form] = Form.useForm() const [codeId, setCodeId] = useState('') const [codeUrl, setCodeUrl] = useState('') + const { t } = useTranslation() const onFinish: FormProps['onFinish'] = async (values) => { const res: any = await http_forget({ @@ -26,7 +28,7 @@ const ForGet = () => { if (res.code === 0) { form.resetFields() notification.success({ - message: '密码已发送至您的邮箱!' + message: t('Password sent to email') }) } }; @@ -52,20 +54,20 @@ const ForGet = () => {
- - + + - +
- +
- + -
push(-1)} className='tac tp'>已有账号,登录
+
push(-1)} className='tac tp'>{t('Already have an account? Sign in')}
diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 1a25092..2892241 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -5,6 +5,7 @@ import { useRouter } from '../../hooks/useRouter' import store from '../../store' import { useEffect, useState } from 'react' import { http_code, http_login } from '../../http/api' +import { useTranslation } from 'react-i18next' type FieldType = { email: string; password: string; @@ -16,6 +17,7 @@ const Login = () => { const { push } = useRouter() const [codeId, setCodeId] = useState('') const [codeUrl, setCodeUrl] = useState('') + const { t } = useTranslation() const onFinish: FormProps['onFinish'] = async (values) => { const res: any = await http_login({ @@ -24,7 +26,7 @@ const Login = () => { }) if (res.code === 0) { notification.success({ - message: '登录成功' + message: t('login successful') }) store.setToken(res.data.token) push('/', null, true) @@ -42,10 +44,6 @@ const Login = () => { } } - const login = async () => { - - } - useEffect(() => { getCode() }, []) @@ -66,49 +64,49 @@ const Login = () => { rules={[ { type: 'email', - message: '请输入有效的邮箱!', + message: t('Invalid account'), }, { required: true, - message: '请输入邮箱!', + message: t('account'), }, ]} > - + - +
- +
- + -
push('/forget')}>忘记密码?
+
push('/forget')}>{t('Forget password?')}
- +
diff --git a/src/router/index.tsx b/src/router/index.tsx index d00d15a..7ee8867 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -10,6 +10,7 @@ import { unLoginPath } from "./routes"; import { Divider, Modal } from "antd"; import { NotifyStatus_Type } from "../types"; import { http_notify } from "../http/api"; +import LanguageMenu from "../components/LanguageMenu"; const LayoutRouter = () => { @@ -75,7 +76,12 @@ const LayoutRouter = () => { <> { !token ? ( - + <> + {/*
+ +
*/} + + ) : (
diff --git a/yarn.lock b/yarn.lock index 945be30..d95772f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5445,6 +5445,13 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html-webpack-plugin@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" @@ -5539,6 +5546,13 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18next@^23.11.5: + version "23.11.5" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.5.tgz#d71eb717a7e65498d87d0594f2664237f9e361ef" + integrity sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -8633,6 +8647,14 @@ react-error-overlay@^6.0.11: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-i18next@^14.1.2: + version "14.1.2" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.2.tgz#cd57a755f25a32a5fcc3dbe546cf3cc62b4f3ebd" + integrity sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg== + dependencies: + "@babel/runtime" "^7.23.9" + html-parse-stringify "^3.0.1" + react-icons@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.2.1.tgz#28c2040917b2a2eda639b0f797bff1888e018e4a" @@ -10159,6 +10181,11 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"