mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-17 09:16:36 +08:00
feat: initial i18n support
This commit is contained in:
parent
1521df6551
commit
bdf312e5dc
@ -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",
|
||||
|
38
web/default/public/locales/en/translation.json
Normal file
38
web/default/public/locales/en/translation.json
Normal file
@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
38
web/default/public/locales/zh/translation.json
Normal file
38
web/default/public/locales/zh/translation.json
Normal file
@ -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": "超级管理员未设置充值链接!"
|
||||
}
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<Menu.Item
|
||||
key={button.name}
|
||||
onClick={() => {
|
||||
navigate(button.to);
|
||||
setShowSidebar(false);
|
||||
}}
|
||||
style={{ fontSize: '15px' }}
|
||||
>
|
||||
{button.name}
|
||||
{t(button.name)}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
@ -134,12 +137,22 @@ const Header = () => {
|
||||
}}
|
||||
>
|
||||
<Icon name={button.icon} style={{ marginRight: '4px' }} />
|
||||
{button.name}
|
||||
{t(button.name)}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// 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 = () => {
|
||||
<Segment style={{ marginTop: 0, borderTop: '0' }}>
|
||||
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||
{renderButtons(true)}
|
||||
<Menu.Item>
|
||||
<Dropdown
|
||||
selection
|
||||
options={languageOptions}
|
||||
value={i18n.language}
|
||||
onChange={(_, { value }) => changeLanguage(value)}
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{userState.user ? (
|
||||
<Button onClick={logout} style={{ color: '#666666' }}>
|
||||
注销
|
||||
{t('header.logout')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
@ -188,7 +209,7 @@ const Header = () => {
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
登录
|
||||
{t('header.login')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -196,7 +217,7 @@ const Header = () => {
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
{t('header.register')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@ -235,6 +256,17 @@ const Header = () => {
|
||||
</Menu.Item>
|
||||
{renderButtons(false)}
|
||||
<Menu.Menu position='right'>
|
||||
<Dropdown
|
||||
item
|
||||
options={languageOptions}
|
||||
value={i18n.language}
|
||||
onChange={(_, { value }) => changeLanguage(value)}
|
||||
style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: '400',
|
||||
color: '#666',
|
||||
}}
|
||||
/>
|
||||
{userState.user ? (
|
||||
<Dropdown
|
||||
text={userState.user.username}
|
||||
@ -255,13 +287,13 @@ const Header = () => {
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
注销
|
||||
{t('header.logout')}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Menu.Item
|
||||
name='登录'
|
||||
name={t('header.login')}
|
||||
as={Link}
|
||||
to='/login'
|
||||
className='btn btn-link'
|
||||
|
23
web/default/src/i18n.js
Normal file
23
web/default/src/i18n.js
Normal file
@ -0,0 +1,23 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'zh',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
@ -11,6 +11,7 @@ import { UserProvider } from './context/User';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { StatusProvider } from './context/Status';
|
||||
import './i18n';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
|
@ -10,8 +10,10 @@ import {
|
||||
} from 'semantic-ui-react';
|
||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
|
||||
import { renderQuota } from '../../helpers/render';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const TopUp = () => {
|
||||
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 = () => {
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header>
|
||||
<Header as='h2'>充值中心</Header>
|
||||
<Header as='h2'>{t('topup.title')}</Header>
|
||||
</Card.Header>
|
||||
|
||||
<Grid columns={2} stackable>
|
||||
@ -109,7 +110,7 @@ const TopUp = () => {
|
||||
<Card.Header>
|
||||
<Header as='h3' style={{ color: '#2185d0', margin: '1em' }}>
|
||||
<i className='credit card icon'></i>
|
||||
获取兑换码
|
||||
{t('topup.get_code.title')}
|
||||
</Header>
|
||||
</Card.Header>
|
||||
<Card.Description
|
||||
@ -132,7 +133,9 @@ const TopUp = () => {
|
||||
<Statistic.Value style={{ color: '#2185d0' }}>
|
||||
{renderQuota(userQuota)}
|
||||
</Statistic.Value>
|
||||
<Statistic.Label>当前可用额度</Statistic.Label>
|
||||
<Statistic.Label>
|
||||
{t('topup.get_code.current_quota')}
|
||||
</Statistic.Label>
|
||||
</Statistic>
|
||||
</div>
|
||||
|
||||
@ -145,7 +148,7 @@ const TopUp = () => {
|
||||
onClick={openTopUpLink}
|
||||
style={{ width: '80%' }}
|
||||
>
|
||||
立即获取兑换码
|
||||
{t('topup.get_code.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -172,7 +175,7 @@ const TopUp = () => {
|
||||
<Card.Header>
|
||||
<Header as='h3' style={{ color: '#21ba45', margin: '1em' }}>
|
||||
<i className='ticket alternate icon'></i>
|
||||
兑换码充值
|
||||
{t('topup.redeem_code.title')}
|
||||
</Header>
|
||||
</Card.Header>
|
||||
<Card.Description
|
||||
@ -194,7 +197,7 @@ const TopUp = () => {
|
||||
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={
|
||||
<Button
|
||||
icon='paste'
|
||||
content='粘贴'
|
||||
content={t('topup.redeem_code.paste')}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const text =
|
||||
await navigator.clipboard.readText();
|
||||
setRedemptionCode(text.trim());
|
||||
} catch (err) {
|
||||
showError('无法访问剪贴板,请手动粘贴');
|
||||
showError(t('topup.redeem_code.paste_error'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -230,7 +233,9 @@ const TopUp = () => {
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '兑换中...' : '立即兑换'}
|
||||
{isSubmitting
|
||||
? t('topup.redeem_code.submitting')
|
||||
: t('topup.redeem_code.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user