From bdf312e5dc99622efd320112ebf0f610a34db27f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 12:15:38 +0800 Subject: [PATCH] feat: initial i18n support --- web/default/package.json | 4 ++ .../public/locales/en/translation.json | 38 +++++++++++ .../public/locales/zh/translation.json | 38 +++++++++++ web/default/src/components/Header.js | 68 ++++++++++++++----- web/default/src/i18n.js | 23 +++++++ web/default/src/index.js | 1 + web/default/src/pages/TopUp/index.js | 33 +++++---- 7 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 web/default/public/locales/en/translation.json create mode 100644 web/default/public/locales/zh/translation.json create mode 100644 web/default/src/i18n.js diff --git a/web/default/package.json b/web/default/package.json index d03288ed..15e48f48 100644 --- a/web/default/package.json +++ b/web/default/package.json @@ -5,10 +5,14 @@ "dependencies": { "axios": "^0.27.2", "history": "^5.3.0", + "i18next": "^24.2.2", + "i18next-browser-languagedetector": "^8.0.2", + "i18next-http-backend": "^3.0.2", "marked": "^4.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-i18next": "^15.4.0", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-toastify": "^9.0.8", diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json new file mode 100644 index 00000000..fc54a52e --- /dev/null +++ b/web/default/public/locales/en/translation.json @@ -0,0 +1,38 @@ +{ + "header": { + "home": "Home", + "channel": "Channel", + "token": "Token", + "redemption": "Redemption", + "topup": "Top Up", + "user": "User", + "dashboard": "Dashboard", + "log": "Log", + "setting": "Settings", + "about": "About", + "chat": "Chat", + "login": "Login", + "logout": "Logout", + "register": "Register" + }, + "topup": { + "title": "Top Up Center", + "get_code": { + "title": "Get Redemption Code", + "current_quota": "Current Available Quota", + "button": "Get Code Now" + }, + "redeem_code": { + "title": "Redeem Code", + "placeholder": "Please enter redemption code", + "paste": "Paste", + "paste_error": "Cannot access clipboard, please paste manually", + "submit": "Redeem Now", + "submitting": "Redeeming...", + "empty_code": "Please enter the redemption code!", + "success": "Top up successful!", + "request_failed": "Request failed", + "no_link": "Admin has not set up the top-up link!" + } + } +} diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json new file mode 100644 index 00000000..51449786 --- /dev/null +++ b/web/default/public/locales/zh/translation.json @@ -0,0 +1,38 @@ +{ + "header": { + "home": "首页", + "channel": "渠道", + "token": "令牌", + "redemption": "兑换", + "topup": "充值", + "user": "用户", + "dashboard": "总览", + "log": "日志", + "setting": "设置", + "about": "关于", + "chat": "聊天", + "login": "登录", + "logout": "注销", + "register": "注册" + }, + "topup": { + "title": "充值中心", + "get_code": { + "title": "获取兑换码", + "current_quota": "当前可用额度", + "button": "立即获取兑换码" + }, + "redeem_code": { + "title": "兑换码充值", + "placeholder": "请输入兑换码", + "paste": "粘贴", + "paste_error": "无法访问剪贴板,请手动粘贴", + "submit": "立即兑换", + "submitting": "兑换中...", + "empty_code": "请输入兑换码!", + "success": "充值成功!", + "request_failed": "请求失败", + "no_link": "超级管理员未设置充值链接!" + } + } +} diff --git a/web/default/src/components/Header.js b/web/default/src/components/Header.js index dd2a5fdd..52d67042 100644 --- a/web/default/src/components/Header.js +++ b/web/default/src/components/Header.js @@ -1,6 +1,7 @@ import React, { useContext, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { UserContext } from '../context/User'; +import { useTranslation } from 'react-i18next'; import { Button, @@ -23,55 +24,55 @@ import '../index.css'; // Header Buttons let headerButtons = [ { - name: '首页', + name: 'header.home', to: '/', icon: 'home', }, { - name: '渠道', + name: 'header.channel', to: '/channel', icon: 'sitemap', admin: true, }, { - name: '令牌', + name: 'header.token', to: '/token', icon: 'key', }, { - name: '兑换', + name: 'header.redemption', to: '/redemption', icon: 'dollar sign', admin: true, }, { - name: '充值', + name: 'header.topup', to: '/topup', icon: 'cart', }, { - name: '用户', + name: 'header.user', to: '/user', icon: 'user', admin: true, }, { - name: '总览', + name: 'header.dashboard', to: '/dashboard', icon: 'chart bar', }, { - name: '日志', + name: 'header.log', to: '/log', icon: 'book', }, { - name: '设置', + name: 'header.setting', to: '/setting', icon: 'setting', }, { - name: '关于', + name: 'header.about', to: '/about', icon: 'info circle', }, @@ -79,13 +80,14 @@ let headerButtons = [ if (localStorage.getItem('chat_link')) { headerButtons.splice(1, 0, { - name: '聊天', + name: 'header.chat', to: '/chat', icon: 'comments', }); } const Header = () => { + const { t, i18n } = useTranslation(); const [userState, userDispatch] = useContext(UserContext); let navigate = useNavigate(); @@ -112,13 +114,14 @@ const Header = () => { if (isMobile) { return ( { navigate(button.to); setShowSidebar(false); }} style={{ fontSize: '15px' }} > - {button.name} + {t(button.name)} ); } @@ -134,12 +137,22 @@ const Header = () => { }} > - {button.name} + {t(button.name)} ); }); }; + // Add language switcher dropdown + const languageOptions = [ + { key: 'zh', text: '中文', value: 'zh' }, + { key: 'en', text: 'English', value: 'en' }, + ]; + + const changeLanguage = (language) => { + i18n.changeLanguage(language); + }; + if (isMobile()) { return ( <> @@ -175,10 +188,18 @@ const Header = () => { {renderButtons(true)} + + changeLanguage(value)} + /> + {userState.user ? ( ) : ( <> @@ -188,7 +209,7 @@ const Header = () => { navigate('/login'); }} > - 登录 + {t('header.login')} )} @@ -235,6 +256,17 @@ const Header = () => { {renderButtons(false)} + changeLanguage(value)} + style={{ + fontSize: '15px', + fontWeight: '400', + color: '#666', + }} + /> {userState.user ? ( { color: '#666', }} > - 注销 + {t('header.logout')} ) : ( { + const { t } = useTranslation(); const [redemptionCode, setRedemptionCode] = useState(''); const [topUpLink, setTopUpLink] = useState(''); const [userQuota, setUserQuota] = useState(0); @@ -20,7 +22,7 @@ const TopUp = () => { const topUp = async () => { if (redemptionCode === '') { - showInfo('请输入兑换码!'); + showInfo(t('topup.redeem_code.empty_code')); return; } setIsSubmitting(true); @@ -30,7 +32,7 @@ const TopUp = () => { }); const { success, message, data } = res.data; if (success) { - showSuccess('充值成功!'); + showSuccess(t('topup.redeem_code.success')); setUserQuota((quota) => { return quota + data; }); @@ -39,7 +41,7 @@ const TopUp = () => { showError(message); } } catch (err) { - showError('请求失败'); + showError(t('topup.redeem_code.request_failed')); } finally { setIsSubmitting(false); } @@ -47,13 +49,12 @@ const TopUp = () => { const openTopUpLink = () => { if (!topUpLink) { - showError('超级管理员未设置充值链接!'); + showError(t('topup.redeem_code.no_link')); return; } let url = new URL(topUpLink); let username = user.username; let user_id = user.id; - // add username and user_id to the topup link url.searchParams.append('username', username); url.searchParams.append('user_id', user_id); url.searchParams.append('transaction_id', crypto.randomUUID()); @@ -87,7 +88,7 @@ const TopUp = () => { -
充值中心
+
{t('topup.title')}
@@ -109,7 +110,7 @@ const TopUp = () => {
- 获取兑换码 + {t('topup.get_code.title')}
{ {renderQuota(userQuota)} - 当前可用额度 + + {t('topup.get_code.current_quota')} + @@ -145,7 +148,7 @@ const TopUp = () => { onClick={openTopUpLink} style={{ width: '80%' }} > - 立即获取兑换码 + {t('topup.get_code.button')} @@ -172,7 +175,7 @@ const TopUp = () => {
- 兑换码充值 + {t('topup.redeem_code.title')}
{ fluid icon='key' iconPosition='left' - placeholder='请输入兑换码' + placeholder={t('topup.redeem_code.placeholder')} value={redemptionCode} onChange={(e) => { setRedemptionCode(e.target.value); @@ -207,14 +210,14 @@ const TopUp = () => { action={