mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: integrate i18next for internationalization support; add English and Chinese translations
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server, ApiResponse } from './types'
|
||||
import ServerCard from './components/ServerCard'
|
||||
import AddServerForm from './components/AddServerForm'
|
||||
import EditServerForm from './components/EditServerForm'
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation()
|
||||
const [servers, setServers] = useState<Server[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
@@ -67,7 +69,7 @@ function App() {
|
||||
setEditingServer(fullServerData)
|
||||
} else {
|
||||
console.error('Failed to get server config from settings:', settingsData)
|
||||
setError(`Could not find configuration data for ${server.name}`)
|
||||
setError(t('server.invalidConfig', { serverName: server.name }))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -89,13 +91,13 @@ function App() {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
setError(result.message || `Failed to delete server ${serverName}`)
|
||||
setError(result.message || t('server.deleteError', { serverName }))
|
||||
return
|
||||
}
|
||||
|
||||
setRefreshKey(prevKey => prevKey + 1)
|
||||
} catch (err) {
|
||||
setError('Error: ' + (err instanceof Error ? err.message : String(err)))
|
||||
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,13 +106,13 @@ function App() {
|
||||
<div className="min-h-screen bg-red-50 p-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<h2 className="text-red-600 text-xl font-semibold">Error</h2>
|
||||
<h2 className="text-red-600 text-xl font-semibold">{t('app.error')}</h2>
|
||||
<p className="text-gray-600 mt-2">{error}</p>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="mt-4 bg-red-100 text-red-800 py-1 px-3 rounded hover:bg-red-200"
|
||||
>
|
||||
Close
|
||||
{t('app.closeButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,12 +124,12 @@ function App() {
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">MCP Hub Dashboard</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">{t('app.title')}</h1>
|
||||
<AddServerForm onAdd={handleServerAdd} />
|
||||
</div>
|
||||
{servers.length === 0 ? (
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<p className="text-gray-600">No MCP servers available</p>
|
||||
<p className="text-gray-600">{t('app.noServers')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ServerForm from './ServerForm'
|
||||
|
||||
interface AddServerFormProps {
|
||||
@@ -6,6 +7,7 @@ interface AddServerFormProps {
|
||||
}
|
||||
|
||||
const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
|
||||
const toggleModal = () => {
|
||||
@@ -40,12 +42,12 @@ const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
||||
onClick={toggleModal}
|
||||
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
|
||||
>
|
||||
Add New Server
|
||||
{t('server.addServer')}
|
||||
</button>
|
||||
|
||||
{modalVisible && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<ServerForm onSubmit={handleSubmit} onCancel={toggleModal} modalTitle="Add New Server" />
|
||||
<ServerForm onSubmit={handleSubmit} onCancel={toggleModal} modalTitle={t('server.addServer')} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import ServerForm from './ServerForm'
|
||||
|
||||
@@ -8,6 +9,8 @@ interface EditServerFormProps {
|
||||
}
|
||||
|
||||
const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleSubmit = async (payload: any) => {
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${server.name}`, {
|
||||
@@ -19,13 +22,13 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
alert(result.message || 'Failed to update server')
|
||||
alert(result.message || t('server.updateError', 'Failed to update server'))
|
||||
return
|
||||
}
|
||||
|
||||
onEdit()
|
||||
} catch (err) {
|
||||
alert(`Error: ${err instanceof Error ? err.message : String(err)}`)
|
||||
alert(`${t('errors.general')}: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +38,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
initialData={server}
|
||||
modalTitle={`Edit Server: ${server.name}`}
|
||||
modalTitle={t('server.editTitle', {serverName: server.name})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import { ChevronDown, ChevronRight } from '@/components/icons/LucideIcons'
|
||||
import Badge from '@/components/ui/Badge'
|
||||
@@ -12,6 +13,7 @@ interface ServerCardProps {
|
||||
}
|
||||
|
||||
const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
|
||||
@@ -45,13 +47,13 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
|
||||
onClick={handleEdit}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm"
|
||||
>
|
||||
Edit
|
||||
{t('server.edit')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className="px-3 py-1 bg-red-100 text-red-800 rounded hover:bg-red-200 text-sm"
|
||||
>
|
||||
Delete
|
||||
{t('server.delete')}
|
||||
</button>
|
||||
<button className="text-gray-400 hover:text-gray-600">
|
||||
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
|
||||
@@ -68,7 +70,7 @@ const ServerCard = ({ server, onRemove, onEdit }: ServerCardProps) => {
|
||||
|
||||
{isExpanded && server.tools && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Available Tools</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">{t('server.tools')}</h3>
|
||||
<div className="space-y-4">
|
||||
{server.tools.map((tool, index) => (
|
||||
<ToolCard key={index} tool={tool} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server, EnvVar, ServerFormData } from '@/types'
|
||||
|
||||
interface ServerFormProps {
|
||||
@@ -9,6 +10,7 @@ interface ServerFormProps {
|
||||
}
|
||||
|
||||
const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: ServerFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [serverType, setServerType] = useState<'sse' | 'stdio'>(
|
||||
initialData && initialData.config && initialData.config.url ? 'sse' : 'stdio',
|
||||
)
|
||||
@@ -107,7 +109,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
||||
Server Name
|
||||
{t('server.name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -123,7 +125,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">Server Type</label>
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">{t('server.type')}</label>
|
||||
<div className="flex space-x-4">
|
||||
<div>
|
||||
<input
|
||||
@@ -155,7 +157,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
{serverType === 'sse' ? (
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url">
|
||||
Server URL
|
||||
{t('server.url')}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
@@ -172,7 +174,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="command">
|
||||
Command
|
||||
{t('server.command')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -187,7 +189,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="arguments">
|
||||
Arguments
|
||||
{t('server.arguments')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -204,14 +206,14 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="block text-gray-700 text-sm font-bold">
|
||||
Environment Variables
|
||||
{t('server.envVars')}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addEnvVar}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center"
|
||||
>
|
||||
+ Add
|
||||
+ {t('server.add')}
|
||||
</button>
|
||||
</div>
|
||||
{envVars.map((envVar, index) => (
|
||||
@@ -222,7 +224,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
value={envVar.key}
|
||||
onChange={(e) => handleEnvVarChange(index, 'key', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
|
||||
placeholder="key"
|
||||
placeholder={t('server.key')}
|
||||
/>
|
||||
<span className="flex items-center">:</span>
|
||||
<input
|
||||
@@ -230,7 +232,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
value={envVar.value}
|
||||
onChange={(e) => handleEnvVarChange(index, 'value', e.target.value)}
|
||||
className="shadow appearance-none border rounded py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline w-1/2"
|
||||
placeholder="value"
|
||||
placeholder={t('server.value')}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -238,7 +240,7 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
onClick={() => removeEnvVar(index)}
|
||||
className="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-1 px-2 rounded text-sm flex items-center justify-center min-w-[56px] ml-2"
|
||||
>
|
||||
- Remove
|
||||
- {t('server.remove')}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -252,13 +254,13 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle }: Serv
|
||||
onClick={onCancel}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
Cancel
|
||||
{t('server.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
|
||||
>
|
||||
{isEdit ? 'Save Changes' : 'Add Server'}
|
||||
{isEdit ? t('server.save') : t('server.add')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ServerStatus } from '@/types'
|
||||
|
||||
interface BadgeProps {
|
||||
@@ -5,17 +6,26 @@ interface BadgeProps {
|
||||
}
|
||||
|
||||
const Badge = ({ status }: BadgeProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const colors = {
|
||||
connecting: 'bg-yellow-100 text-yellow-800',
|
||||
connected: 'bg-green-100 text-green-800',
|
||||
disconnected: 'bg-red-100 text-red-800',
|
||||
}
|
||||
|
||||
// Map status to translation keys
|
||||
const statusTranslations = {
|
||||
connected: 'status.online',
|
||||
disconnected: 'status.offline',
|
||||
connecting: 'status.connecting'
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${colors[status]}`}
|
||||
>
|
||||
{status}
|
||||
{t(statusTranslations[status] || status)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface DeleteDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
@@ -6,27 +8,29 @@ interface DeleteDialogProps {
|
||||
}
|
||||
|
||||
const DeleteDialog = ({ isOpen, onClose, onConfirm, serverName }: DeleteDialogProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white shadow rounded-lg p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Delete Server</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">{t('server.delete')}</h3>
|
||||
<p className="text-gray-700">
|
||||
Are you sure you want to delete server <strong>{serverName}</strong>? This action cannot be undone.
|
||||
{t('server.confirmDelete')} <strong>{serverName}</strong>
|
||||
</p>
|
||||
<div className="flex justify-end mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
Cancel
|
||||
{t('server.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded"
|
||||
>
|
||||
Delete
|
||||
{t('server.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
42
frontend/src/i18n.ts
Normal file
42
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// Import translations
|
||||
import enTranslation from './locales/en.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
// Detect user language
|
||||
.use(LanguageDetector)
|
||||
// Pass the i18n instance to react-i18next
|
||||
.use(initReactI18next)
|
||||
// Initialize i18next
|
||||
.init({
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation
|
||||
}
|
||||
},
|
||||
fallbackLng: 'en',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
// Common namespace used for all translations
|
||||
defaultNS: 'translation',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // React already safe from XSS
|
||||
},
|
||||
|
||||
detection: {
|
||||
// Order of detection; we put 'navigator' first to use browser language
|
||||
order: ['navigator', 'localStorage', 'cookie', 'htmlTag'],
|
||||
// Cache the language in localStorage
|
||||
caches: ['localStorage', 'cookie'],
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
41
frontend/src/locales/en.json
Normal file
41
frontend/src/locales/en.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "MCP Hub Dashboard",
|
||||
"error": "Error",
|
||||
"closeButton": "Close",
|
||||
"noServers": "No MCP servers available"
|
||||
},
|
||||
"server": {
|
||||
"addServer": "Add Server",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"confirmDelete": "Are you sure you want to delete this server?",
|
||||
"status": "Status",
|
||||
"tools": "Tools",
|
||||
"name": "Server Name",
|
||||
"url": "Server URL",
|
||||
"apiKey": "API Key",
|
||||
"save": "Save Changes",
|
||||
"cancel": "Cancel",
|
||||
"invalidConfig": "Could not find configuration data for {{serverName}}",
|
||||
"deleteError": "Failed to delete server {{serverName}}",
|
||||
"updateError": "Failed to update server",
|
||||
"editTitle": "Edit Server: {{serverName}}",
|
||||
"type": "Server Type",
|
||||
"command": "Command",
|
||||
"arguments": "Arguments",
|
||||
"envVars": "Environment Variables",
|
||||
"key": "key",
|
||||
"value": "value",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"status": {
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"connecting": "Connecting"
|
||||
},
|
||||
"errors": {
|
||||
"general": "Something went wrong"
|
||||
}
|
||||
}
|
||||
41
frontend/src/locales/zh.json
Normal file
41
frontend/src/locales/zh.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"app": {
|
||||
"title": "MCP Hub 控制面板",
|
||||
"error": "错误",
|
||||
"closeButton": "关闭",
|
||||
"noServers": "没有可用的 MCP 服务器"
|
||||
},
|
||||
"server": {
|
||||
"addServer": "添加服务器",
|
||||
"add": "添加",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"confirmDelete": "您确定要删除此服务器吗?",
|
||||
"status": "状态",
|
||||
"tools": "工具",
|
||||
"name": "服务器名称",
|
||||
"url": "服务器 URL",
|
||||
"apiKey": "API 密钥",
|
||||
"save": "保存更改",
|
||||
"cancel": "取消",
|
||||
"invalidConfig": "无法找到 {{serverName}} 的配置数据",
|
||||
"deleteError": "删除服务器 {{serverName}} 失败",
|
||||
"updateError": "更新服务器失败",
|
||||
"editTitle": "编辑服务器: {{serverName}}",
|
||||
"type": "服务器类型",
|
||||
"command": "命令",
|
||||
"arguments": "参数",
|
||||
"envVars": "环境变量",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"remove": "移除"
|
||||
},
|
||||
"status": {
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"connecting": "连接中"
|
||||
},
|
||||
"errors": {
|
||||
"general": "发生错误"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
// Import the i18n configuration
|
||||
import './i18n'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
|
||||
Reference in New Issue
Block a user