Merge pull request #622 from Calcium-Ion/i18n-fix

feat: Enhance i18n support
This commit is contained in:
Calcium-Ion
2024-12-14 14:12:27 +08:00
committed by GitHub
8 changed files with 124 additions and 129 deletions

View File

@@ -27,44 +27,15 @@ import Task from "./pages/Task/index.js";
import Playground from './pages/Playground/Playground.js'; import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js"; import OAuth2Callback from "./components/OAuth2Callback.js";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { StatusContext } from './context/Status';
import { setStatusData } from './helpers/data.js';
import { API, showError } from './helpers';
const Home = lazy(() => import('./pages/Home')); const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail')); const Detail = lazy(() => import('./pages/Detail'));
const About = lazy(() => import('./pages/About')); const About = lazy(() => import('./pages/About'));
function App() { function App() {
const [userState, userDispatch] = useContext(UserContext);
// const [statusState, statusDispatch] = useContext(StatusContext);
const { i18n } = useTranslation();
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
useEffect(() => {
loadUser();
let systemName = getSystemName();
if (systemName) {
document.title = systemName;
}
let logo = getLogo();
if (logo) {
let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) {
linkElement.href = logo;
}
}
// 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
i18n.changeLanguage(savedLang);
}
}, [i18n]);
return ( return (
<> <>
<Routes> <Routes>

View File

@@ -249,7 +249,7 @@ const ChannelsTable = () => {
} }
}, },
{ {
title: '优先级', title: t('优先级'),
dataIndex: 'priority', dataIndex: 'priority',
render: (text, record, index) => { render: (text, record, index) => {
if (record.children === undefined) { if (record.children === undefined) {
@@ -276,8 +276,8 @@ const ChannelsTable = () => {
keepFocus={true} keepFocus={true}
onBlur={(e) => { onBlur={(e) => {
Modal.warning({ Modal.warning({
title: '修改子渠道优先级', title: t('修改子渠道优先级'),
content: '确定要修改所有子渠道优先级为 ' + e.target.value + ' 吗?', content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'),
onOk: () => { onOk: () => {
if (e.target.value === '') { if (e.target.value === '') {
return; return;
@@ -298,7 +298,7 @@ const ChannelsTable = () => {
} }
}, },
{ {
title: '权重', title: t('权重'),
dataIndex: 'weight', dataIndex: 'weight',
render: (text, record, index) => { render: (text, record, index) => {
if (record.children === undefined) { if (record.children === undefined) {
@@ -325,8 +325,8 @@ const ChannelsTable = () => {
keepFocus={true} keepFocus={true}
onBlur={(e) => { onBlur={(e) => {
Modal.warning({ Modal.warning({
title: '修改子渠道权重', title: t('修改子渠道权重'),
content: '确定要修改所有子渠道权重为 ' + e.target.value + ' 吗?', content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'),
onOk: () => { onOk: () => {
if (e.target.value === '') { if (e.target.value === '') {
return; return;
@@ -646,25 +646,25 @@ const ChannelsTable = () => {
const copySelectedChannel = async (record) => { const copySelectedChannel = async (record) => {
const channelToCopy = record const channelToCopy = record
channelToCopy.name += '_复制'; channelToCopy.name += t('_复制');
channelToCopy.created_time = null; channelToCopy.created_time = null;
channelToCopy.balance = 0; channelToCopy.balance = 0;
channelToCopy.used_quota = 0; channelToCopy.used_quota = 0;
if (!channelToCopy) { if (!channelToCopy) {
showError('渠道未找到,请刷新页面后重试。'); showError(t('渠道未找到,请刷新页面后重试。'));
return; return;
} }
try { try {
const newChannel = { ...channelToCopy, id: undefined }; const newChannel = { ...channelToCopy, id: undefined };
const response = await API.post('/api/channel/', newChannel); const response = await API.post('/api/channel/', newChannel);
if (response.data.success) { if (response.data.success) {
showSuccess('渠道复制成功'); showSuccess(t('渠道复制成功'));
await refresh(); await refresh();
} else { } else {
showError(response.data.message); showError(response.data.message);
} }
} catch (error) { } catch (error) {
showError('渠道复制失败: ' + error.message); showError(t('渠道复制失败: ') + error.message);
} }
}; };
@@ -723,7 +723,7 @@ const ChannelsTable = () => {
} }
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess('操作成功完成!'); showSuccess(t('操作成功完成!'));
let channel = res.data.data; let channel = res.data.data;
let newChannels = [...channels]; let newChannels = [...channels];
if (action === 'delete') { if (action === 'delete') {

View File

@@ -25,24 +25,6 @@ import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { StyleContext } from '../context/Style/index.js'; import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
let headerButtons = [
{
text: '关于',
itemKey: 'about',
to: '/about',
icon: <IconHelpCircle />,
},
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
to: '/chat',
icon: 'comments',
});
}
const HeaderBar = () => { const HeaderBar = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);

View File

@@ -4,15 +4,65 @@ import SiderBar from './SiderBar.js';
import App from '../App.js'; import App from '../App.js';
import FooterBar from './Footer.js'; import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import React, { useContext } from 'react'; import React, { useContext, useEffect } from 'react';
import { StyleContext } from '../context/Style/index.js'; import { StyleContext } from '../context/Style/index.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
import { setStatusData } from '../helpers/data.js';
import { UserContext } from '../context/User/index.js';
import { StatusContext } from '../context/Status/index.js';
const { Sider, Content, Header, Footer } = Layout; const { Sider, Content, Header, Footer } = Layout;
const PageLayout = () => { const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext);
const [styleState, styleDispatch] = useContext(StyleContext); const [styleState, styleDispatch] = useContext(StyleContext);
const { t } = useTranslation(); const { i18n } = useTranslation();
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
const loadStatus = async () => {
try {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
} else {
showError('Unable to connect to server');
}
} catch (error) {
showError('Failed to load status');
}
};
useEffect(() => {
loadUser();
loadStatus().catch(console.error);
let systemName = getSystemName();
if (systemName) {
document.title = systemName;
}
let logo = getLogo();
if (logo) {
let linkElement = document.querySelector("link[rel~='icon']");
if (linkElement) {
linkElement.href = logo;
}
}
// 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) {
i18n.changeLanguage(savedLang);
}
}, [i18n]);
return ( return (
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}> <Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>

View File

@@ -168,31 +168,13 @@ const SiderBar = () => {
], ],
); );
const loadStatus = async () => {
const res = await API.get('/api/status');
if (res === undefined) {
return;
}
const { success, data } = res.data;
if (success) {
statusDispatch({ type: 'set', payload: data });
setStatusData(data);
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => { useEffect(() => {
loadStatus().then(() => {
setIsCollapsed(
localStorage.getItem('default_collapse_sidebar') === 'true',
);
});
let localKey = window.location.pathname.split('/')[1]; let localKey = window.location.pathname.split('/')[1];
if (localKey === '') { if (localKey === '') {
localKey = 'home'; localKey = 'home';
} }
setSelectedKeys([localKey]); setSelectedKeys([localKey]);
let chatLink = localStorage.getItem('chat_link'); let chatLink = localStorage.getItem('chat_link');
if (!chatLink) { if (!chatLink) {
let chats = localStorage.getItem('chats'); let chats = localStorage.getItem('chats');
@@ -220,6 +202,8 @@ const SiderBar = () => {
} }
} }
} }
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
}, []); }, []);
return ( return (

View File

@@ -709,7 +709,7 @@
"密码修改成功!": "Password changed successfully!", "密码修改成功!": "Password changed successfully!",
"划转金额最低为": "The minimum transfer amount is", "划转金额最低为": "The minimum transfer amount is",
"请输入邮箱!": "Please enter your email!", "请输入邮箱!": "Please enter your email!",
"验证码发送成功,请检查<EFBFBD><EFBFBD><EFBFBD>箱!": "The verification code was sent successfully, please check your email!", "验证码发送成功,请检查箱!": "The verification code was sent successfully, please check your email!",
"请输入邮箱验证码!": "Please enter the email verification code!", "请输入邮箱验证码!": "Please enter the email verification code!",
"请输入要划转的数量": "Please enter the amount to be transferred", "请输入要划转的数量": "Please enter the amount to be transferred",
"当前余额": "Current balance", "当前余额": "Current balance",
@@ -827,8 +827,8 @@
"模型消耗分布": "Model consumption distribution", "模型消耗分布": "Model consumption distribution",
"模型调用次数占比": "Proportion of model calls", "模型调用次数占比": "Proportion of model calls",
"用户消耗分布": "User consumption distribution", "用户消耗分布": "User consumption distribution",
"时间粒度": "time granularity", "时间粒度": "Time granularity",
"天": "sky", "天": "day",
"模型概览": "Model overview", "模型概览": "Model overview",
"用户概览": "User overview", "用户概览": "User overview",
"正在策划中": "Under planning", "正在策划中": "Under planning",
@@ -1209,5 +1209,13 @@
"首页内容已更新": "Home page content updated", "首页内容已更新": "Home page content updated",
"关于已更新": "About updated", "关于已更新": "About updated",
"模型测试": "model test", "模型测试": "model test",
"当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。": "Current Midjourney callback is not enabled, some projects may not be able to obtain drawing results, which can be enabled in the operation settings." "当前未开启Midjourney回调部分项目可能无法获得绘图结果可在运营设置中开启。": "Current Midjourney callback is not enabled, some projects may not be able to obtain drawing results, which can be enabled in the operation settings.",
"Telegram 身份验证": "Telegram authentication",
"Linux DO 身份验证": "Linux DO authentication",
"协议": "License",
"修改子渠道权重": "Modify sub-channel weight",
"确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ",
" 吗?": "?",
"修改子渠道优先级": "Modify sub-channel priority",
"确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to "
} }

View File

@@ -4,8 +4,10 @@ import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status'; import { StatusContext } from '../../context/Status';
import { marked } from 'marked'; import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js'; import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
const Home = () => { const Home = () => {
const { t } = useTranslation();
const [statusState] = useContext(StatusContext); const [statusState] = useContext(StatusContext);
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState(''); const [homePageContent, setHomePageContent] = useState('');
@@ -52,7 +54,8 @@ const Home = () => {
useEffect(() => { useEffect(() => {
displayNotice().then(); displayNotice().then();
displayHomePageContent().then(); displayHomePageContent().then();
}, []); });
return ( return (
<> <>
{homePageContentLoaded && homePageContent === '' ? ( {homePageContentLoaded && homePageContent === '' ? (
@@ -60,13 +63,13 @@ const Home = () => {
<Card <Card
bordered={false} bordered={false}
headerLine={false} headerLine={false}
title='系统状况' title={t('系统状况')}
bodyStyle={{ padding: '10px 20px' }} bodyStyle={{ padding: '10px 20px' }}
> >
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Card <Card
title='系统信息' title={t('系统信息')}
headerExtraContent={ headerExtraContent={
<span <span
style={{ style={{
@@ -74,19 +77,19 @@ const Home = () => {
color: 'var(--semi-color-text-1)', color: 'var(--semi-color-text-1)',
}} }}
> >
系统信息总览 {t('系统信息总览')}
</span> </span>
} }
> >
<p>名称{statusState?.status?.system_name}</p> <p>{t('名称')}{statusState?.status?.system_name}</p>
<p> <p>
版本 {t('版本')}
{statusState?.status?.version {statusState?.status?.version
? statusState?.status?.version ? statusState?.status?.version
: 'unknown'} : 'unknown'}
</p> </p>
<p> <p>
源码 {t('源码')}
<a <a
href='https://github.com/Calcium-Ion/new-api' href='https://github.com/Calcium-Ion/new-api'
target='_blank' target='_blank'
@@ -96,7 +99,7 @@ const Home = () => {
</a> </a>
</p> </p>
<p> <p>
协议 {t('协议')}
<a <a
href='https://www.apache.org/licenses/LICENSE-2.0' href='https://www.apache.org/licenses/LICENSE-2.0'
target='_blank' target='_blank'
@@ -105,12 +108,12 @@ const Home = () => {
Apache-2.0 License Apache-2.0 License
</a> </a>
</p> </p>
<p>启动时间{getStartTimeString()}</p> <p>{t('启动时间')}{getStartTimeString()}</p>
</Card> </Card>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Card <Card
title='系统配置' title={t('系统配置')}
headerExtraContent={ headerExtraContent={
<span <span
style={{ style={{
@@ -118,45 +121,45 @@ const Home = () => {
color: 'var(--semi-color-text-1)', color: 'var(--semi-color-text-1)',
}} }}
> >
系统配置总览 {t('系统配置总览')}
</span> </span>
} }
> >
<p> <p>
邮箱验证 {t('邮箱验证')}
{statusState?.status?.email_verification === true {statusState?.status?.email_verification === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
GitHub 身份验证 {t('GitHub 身份验证')}
{statusState?.status?.github_oauth === true {statusState?.status?.github_oauth === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
微信身份验证 {t('微信身份验证')}
{statusState?.status?.wechat_login === true {statusState?.status?.wechat_login === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
Turnstile 用户校验 {t('Turnstile 用户校验')}
{statusState?.status?.turnstile_check === true {statusState?.status?.turnstile_check === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
Telegram 身份验证 {t('Telegram 身份验证')}
{statusState?.status?.telegram_oauth === true {statusState?.status?.telegram_oauth === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
<p> <p>
Linux DO 身份验证 {t('Linux DO 身份验证')}
{statusState?.status?.linuxdo_oauth === true {statusState?.status?.linuxdo_oauth === true
? '已启用' ? t('已启用')
: '未启用'} : t('未启用')}
</p> </p>
</Card> </Card>
</Col> </Col>

View File

@@ -97,32 +97,29 @@ const Playground = () => {
let res = await API.get(`/api/user/self/groups`); let res = await API.get(`/api/user/self/groups`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
// return data is a map, key is group name, value is group description
// label is group description, value is group name
let localGroupOptions = Object.keys(data).map((group) => ({ let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group], label: data[group],
value: group, value: group,
})); }));
// handleInputChange('group', localGroupOptions[0].value);
if (localGroupOptions.length > 0) { if (localGroupOptions.length === 0) {
// set user group at first localGroupOptions = [{
if (userState.user && userState.user.group) { label: t('用户分组'),
let userGroup = userState.user.group; value: '',
// Find and move user's group to the front }];
} else {
const localUser = JSON.parse(localStorage.getItem('user'));
const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group);
if (userGroup) {
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup); const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
if (userGroupIndex > -1) { if (userGroupIndex > -1) {
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0]; const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
localGroupOptions.unshift(userGroupOption); localGroupOptions.unshift(userGroupOption);
} }
} }
} else {
localGroupOptions = [{
label: t('用户分组'),
value: '',
}];
setGroups(localGroupOptions);
} }
setGroups(localGroupOptions); setGroups(localGroupOptions);
handleInputChange('group', localGroupOptions[0].value); handleInputChange('group', localGroupOptions[0].value);
} else { } else {