diff --git a/i18n/en.json b/i18n/en.json
index 944f903b..1f480f96 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -174,11 +174,11 @@
"\"验证码\"": "\"Verification code\"",
"全部用户": "All users",
"当前用户": "Current user",
- "'全部'": "'All'",
- "'充值'": "'Recharge'",
- "'消费'": "'Consumption'",
- "'管理'": "'Management'",
- "'系统'": "'System'",
+ "全部'": "All'",
+ "充值'": "Recharge'",
+ "消费'": "Consumption'",
+ "管理'": "Management'",
+ "系统'": "System'",
" 充值 ": " Recharge ",
" 消费 ": " Consumption ",
" 管理 ": " Management ",
@@ -377,6 +377,7 @@
"添加新的用户": "Add New User",
"自定义": "Custom",
"等价金额": "Equivalent Amount",
+ "等价金额:{{quota}}:": "Equivalent amount: {{quota}}",
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
"请求次数过多,请稍后再试": "Too many requests, please try again later",
"服务器内部错误,请联系管理员": "Server internal error, please contact the administrator",
@@ -525,5 +526,13 @@
"模型版本": "Model version",
"请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
"点击查看": "click to view",
- "请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!"
+ "请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!",
+ "第 {{start}} - {{end}} 条,共 {{total}} 条": "Items {{start}} - {{end}} of {{total}}",
+ "模型测试": "Model test",
+ "请选择最长响应时间": "Please select the longest response time",
+ "成功时自动启用通道": "Enable channel when successful",
+ "分钟": "minutes",
+ "设置过短会影响数据库性能": "Setting too short will affect database performance",
+ "仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour",
+ "当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded"
}
diff --git a/web/package.json b/web/package.json
index 7c32b5d9..edb0ccf5 100644
--- a/web/package.json
+++ b/web/package.json
@@ -23,7 +23,10 @@
"react-turnstile": "^1.0.5",
"semantic-ui-offline": "^2.5.0",
"semantic-ui-react": "^2.1.3",
- "sse": "github:mpetazzoni/sse.js"
+ "sse": "github:mpetazzoni/sse.js",
+ "i18next": "^23.16.8",
+ "react-i18next": "^13.0.0",
+ "i18next-browser-languagedetector": "^7.2.0"
},
"scripts": {
"dev": "vite",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 852b35d0..5da07e60 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -32,6 +32,12 @@ importers:
history:
specifier: ^5.3.0
version: 5.3.0
+ i18next:
+ specifier: ^23.16.8
+ version: 23.16.8
+ i18next-browser-languagedetector:
+ specifier: ^7.2.0
+ version: 7.2.2
marked:
specifier: ^4.1.1
version: 4.3.0
@@ -47,6 +53,9 @@ importers:
react-fireworks:
specifier: ^1.0.4
version: 1.0.4
+ react-i18next:
+ specifier: ^13.0.0
+ version: 13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: ^6.3.0
version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1071,6 +1080,15 @@ packages:
history@5.3.0:
resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}
+ html-parse-stringify@3.0.1:
+ resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
+
+ i18next-browser-languagedetector@7.2.2:
+ resolution: {integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==}
+
+ i18next@23.16.8:
+ resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -1487,6 +1505,19 @@ packages:
react-fireworks@1.0.4:
resolution: {integrity: sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw==}
+ react-i18next@13.5.0:
+ resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==}
+ peerDependencies:
+ i18next: '>= 23.2.3'
+ react: '>= 16.8.0'
+ react-dom: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -1812,6 +1843,10 @@ packages:
terser:
optional: true
+ void-elements@3.1.0:
+ resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
+ engines: {node: '>=0.10.0'}
+
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
@@ -2935,6 +2970,18 @@ snapshots:
dependencies:
'@babel/runtime': 7.26.0
+ html-parse-stringify@3.0.1:
+ dependencies:
+ void-elements: 3.1.0
+
+ i18next-browser-languagedetector@7.2.2:
+ dependencies:
+ '@babel/runtime': 7.26.0
+
+ i18next@23.16.8:
+ dependencies:
+ '@babel/runtime': 7.26.0
+
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -3611,6 +3658,15 @@ snapshots:
react-fireworks@1.0.4: {}
+ react-i18next@13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ html-parse-stringify: 3.0.1
+ i18next: 23.16.8
+ react: 18.3.1
+ optionalDependencies:
+ react-dom: 18.3.1(react@18.3.1)
+
react-is@16.13.1: {}
react-is@18.3.1: {}
@@ -3998,6 +4054,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
+ void-elements@3.1.0: {}
+
warning@4.0.3:
dependencies:
loose-envify: 1.4.0
diff --git a/web/src/App.js b/web/src/App.js
index c42e6363..10ad9e34 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -26,6 +26,7 @@ import Pricing from './pages/Pricing/index.js';
import Task from "./pages/Task/index.js";
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
+import { useTranslation } from 'react-i18next';
const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
@@ -34,6 +35,7 @@ const About = lazy(() => import('./pages/About'));
function App() {
const [userState, userDispatch] = useContext(UserContext);
// const [statusState, statusDispatch] = useContext(StatusContext);
+ const { i18n } = useTranslation();
const loadUser = () => {
let user = localStorage.getItem('user');
@@ -56,7 +58,12 @@ function App() {
linkElement.href = logo;
}
}
- }, []);
+ // 从localStorage获取上次使用的语言
+ const savedLang = localStorage.getItem('i18nextLng');
+ if (savedLang) {
+ i18n.changeLanguage(savedLang);
+ }
+ }, [i18n]);
return (
<>
diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js
index 61dd45a2..3167650f 100644
--- a/web/src/components/ChannelsTable.js
+++ b/web/src/components/ChannelsTable.js
@@ -36,43 +36,111 @@ import { IconList, IconTreeTriangleDown } from '@douyinfe/semi-icons';
import { loadChannelModels } from './utils.js';
import EditTagModal from '../pages/Channel/EditTagModal.js';
import TextNumberInput from './custom/TextNumberInput.js';
+import { useTranslation } from 'react-i18next';
function renderTimestamp(timestamp) {
return <>{timestamp2string(timestamp)}>;
}
-let type2label = undefined;
-
-function renderType(type) {
- if (!type2label) {
- type2label = new Map();
- for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
- type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
- }
- type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
- }
- return (
-