feat: integrate i18next for internationalization support; add English and Chinese translations

This commit is contained in:
samanhappy@qq.com
2025-04-10 22:40:11 +08:00
parent 4cf1bfaadd
commit 11c80f7469
14 changed files with 253 additions and 33 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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
View 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;

View 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"
}
}

View 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": "发生错误"
}
}

View File

@@ -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>