fix: Complete i18n implementation for English language display

This commit addresses the issue where switching to English still showed Chinese text in many UI elements.

Changes made:
- Added 40+ missing translation keys to translations.ts for both English and Chinese
- Updated App.tsx to use t() function for all hardcoded text (logout button, loading message)
- Updated AITradersPage.tsx extensively:
  - Replaced all alert/confirm messages with translation keys
  - Updated status labels (inUse, enabled, configured)
  - Updated all modal components (SignalSource, ModelConfig, ExchangeConfig)
  - Added language prop to all modal components
  - Replaced all Chinese UI text with translation function calls

The implementation now properly supports language switching between English and Chinese throughout the entire application.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tangmengqiu
2025-10-31 23:58:44 -04:00
parent b87fcb62bd
commit 8bc06ae619
3 changed files with 212 additions and 81 deletions

View File

@@ -179,7 +179,7 @@ function App() {
style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}> style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
</div> </div>
<p style={{ color: '#EAECEF' }}>...</p> <p style={{ color: '#EAECEF' }}>{t('loading', language)}</p>
</div> </div>
</div> </div>
); );
@@ -299,7 +299,7 @@ function App() {
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105" className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }} style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
> >
退 {t('logout', language)}
</button> </button>
)} )}
</div> </div>

View File

