From ae20aea5553890f4deaab4c5cfc0dc017a5d1501 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 1 Feb 2025 17:00:24 +0800 Subject: [PATCH] feat: i18n support --- .../public/locales/en/translation.json | 31 ++++++++++++++++ .../public/locales/zh/translation.json | 31 ++++++++++++++++ web/default/src/App.js | 5 +-- web/default/src/components/LogsTable.js | 10 +++--- .../src/components/RedemptionsTable.js | 19 +++++----- web/default/src/components/TokensTable.js | 35 ++++++++++++------- web/default/src/components/UsersTable.js | 6 ++-- web/default/src/helpers/render.js | 22 +++++++----- web/default/src/pages/Log/index.js | 25 +++++++------ .../src/pages/Redemption/EditRedemption.js | 22 +++++++----- web/default/src/pages/Token/EditToken.js | 9 +++-- web/default/src/pages/Token/index.js | 2 +- web/default/src/pages/TopUp/index.js | 2 +- 13 files changed, 156 insertions(+), 63 deletions(-) diff --git a/web/default/public/locales/en/translation.json b/web/default/public/locales/en/translation.json index 3c1b23fa..ea32c04c 100644 --- a/web/default/public/locales/en/translation.json +++ b/web/default/public/locales/en/translation.json @@ -240,5 +240,36 @@ "display_short": "${{amount}}", "unit": "$" } + }, + "redemption": { + "title": "Redemption Management", + "edit": { + "title_edit": "Update Redemption Code", + "title_create": "Create New Redemption Code", + "name": "Name", + "name_placeholder": "Please enter name", + "quota": "Quota", + "quota_placeholder": "Please enter quota per redemption code", + "count": "Generate Count", + "count_placeholder": "Please enter number of codes to generate", + "buttons": { + "submit": "Submit", + "cancel": "Cancel" + } + } + }, + "log": { + "title": "Operation Log", + "usage_details": "Usage Details", + "total_quota": "Total Quota Used", + "click_to_view": "Click to View", + "table": { + "id": "ID", + "username": "Username", + "type": "Type", + "content": "Content", + "amount": "Amount", + "time": "Time" + } } } diff --git a/web/default/public/locales/zh/translation.json b/web/default/public/locales/zh/translation.json index 9cf78f34..9a978289 100644 --- a/web/default/public/locales/zh/translation.json +++ b/web/default/public/locales/zh/translation.json @@ -240,5 +240,36 @@ "display_short": "${{amount}}", "unit": "$" } + }, + "redemption": { + "title": "兑换管理", + "edit": { + "title_edit": "更新兑换码信息", + "title_create": "创建新的兑换码", + "name": "名称", + "name_placeholder": "请输入名称", + "quota": "额度", + "quota_placeholder": "请输入单个兑换码中包含的额度", + "count": "生成数量", + "count_placeholder": "请输入生成数量", + "buttons": { + "submit": "提交", + "cancel": "取消" + } + } + }, + "log": { + "title": "操作日志", + "usage_details": "使用明细", + "total_quota": "总消耗额度", + "click_to_view": "点击查看", + "table": { + "id": "ID", + "username": "用户名", + "type": "类型", + "content": "内容", + "amount": "数量", + "time": "时间" + } } } diff --git a/web/default/src/App.js b/web/default/src/App.js index 0e63a403..e76ee7ea 100644 --- a/web/default/src/App.js +++ b/web/default/src/App.js @@ -44,8 +44,9 @@ function App() { const loadStatus = async () => { try { const res = await API.get('/api/status'); - const { success, message, data } = res.data || {}; // Add default empty object - if (success && data) { // Check data exists + const { success, message, data } = res.data || {}; // Add default empty object + if (success && data) { + // Check data exists localStorage.setItem('status', JSON.stringify(data)); statusDispatch({ type: 'set', payload: data }); localStorage.setItem('system_name', data.system_name); diff --git a/web/default/src/components/LogsTable.js b/web/default/src/components/LogsTable.js index e1c13331..5cf7ceba 100644 --- a/web/default/src/components/LogsTable.js +++ b/web/default/src/components/LogsTable.js @@ -18,6 +18,7 @@ import { showWarning, timestamp2string, } from '../helpers'; +import { useTranslation } from 'react-i18next'; import { ITEMS_PER_PAGE } from '../constants'; import { renderColorLabel, renderQuota } from '../helpers/render'; @@ -137,6 +138,7 @@ function renderDetail(log) { } const LogsTable = () => { + const { t } = useTranslation(); const [logs, setLogs] = useState([]); const [showStat, setShowStat] = useState(false); const [loading, setLoading] = useState(true); @@ -309,14 +311,14 @@ const LogsTable = () => { <> <>
- 使用明细(总消耗额度: - {showStat && renderQuota(stat.quota)} + {t('log.usage_details')}({t('log.total_quota')}: + {showStat && renderQuota(stat.quota, t)} {!showStat && ( - 点击查看 + {t('log.click_to_view')} )} ) @@ -554,7 +556,7 @@ const LogsTable = () => { {log.completion_tokens ? log.completion_tokens : ''} - {log.quota ? renderQuota(log.quota, 6) : ''} + {log.quota ? renderQuota(log.quota, t, 6) : ''} )} diff --git a/web/default/src/components/RedemptionsTable.js b/web/default/src/components/RedemptionsTable.js index ccea9ff3..70dab123 100644 --- a/web/default/src/components/RedemptionsTable.js +++ b/web/default/src/components/RedemptionsTable.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, @@ -25,39 +26,37 @@ function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; } -function renderStatus(status) { +function renderStatus(status, t) { switch (status) { case 1: return ( ); case 2: return ( ); case 3: return ( ); default: return ( ); } } const RedemptionsTable = () => { + const { t } = useTranslation(); const [redemptions, setRedemptions] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); @@ -260,8 +259,8 @@ const RedemptionsTable = () => { {redemption.name ? redemption.name : '无'} - {renderStatus(redemption.status)} - {renderQuota(redemption.quota)} + {renderStatus(redemption.status, t)} + {renderQuota(redemption.quota, t)} {renderTimestamp(redemption.created_time)} diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 401d5fe8..f8e9bcdb 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -69,14 +69,14 @@ const TokensTable = () => { { key: 'next', text: t('token.copy_options.next'), value: 'next' }, { key: 'ama', text: t('token.copy_options.ama'), value: 'ama' }, { key: 'opencat', text: t('token.copy_options.opencat'), value: 'opencat' }, - { key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' } + { key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' }, ]; const OPEN_LINK_OPTIONS = [ { key: 'next', text: t('token.copy_options.next'), value: 'next' }, { key: 'ama', text: t('token.copy_options.ama'), value: 'ama' }, { key: 'opencat', text: t('token.copy_options.opencat'), value: 'opencat' }, - { key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' } + { key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' }, ]; const [tokens, setTokens] = useState([]); @@ -135,7 +135,8 @@ const TokensTable = () => { let nextUrl; if (nextLink) { - nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + nextUrl = + nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } else { nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } @@ -152,7 +153,9 @@ const TokensTable = () => { url = nextUrl; break; case 'lobechat': - url = nextLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`; + url = + nextLink + + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}/v1"}}}`; break; default: url = `sk-${key}`; @@ -376,19 +379,21 @@ const TokensTable = () => { .map((token, idx) => { if (token.deleted) return <>; - const copyOptionsWithHandlers = COPY_OPTIONS.map(option => ({ + const copyOptionsWithHandlers = COPY_OPTIONS.map((option) => ({ ...option, onClick: async () => { await onCopy(option.value, token.key); - } + }, })); - const openLinkOptionsWithHandlers = OPEN_LINK_OPTIONS.map(option => ({ - ...option, - onClick: async () => { - await onOpenLink(option.value, token.key); - } - })); + const openLinkOptionsWithHandlers = OPEN_LINK_OPTIONS.map( + (option) => ({ + ...option, + onClick: async () => { + await onOpenLink(option.value, token.key); + }, + }) + ); return ( @@ -473,7 +478,11 @@ const TokensTable = () => { ? t('token.buttons.disable') : t('token.buttons.enable')} - diff --git a/web/default/src/components/UsersTable.js b/web/default/src/components/UsersTable.js index 2e08b715..4951d104 100644 --- a/web/default/src/components/UsersTable.js +++ b/web/default/src/components/UsersTable.js @@ -10,6 +10,7 @@ import { } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; import { API, showError, showSuccess } from '../helpers'; +import { useTranslation } from 'react-i18next'; import { ITEMS_PER_PAGE } from '../constants'; import { @@ -33,6 +34,7 @@ function renderRole(role) { } const UsersTable = () => { + const { t } = useTranslation(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); @@ -266,12 +268,12 @@ const UsersTable = () => { {renderQuota(user.quota)}} + trigger={} /> {renderQuota(user.used_quota)} + } /> ( -
- - - {/*操作日志*/} - - - -
-); +const Log = () => { + const { t } = useTranslation(); + + return ( +
+ + + {t('log.title')} + + + +
+ ); +}; export default Log; diff --git a/web/default/src/pages/Redemption/EditRedemption.js b/web/default/src/pages/Redemption/EditRedemption.js index e0801830..1aee6e10 100644 --- a/web/default/src/pages/Redemption/EditRedemption.js +++ b/web/default/src/pages/Redemption/EditRedemption.js @@ -1,10 +1,12 @@ import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button, Form, Card } from 'semantic-ui-react'; import { useParams, useNavigate } from 'react-router-dom'; import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; const EditRedemption = () => { + const { t } = useTranslation(); const params = useParams(); const navigate = useNavigate(); const redemptionId = params.id; @@ -83,14 +85,14 @@ const EditRedemption = () => { - {isEdit ? '更新兑换码信息' : '创建新的兑换码'} + {isEdit ? t('redemption.edit.title_edit') : t('redemption.edit.title_create')}
{ { <> { )} + -
diff --git a/web/default/src/pages/Token/EditToken.js b/web/default/src/pages/Token/EditToken.js index 28ae1e59..e5bfe786 100644 --- a/web/default/src/pages/Token/EditToken.js +++ b/web/default/src/pages/Token/EditToken.js @@ -107,12 +107,12 @@ const EditToken = () => { useEffect(() => { if (isEdit) { - loadToken().catch(error => { + loadToken().catch((error) => { showError(error.message || 'Failed to load token'); setLoading(false); }); } - loadAvailableModels().catch(error => { + loadAvailableModels().catch((error) => { showError(error.message || 'Failed to load models'); }); }, []); @@ -255,7 +255,10 @@ const EditToken = () => { {t('token.edit.quota_notice')} { const { t } = useTranslation(); - + return (
diff --git a/web/default/src/pages/TopUp/index.js b/web/default/src/pages/TopUp/index.js index 9ffb6af0..2d037646 100644 --- a/web/default/src/pages/TopUp/index.js +++ b/web/default/src/pages/TopUp/index.js @@ -131,7 +131,7 @@ const TopUp = () => {
- {renderQuota(userQuota)} + {renderQuota(userQuota, t)} {t('topup.get_code.current_quota')}