@@ -3,7 +3,7 @@ import useSWR from 'swr';
import { api } from '../lib/api'; import { api } from '../lib/api';
import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t, type Language } from '../i18n/translations';
import { getExchangeIcon } from './ExchangeIcons'; import { getExchangeIcon } from './ExchangeIcons';
import { getModelIcon } from './ModelIcons'; import { getModelIcon } from './ModelIcons';
import { TraderConfigModal } from './TraderConfigModal'; import { TraderConfigModal } from './TraderConfigModal';
@@ -149,7 +149,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
mutateTraders(); mutateTraders();
} catch (error) { } catch (error) {
console.error('Failed to create trader:', error); console.error('Failed to create trader:', error);
alert('创建交易员失败'); alert(t('createTraderFailed', language));
} }
}; };
@@ -160,7 +160,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowEditModal(true); setShowEditModal(true);
} catch (error) { } catch (error) {
console.error('Failed to fetch trader config:', error); console.error('Failed to fetch trader config:', error);
alert('获取交易员配置失败'); alert(t('getTraderConfigFailed', language));
} }
}; };
@@ -170,14 +170,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
try { try {
const model = enabledModels?.find(m => m.id === data.ai_model_id); const model = enabledModels?.find(m => m.id === data.ai_model_id);
const exchange = enabledExchanges?.find(e => e.id === data.exchange_id); const exchange = enabledExchanges?.find(e => e.id === data.exchange_id);
if (!model) { if (!model) {
alert('AI模型配置不存在或未启用'); alert(t('modelConfigNotExist', language));
return; return;
} }
if (!exchange) { if (!exchange) {
alert('交易所配置不存在或未启用'); alert(t('exchangeConfigNotExist', language));
return; return;
} }
@@ -202,19 +202,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
mutateTraders(); mutateTraders();
} catch (error) { } catch (error) {
console.error('Failed to update trader:', error); console.error('Failed to update trader:', error);
alert('更新交易员失败'); alert(t('updateTraderFailed', language));
} }
}; };
const handleDeleteTrader = async (traderId: string) => { const handleDeleteTrader = async (traderId: string) => {
if (!confirm(t('confirmDeleteTrader', language))) return; if (!confirm(t('confirmDeleteTrader', language))) return;
try { try {
await api.deleteTrader(traderId); await api.deleteTrader(traderId);
mutateTraders(); mutateTraders();
} catch (error) { } catch (error) {
console.error('Failed to delete trader:', error); console.error('Failed to delete trader:', error);
alert('删除交易员失败'); alert(t('deleteTraderFailed', language));
} }
}; };
@@ -228,7 +228,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
mutateTraders(); mutateTraders();
} catch (error) { } catch (error) {
console.error('Failed to toggle trader:', error); console.error('Failed to toggle trader:', error);
alert('操作失败'); alert(t('operationFailed', language));
} }
}; };
@@ -247,7 +247,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}; };
const handleDeleteModelConfig = async (modelId: string) => { const handleDeleteModelConfig = async (modelId: string) => {
if (!confirm('确定要删除此AI模型配置吗')) return; if (!confirm(t('confirmDeleteModel', language))) return;
try { try {
const updatedModels = allModels?.map(m => const updatedModels = allModels?.map(m =>
@@ -272,7 +272,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingModel(null); setEditingModel(null);
} catch (error) { } catch (error) {
console.error('Failed to delete model config:', error); console.error('Failed to delete model config:', error);
alert('删除配置失败'); alert(t('deleteConfigFailed', language));
} }
}; };
@@ -281,7 +281,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// 找到要配置的模型从supportedModels中 // 找到要配置的模型从supportedModels中
const modelToUpdate = supportedModels?.find(m => m.id === modelId); const modelToUpdate = supportedModels?.find(m => m.id === modelId);
if (!modelToUpdate) { if (!modelToUpdate) {
alert('模型不存在'); alert(t('modelNotExist', language));
return; return;
} }
@@ -323,12 +323,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingModel(null); setEditingModel(null);
} catch (error) { } catch (error) {
console.error('Failed to save model config:', error); console.error('Failed to save model config:', error);
alert('保存配置失败'); alert(t('saveConfigFailed', language));
} }
}; };
const handleDeleteExchangeConfig = async (exchangeId: string) => { const handleDeleteExchangeConfig = async (exchangeId: string) => {
if (!confirm('确定要删除此交易所配置吗?')) return; if (!confirm(t('confirmDeleteExchange', language))) return;
try { try {
const updatedExchanges = allExchanges?.map(e => const updatedExchanges = allExchanges?.map(e =>
@@ -355,7 +355,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingExchange(null); setEditingExchange(null);
} catch (error) { } catch (error) {
console.error('Failed to delete exchange config:', error); console.error('Failed to delete exchange config:', error);
alert('删除交易所配置失败'); alert(t('deleteExchangeConfigFailed', language));
} }
}; };
@@ -364,7 +364,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// 找到要配置的交易所从supportedExchanges中 // 找到要配置的交易所从supportedExchanges中
const exchangeToUpdate = supportedExchanges?.find(e => e.id === exchangeId); const exchangeToUpdate = supportedExchanges?.find(e => e.id === exchangeId);
if (!exchangeToUpdate) { if (!exchangeToUpdate) {
alert('交易所不存在'); alert(t('exchangeNotExist', language));
return; return;
} }
@@ -411,7 +411,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setEditingExchange(null); setEditingExchange(null);
} catch (error) { } catch (error) {
console.error('Failed to save exchange config:', error); console.error('Failed to save exchange config:', error);
alert('保存交易所配置失败'); alert(t('saveConfigFailed', language));
} }
}; };
@@ -432,7 +432,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowSignalSourceModal(false); setShowSignalSourceModal(false);
} catch (error) { } catch (error) {
console.error('Failed to save signal source:', error); console.error('Failed to save signal source:', error);
alert('保存信号源配置失败'); alert(t('saveSignalSourceFailed', language));
} }
}; };
@@ -493,13 +493,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button <button
onClick={() => setShowSignalSourceModal(true)} onClick={() => setShowSignalSourceModal(true)}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105" className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ style={{
background: '#2B3139', background: '#2B3139',
color: '#EAECEF', color: '#EAECEF',
border: '1px solid #474D57' border: '1px solid #474D57'
}} }}
> >
📡 📡 {t('signalSource', language)}
</button> </button>
<button <button
@@ -552,7 +552,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div> <div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(model.name)}</div> <div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(model.name)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{inUse ? '正在使用' : model.enabled ? '已启用' : '已配置'} {inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)}
</div> </div>
</div> </div>
</div> </div>
@@ -563,7 +563,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{configuredModels.length === 0 && ( {configuredModels.length === 0 && (
<div className="text-center py-8" style={{ color: '#848E9C' }}> <div className="text-center py-8" style={{ color: '#848E9C' }}>
<Brain className="w-12 h-12 mx-auto mb-2 opacity-50" /> <Brain className="w-12 h-12 mx-auto mb-2 opacity-50" />
<div className="text-sm">AI模型</div> <div className="text-sm">{t('noModelsConfigured', language)}</div>
</div> </div>
)} )}
</div> </div>
@@ -594,7 +594,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<div> <div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(exchange.name)}</div> <div className="font-semibold" style={{ color: '#EAECEF' }}>{getShortName(exchange.name)}</div>
<div className="text-xs" style={{ color: '#848E9C' }}> <div className="text-xs" style={{ color: '#848E9C' }}>
{exchange.type.toUpperCase()} {inUse ? '正在使用' : exchange.enabled ? '已启用' : '已配置'} {exchange.type.toUpperCase()} {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
</div> </div>
</div> </div>
</div> </div>
@@ -605,7 +605,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{configuredExchanges.length === 0 && ( {configuredExchanges.length === 0 && (
<div className="text-center py-8" style={{ color: '#848E9C' }}> <div className="text-center py-8" style={{ color: '#848E9C' }}>
<Landmark className="w-12 h-12 mx-auto mb-2 opacity-50" /> <Landmark className="w-12 h-12 mx-auto mb-2 opacity-50" />
<div className="text-sm"></div> <div className="text-sm">{t('noExchangesConfigured', language)}</div>
</div> </div>
)} )}
</div> </div>
@@ -669,19 +669,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }} style={{ background: 'rgba(99, 102, 241, 0.1)', color: '#6366F1' }}
> >
<BarChart3 className="w-4 h-4" /> <BarChart3 className="w-4 h-4" />
{t('view', language)}
</button> </button>
<button <button
onClick={() => handleEditTrader(trader.trader_id)} onClick={() => handleEditTrader(trader.trader_id)}
disabled={trader.is_running} disabled={trader.is_running}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed" className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed"
style={{ style={{
background: trader.is_running ? 'rgba(132, 142, 156, 0.1)' : 'rgba(255, 193, 7, 0.1)', background: trader.is_running ? 'rgba(132, 142, 156, 0.1)' : 'rgba(255, 193, 7, 0.1)',
color: trader.is_running ? '#848E9C' : '#FFC107' color: trader.is_running ? '#848E9C' : '#FFC107'
}} }}
> >
{t('edit', language)}
</button> </button>
<button <button
@@ -766,6 +766,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowModelModal(false); setShowModelModal(false);
setEditingModel(null); setEditingModel(null);
}} }}
language={language}
/> />
)} )}
@@ -780,6 +781,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
setShowExchangeModal(false); setShowExchangeModal(false);
setEditingExchange(null); setEditingExchange(null);
}} }}
language={language}
/> />
)} )}
@@ -790,6 +792,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
oiTopUrl={userSignalSource.oiTopUrl} oiTopUrl={userSignalSource.oiTopUrl}
onSave={handleSaveSignalSource} onSave={handleSaveSignalSource}
onClose={() => setShowSignalSourceModal(false)} onClose={() => setShowSignalSourceModal(false)}
language={language}
/> />
)} )}
</div> </div>
@@ -801,12 +804,14 @@ function SignalSourceModal({
coinPoolUrl, coinPoolUrl,
oiTopUrl, oiTopUrl,
onSave, onSave,
onClose onClose,
language
}: { }: {
coinPoolUrl: string; coinPoolUrl: string;
oiTopUrl: string; oiTopUrl: string;
onSave: (coinPoolUrl: string, oiTopUrl: string) => void; onSave: (coinPoolUrl: string, oiTopUrl: string) => void;
onClose: () => void; onClose: () => void;
language: Language;
}) { }) {
const [coinPool, setCoinPool] = useState(coinPoolUrl || ''); const [coinPool, setCoinPool] = useState(coinPoolUrl || '');
const [oiTop, setOiTop] = useState(oiTopUrl || ''); const [oiTop, setOiTop] = useState(oiTopUrl || '');
@@ -820,7 +825,7 @@ function SignalSourceModal({
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}> <div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}>
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}> <h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
📡 📡 {t('signalSourceConfig', language)}
</h3> </h3>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
@@ -837,7 +842,7 @@ function SignalSourceModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/> />
<div className="text-xs mt-1" style={{ color: '#848E9C' }}> <div className="text-xs mt-1" style={{ color: '#848E9C' }}>
API地址使 {t('coinPoolDescription', language)}
</div> </div>
</div> </div>
@@ -854,18 +859,18 @@ function SignalSourceModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/> />
<div className="text-xs mt-1" style={{ color: '#848E9C' }}> <div className="text-xs mt-1" style={{ color: '#848E9C' }}>
API地址使 {t('oiTopDescription', language)}
</div> </div>
</div> </div>
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}> <div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}> <div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('information', language)}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div> URL</div> <div>{t('signalSourceInfo1', language)}</div>
<div> 使</div> <div>{t('signalSourceInfo2', language)}</div>
<div> URL将用于获取市场数据和交易信号</div> <div>{t('signalSourceInfo3', language)}</div>
</div> </div>
</div> </div>
@@ -876,14 +881,14 @@ function SignalSourceModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold" className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }} style={{ background: '#2B3139', color: '#848E9C' }}
> >
{t('cancel', language)}
</button> </button>
<button <button
type="submit" type="submit"
className="flex-1 px-4 py-2 rounded text-sm font-semibold" className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{t('save', language)}
</button> </button>
</div> </div>
</form> </form>
@@ -899,7 +904,8 @@ function ModelConfigModal({
editingModelId, editingModelId,
onSave, onSave,
onDelete, onDelete,
onClose onClose,
language
}: { }: {
allModels: AIModel[]; allModels: AIModel[];
configuredModels: AIModel[]; configuredModels: AIModel[];
@@ -907,6 +913,7 @@ function ModelConfigModal({
onSave: (modelId: string, apiKey: string, baseUrl?: string) => void; onSave: (modelId: string, apiKey: string, baseUrl?: string) => void;
onDelete: (modelId: string) => void; onDelete: (modelId: string) => void;
onClose: () => void; onClose: () => void;
language: Language;
}) { }) {
const [selectedModelId, setSelectedModelId] = useState(editingModelId || ''); const [selectedModelId, setSelectedModelId] = useState(editingModelId || '');
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState('');
@@ -940,30 +947,30 @@ function ModelConfigModal({
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}> <div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}> <h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{editingModelId ? '编辑AI模型' : '添加AI模型'} {editingModelId ? t('editAIModel', language) : t('addAIModel', language)}
</h3> </h3>
{editingModelId && ( {editingModelId && (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (confirm('确定要删除此AI模型配置吗')) { if (confirm(t('confirmDeleteModel', language))) {
onDelete(editingModelId); onDelete(editingModelId);
} }
}} }}
className="p-2 rounded hover:bg-red-100 transition-colors" className="p-2 rounded hover:bg-red-100 transition-colors"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
title="删除配置" title={t('deleteConfigFailed', language)}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
)} )}
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{!editingModelId && ( {!editingModelId && (
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
AI模型 {t('selectModel', language)}
</label> </label>
<select <select
value={selectedModelId} value={selectedModelId}
@@ -972,7 +979,7 @@ function ModelConfigModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
> >
<option value=""></option> <option value="">{t('pleaseSelectModel', language)}</option>
{availableModels.map(model => ( {availableModels.map(model => (
<option key={model.id} value={model.id}> <option key={model.id} value={model.id}>
{getShortName(model.name)} ({model.provider}) {getShortName(model.name)} ({model.provider})
@@ -1016,7 +1023,7 @@ function ModelConfigModal({
type="password" type="password"
value={apiKey} value={apiKey}
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
placeholder="输入API密钥" placeholder={t('enterAPIKey', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
@@ -1025,29 +1032,29 @@ function ModelConfigModal({
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
Base URL () {t('customBaseURL', language)}
</label> </label>
<input <input
type="url" type="url"
value={baseUrl} value={baseUrl}
onChange={(e) => setBaseUrl(e.target.value)} onChange={(e) => setBaseUrl(e.target.value)}
placeholder="自定义API基础URL如: https://api.openai.com/v1" placeholder={t('customBaseURLPlaceholder', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/> />
<div className="text-xs mt-1" style={{ color: '#848E9C' }}> <div className="text-xs mt-1" style={{ color: '#848E9C' }}>
使API地址 {t('leaveBlankForDefault', language)}
</div> </div>
</div> </div>
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}> <div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}> <div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('information', language)}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div> API Key将被加密存储</div> <div>{t('modelConfigInfo1', language)}</div>
<div> Base URL用于自定义API服务器地址</div> <div>{t('modelConfigInfo2', language)}</div>
<div> 使</div> <div>{t('modelConfigInfo3', language)}</div>
</div> </div>
</div> </div>
</> </>
@@ -1060,7 +1067,7 @@ function ModelConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold" className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }} style={{ background: '#2B3139', color: '#848E9C' }}
> >
{t('cancel', language)}
</button> </button>
<button <button
type="submit" type="submit"
@@ -1068,7 +1075,7 @@ function ModelConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50" className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{t('saveConfig', language)}
</button> </button>
</div> </div>
</form> </form>
@@ -1083,13 +1090,15 @@ function ExchangeConfigModal({
editingExchangeId, editingExchangeId,
onSave, onSave,
onDelete, onDelete,
onClose onClose,
language
}: { }: {
allExchanges: Exchange[]; allExchanges: Exchange[];
editingExchangeId: string | null; editingExchangeId: string | null;
onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => Promise<void>; onSave: (exchangeId: string, apiKey: string, secretKey?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string) => Promise<void>;
onDelete: (exchangeId: string) => void; onDelete: (exchangeId: string) => void;
onClose: () => void; onClose: () => void;
language: Language;
}) { }) {
const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || ''); const [selectedExchangeId, setSelectedExchangeId] = useState(editingExchangeId || '');
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState('');
@@ -1132,30 +1141,30 @@ function ExchangeConfigModal({
<div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}> <div className="bg-gray-800 rounded-lg p-6 w-full max-w-lg relative" style={{ background: '#1E2329' }}>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}> <h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{editingExchangeId ? '编辑交易所' : '添加交易所'} {editingExchangeId ? t('editExchange', language) : t('addExchange', language)}
</h3> </h3>
{editingExchangeId && ( {editingExchangeId && (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (confirm('确定要删除此交易所配置吗?')) { if (confirm(t('confirmDeleteExchange', language))) {
onDelete(editingExchangeId); onDelete(editingExchangeId);
} }
}} }}
className="p-2 rounded hover:bg-red-100 transition-colors" className="p-2 rounded hover:bg-red-100 transition-colors"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
title="删除配置" title={t('deleteConfigFailed', language)}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
)} )}
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{!editingExchangeId && ( {!editingExchangeId && (
<div> <div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}> <label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('selectExchange', language)}
</label> </label>
<select <select
value={selectedExchangeId} value={selectedExchangeId}
@@ -1164,7 +1173,7 @@ function ExchangeConfigModal({
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
> >
<option value=""></option> <option value="">{t('pleaseSelectExchange', language)}</option>
{availableExchanges.map(exchange => ( {availableExchanges.map(exchange => (
<option key={exchange.id} value={exchange.id}> <option key={exchange.id} value={exchange.id}>
{getShortName(exchange.name)} ({exchange.type.toUpperCase()}) {getShortName(exchange.name)} ({exchange.type.toUpperCase()})
@@ -1200,7 +1209,7 @@ function ExchangeConfigModal({
type="password" type="password"
value={apiKey} value={apiKey}
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
placeholder="输入API密钥" placeholder={t('enterAPIKey', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
@@ -1215,7 +1224,7 @@ function ExchangeConfigModal({
type="password" type="password"
value={secretKey} value={secretKey}
onChange={(e) => setSecretKey(e.target.value)} onChange={(e) => setSecretKey(e.target.value)}
placeholder="输入密钥" placeholder={t('enterSecretKey', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
@@ -1231,7 +1240,7 @@ function ExchangeConfigModal({
type="password" type="password"
value={passphrase} value={passphrase}
onChange={(e) => setPassphrase(e.target.value)} onChange={(e) => setPassphrase(e.target.value)}
placeholder="输入Passphrase (OKX必填)" placeholder={t('enterPassphrase', language)}
className="w-full px-3 py-2 rounded" className="w-full px-3 py-2 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
required required
@@ -1248,21 +1257,21 @@ function ExchangeConfigModal({
className="form-checkbox rounded" className="form-checkbox rounded"
style={{ accentColor: '#F0B90B' }} style={{ accentColor: '#F0B90B' }}
/> />
<span style={{ color: '#EAECEF' }}>使</span> <span style={{ color: '#EAECEF' }}>{t('useTestnet', language)}</span>
</label> </label>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}> <div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('testnetDescription', language)}
</div> </div>
</div> </div>
<div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}> <div className="p-4 rounded" style={{ background: 'rgba(240, 185, 11, 0.1)', border: '1px solid rgba(240, 185, 11, 0.2)' }}>
<div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}> <div className="text-sm font-semibold mb-2" style={{ color: '#F0B90B' }}>
{t('securityWarning', language)}
</div> </div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}> <div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div> API密钥将被加密存储使</div> <div>{t('exchangeConfigWarning1', language)}</div>
<div> </div> <div>{t('exchangeConfigWarning2', language)}</div>
<div> </div> <div>{t('exchangeConfigWarning3', language)}</div>
</div> </div>
</div> </div>
</> </>
@@ -1275,7 +1284,7 @@ function ExchangeConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold" className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }} style={{ background: '#2B3139', color: '#848E9C' }}
> >
{t('cancel', language)}
</button> </button>
<button <button
type="submit" type="submit"
@@ -1283,7 +1292,7 @@ function ExchangeConfigModal({
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50" className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }} style={{ background: '#F0B90B', color: '#000' }}
> >
{t('saveConfig', language)}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -8,8 +8,13 @@ export const translations = {
aiTraders: 'AI Traders', aiTraders: 'AI Traders',
details: 'Details', details: 'Details',
tradingPanel: 'Trading Panel', tradingPanel: 'Trading Panel',
competition: 'Competition',
running: 'RUNNING', running: 'RUNNING',
stopped: 'STOPPED', stopped: 'STOPPED',
adminMode: 'Admin Mode',
logout: 'Logout',
switchTrader: 'Switch Trader:',
view: 'View',
// Footer // Footer
footerTitle: 'NOFX - AI Trading System', footerTitle: 'NOFX - AI Trading System',
@@ -75,11 +80,14 @@ export const translations = {
aiCompetition: 'AI Competition', aiCompetition: 'AI Competition',
traders: 'traders', traders: 'traders',
liveBattle: 'Live Battle', liveBattle: 'Live Battle',
realTimeBattle: 'Real-time Battle',
leader: 'Leader', leader: 'Leader',
leaderboard: 'Leaderboard', leaderboard: 'Leaderboard',
live: 'LIVE', live: 'LIVE',
realTime: 'LIVE',
performanceComparison: 'Performance Comparison', performanceComparison: 'Performance Comparison',
realTimePnL: 'Real-time PnL %', realTimePnL: 'Real-time PnL %',
realTimePnLPercent: 'Real-time PnL %',
headToHead: 'Head-to-Head Battle', headToHead: 'Head-to-Head Battle',
leadingBy: 'Leading by {gap}%', leadingBy: 'Leading by {gap}%',
behindBy: 'Behind by {gap}%', behindBy: 'Behind by {gap}%',
@@ -190,6 +198,59 @@ export const translations = {
loading: 'Loading...', loading: 'Loading...',
loadingError: '⚠️ Failed to load AI learning data', loadingError: '⚠️ Failed to load AI learning data',
noCompleteData: 'No complete trading data (needs to complete open → close cycle)', noCompleteData: 'No complete trading data (needs to complete open → close cycle)',
// AI Traders Page - Additional
inUse: 'In Use',
noModelsConfigured: 'No configured AI models',
noExchangesConfigured: 'No configured exchanges',
signalSource: 'Signal Source',
signalSourceConfig: 'Signal Source Configuration',
coinPoolDescription: 'API endpoint for coin pool data, leave blank to disable this signal source',
oiTopDescription: 'API endpoint for open interest rankings, leave blank to disable this signal source',
information: 'Information',
signalSourceInfo1: '• Signal source configuration is per-user, each user can set their own URLs',
signalSourceInfo2: '• When creating traders, you can choose whether to use these signal sources',
signalSourceInfo3: '• Configured URLs will be used to fetch market data and trading signals',
editAIModel: 'Edit AI Model',
addAIModel: 'Add AI Model',
confirmDeleteModel: 'Are you sure you want to delete this AI model configuration?',
selectModel: 'Select AI Model',
pleaseSelectModel: 'Please select a model',
customBaseURL: 'Base URL (Optional)',
customBaseURLPlaceholder: 'Custom API base URL, e.g.: https://api.openai.com/v1',
leaveBlankForDefault: 'Leave blank to use default API address',
modelConfigInfo1: '• API Key will be encrypted and stored, please ensure it is valid',
modelConfigInfo2: '• Base URL is used for custom API server address',
modelConfigInfo3: '• After deleting configuration, traders using this model will not work properly',
saveConfig: 'Save Configuration',
editExchange: 'Edit Exchange',
addExchange: 'Add Exchange',
confirmDeleteExchange: 'Are you sure you want to delete this exchange configuration?',
selectExchange: 'Select Exchange',
pleaseSelectExchange: 'Please select an exchange',
enterSecretKey: 'Enter secret key',
enterPassphrase: 'Enter Passphrase (Required for OKX)',
testnetDescription: 'Enable to connect to exchange test environment for simulated trading',
securityWarning: 'Security Warning',
exchangeConfigWarning1: '• API keys will be encrypted, recommend using read-only or futures trading permissions',
exchangeConfigWarning2: '• Do not grant withdrawal permissions to ensure fund security',
exchangeConfigWarning3: '• After deleting configuration, related traders will not be able to trade',
edit: 'Edit',
// Error Messages
createTraderFailed: 'Failed to create trader',
getTraderConfigFailed: 'Failed to get trader configuration',
modelConfigNotExist: 'Model configuration does not exist or is not enabled',
exchangeConfigNotExist: 'Exchange configuration does not exist or is not enabled',
updateTraderFailed: 'Failed to update trader',
deleteTraderFailed: 'Failed to delete trader',
operationFailed: 'Operation failed',
deleteConfigFailed: 'Failed to delete configuration',
modelNotExist: 'Model does not exist',
saveConfigFailed: 'Failed to save configuration',
exchangeNotExist: 'Exchange does not exist',
deleteExchangeConfigFailed: 'Failed to delete exchange configuration',
saveSignalSourceFailed: 'Failed to save signal source configuration',
// Login & Register // Login & Register
login: 'Sign In', login: 'Sign In',
@@ -250,8 +311,13 @@ export const translations = {
aiTraders: 'AI交易员', aiTraders: 'AI交易员',
details: '详情', details: '详情',
tradingPanel: '交易面板', tradingPanel: '交易面板',
competition: '竞赛',
running: '运行中', running: '运行中',
stopped: '已停止', stopped: '已停止',
adminMode: '管理员模式',
logout: '退出',
switchTrader: '切换交易员:',
view: '查看',
// Footer // Footer
footerTitle: 'NOFX - AI交易系统', footerTitle: 'NOFX - AI交易系统',
@@ -317,11 +383,14 @@ export const translations = {
aiCompetition: 'AI竞赛', aiCompetition: 'AI竞赛',
traders: '交易员', traders: '交易员',
liveBattle: '实时对战', liveBattle: '实时对战',
realTimeBattle: '实时对战',
leader: '领先者', leader: '领先者',
leaderboard: '排行榜', leaderboard: '排行榜',
live: '实时', live: '实时',
realTime: '实时',
performanceComparison: '表现对比', performanceComparison: '表现对比',
realTimePnL: '实时收益率', realTimePnL: '实时收益率',
realTimePnLPercent: '实时收益率',
headToHead: '正面对决', headToHead: '正面对决',
leadingBy: '领先 {gap}%', leadingBy: '领先 {gap}%',
behindBy: '落后 {gap}%', behindBy: '落后 {gap}%',
@@ -432,6 +501,59 @@ export const translations = {
loading: '加载中...', loading: '加载中...',
loadingError: '⚠️ 加载AI学习数据失败', loadingError: '⚠️ 加载AI学习数据失败',
noCompleteData: '暂无完整交易数据(需要完成开仓→平仓的完整周期)', noCompleteData: '暂无完整交易数据(需要完成开仓→平仓的完整周期)',
// AI Traders Page - Additional
inUse: '正在使用',
noModelsConfigured: '暂无已配置的AI模型',
noExchangesConfigured: '暂无已配置的交易所',
signalSource: '信号源',
signalSourceConfig: '信号源配置',
coinPoolDescription: '用于获取币种池数据的API地址留空则不使用此信号源',
oiTopDescription: '用于获取持仓量排行数据的API地址留空则不使用此信号源',
information: '说明',
signalSourceInfo1: '• 信号源配置为用户级别每个用户可以设置自己的信号源URL',
signalSourceInfo2: '• 在创建交易员时可以选择是否使用这些信号源',
signalSourceInfo3: '• 配置的URL将用于获取市场数据和交易信号',
editAIModel: '编辑AI模型',
addAIModel: '添加AI模型',
confirmDeleteModel: '确定要删除此AI模型配置吗',
selectModel: '选择AI模型',
pleaseSelectModel: '请选择模型',
customBaseURL: 'Base URL (可选)',
customBaseURLPlaceholder: '自定义API基础URL如: https://api.openai.com/v1',
leaveBlankForDefault: '留空则使用默认API地址',
modelConfigInfo1: '• API Key将被加密存储请确保密钥有效',
modelConfigInfo2: '• Base URL用于自定义API服务器地址',
modelConfigInfo3: '• 删除配置后,使用此模型的交易员将无法正常工作',
saveConfig: '保存配置',
editExchange: '编辑交易所',
addExchange: '添加交易所',
confirmDeleteExchange: '确定要删除此交易所配置吗?',
selectExchange: '选择交易所',
pleaseSelectExchange: '请选择交易所',
enterSecretKey: '输入密钥',
enterPassphrase: '输入Passphrase (OKX必填)',
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
securityWarning: '安全提示',
exchangeConfigWarning1: '• API密钥将被加密存储建议使用只读或期货交易权限',
exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全',
exchangeConfigWarning3: '• 删除配置后,相关交易员将无法正常交易',
edit: '编辑',
// Error Messages
createTraderFailed: '创建交易员失败',
getTraderConfigFailed: '获取交易员配置失败',
modelConfigNotExist: 'AI模型配置不存在或未启用',
exchangeConfigNotExist: '交易所配置不存在或未启用',
updateTraderFailed: '更新交易员失败',
deleteTraderFailed: '删除交易员失败',
operationFailed: '操作失败',
deleteConfigFailed: '删除配置失败',
modelNotExist: '模型不存在',
saveConfigFailed: '保存配置失败',
exchangeNotExist: '交易所不存在',
deleteExchangeConfigFailed: '删除交易所配置失败',
saveSignalSourceFailed: '保存信号源配置失败',
// Login & Register // Login & Register
login: '登录', login: '登录',