feat(i18n): 统一国际化系统架构,支持插件独立翻译 (#301)

* feat(i18n): 统一国际化系统架构,支持插件独立翻译

## 主要改动

### 核心架构
- 增强 LocaleService,支持插件命名空间翻译扩展
- 新增 editor-runtime/i18n 模块,提供 createPluginLocale/createPluginTranslator
- 新增 editor-core/tokens.ts,定义 LocaleServiceToken 等服务令牌
- 改进 PluginAPI 类型安全,使用 ServiceToken<T> 替代 any

### 编辑器本地化
- 扩展 en.ts/zh.ts 翻译文件,覆盖所有 UI 组件
- 新增 es.ts 西班牙语支持
- 重构 40+ 组件使用 useLocale() hook

### 插件本地化系统
- behavior-tree-editor: 新增 locales/ 和 useBTLocale hook
- material-editor: 新增 locales/ 和 useMaterialLocale hook
- particle-editor: 新增 locales/ 和 useParticleLocale hook
- tilemap-editor: 新增 locales/ 和 useTilemapLocale hook
- ui-editor: 新增 locales/ 和 useUILocale hook

### 类型安全改进
- 修复 Debug 工具使用公共接口替代 as any
- 修复 ChunkStreamingSystem 添加 forEachChunk 公共方法
- 修复 blueprint-editor 移除不必要的向后兼容代码

* fix(behavior-tree-editor): 使用 ServiceToken 模式修复服务解析

- 创建 BehaviorTreeServiceToken 遵循"谁定义接口,谁导出Token"原则
- 使用 ServiceToken.id (symbol) 注册服务到 ServiceContainer
- 更新 PluginSDKRegistry.resolveService 支持 ServiceToken 检测
- BehaviorTreeEditorPanel 现在使用类型安全的 PluginAPI.resolve

* fix(behavior-tree-editor): 使用 ServiceContainer.resolve 获取类注册的服务

* fix: 修复多个包的依赖和类型问题

- core: EntityDataCollector.getEntityDetails 使用 HierarchySystem 获取父实体
- ui-editor: 添加 @esengine/editor-runtime 依赖
- tilemap-editor: 添加 @esengine/editor-runtime 依赖
- particle-editor: 添加 @esengine/editor-runtime 依赖
This commit is contained in:
YHH
2025-12-09 18:04:03 +08:00
committed by GitHub
parent 995fa2d514
commit 1b0d38edce
103 changed files with 8015 additions and 1633 deletions

View File

@@ -4,15 +4,15 @@
*/
import { AlertCircle, CheckCircle, ExternalLink, Github, Loader } from 'lucide-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { open } from '@tauri-apps/plugin-shell';
import { useLocale } from '../../hooks/useLocale';
import { useForumAuth } from '../../hooks/useForum';
import './ForumAuth.css';
type AuthStatus = 'idle' | 'pending' | 'authorized' | 'error';
export function ForumAuth() {
const { i18n } = useTranslation();
const { t } = useLocale();
const { requestDeviceCode, authenticateWithDeviceFlow, signInWithGitHubToken } = useForumAuth();
const [authStatus, setAuthStatus] = useState<AuthStatus>('idle');
@@ -20,8 +20,6 @@ export function ForumAuth() {
const [verificationUri, setVerificationUri] = useState('');
const [error, setError] = useState<string | null>(null);
const isEnglish = i18n.language === 'en';
const handleGitHubLogin = async () => {
setAuthStatus('pending');
setError(null);
@@ -60,7 +58,7 @@ export function ForumAuth() {
} catch (err) {
console.error('[ForumAuth] GitHub login failed:', err);
setAuthStatus('error');
setError(err instanceof Error ? err.message : (isEnglish ? 'Authorization failed' : '授权失败'));
setError(err instanceof Error ? err.message : t('forum.authFailed'));
}
};
@@ -84,21 +82,21 @@ export function ForumAuth() {
<div className="forum-auth-card">
<div className="forum-auth-header">
<Github size={32} className="forum-auth-icon" />
<h2>{isEnglish ? 'ESEngine Community' : 'ESEngine 社区'}</h2>
<p>{isEnglish ? 'Sign in with GitHub to join the discussion' : '使用 GitHub 登录参与讨论'}</p>
<h2>{t('forum.communityTitle')}</h2>
<p>{t('forum.signInWithGitHub')}</p>
</div>
{/* 初始状态 | Idle state */}
{authStatus === 'idle' && (
<div className="forum-auth-content">
<div className="forum-auth-instructions">
<p>{isEnglish ? '1. Click the button below' : '1. 点击下方按钮'}</p>
<p>{isEnglish ? '2. Enter the code on GitHub' : '2. 在 GitHub 页面输入验证码'}</p>
<p>{isEnglish ? '3. Authorize the application' : '3. 授权应用'}</p>
<p>{t('forum.step1')}</p>
<p>{t('forum.step2')}</p>
<p>{t('forum.step3')}</p>
</div>
<button className="forum-auth-github-btn" onClick={handleGitHubLogin}>
<Github size={16} />
<span>{isEnglish ? 'Continue with GitHub' : '使用 GitHub 登录'}</span>
<span>{t('forum.continueWithGitHub')}</span>
</button>
</div>
)}
@@ -108,18 +106,18 @@ export function ForumAuth() {
<div className="forum-auth-pending">
<Loader size={24} className="spinning" />
<p className="forum-auth-pending-text">
{isEnglish ? 'Waiting for authorization...' : '等待授权中...'}
{t('forum.waitingForAuth')}
</p>
{userCode && (
<div className="forum-auth-code-section">
<label>{isEnglish ? 'Enter this code on GitHub:' : '在 GitHub 输入此验证码:'}</label>
<label>{t('forum.enterCodeOnGitHub')}</label>
<div className="forum-auth-code-box">
<span className="forum-auth-code">{userCode}</span>
<button
className="forum-auth-copy-btn"
onClick={() => copyToClipboard(userCode)}
title={isEnglish ? 'Copy code' : '复制验证码'}
title={t('forum.copyCode')}
>
📋
</button>
@@ -129,7 +127,7 @@ export function ForumAuth() {
onClick={() => open(verificationUri)}
>
<ExternalLink size={14} />
<span>{isEnglish ? 'Open GitHub' : '打开 GitHub'}</span>
<span>{t('forum.openGitHub')}</span>
</button>
</div>
)}
@@ -140,7 +138,7 @@ export function ForumAuth() {
{authStatus === 'authorized' && (
<div className="forum-auth-success">
<CheckCircle size={32} className="forum-auth-success-icon" />
<p>{isEnglish ? 'Authorization successful!' : '授权成功!'}</p>
<p>{t('forum.authSuccess')}</p>
</div>
)}
@@ -148,10 +146,10 @@ export function ForumAuth() {
{authStatus === 'error' && (
<div className="forum-auth-error-state">
<AlertCircle size={32} className="forum-auth-error-icon" />
<p>{isEnglish ? 'Authorization failed' : '授权失败'}</p>
<p>{t('forum.authFailed')}</p>
{error && <p className="forum-auth-error-detail">{error}</p>}
<button className="forum-auth-retry-btn" onClick={handleRetry}>
{isEnglish ? 'Try Again' : '重试'}
{t('forum.tryAgain')}
</button>
</div>
)}

View File

@@ -10,6 +10,7 @@ import {
} from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useLocale } from '../../hooks/useLocale';
import { getForumService } from '../../services/forum';
import type { Category } from '../../services/forum';
import { parseEmoji } from './utils';
@@ -17,14 +18,14 @@ import './ForumCreatePost.css';
interface ForumCreatePostProps {
categories: Category[];
isEnglish: boolean;
onBack: () => void;
onCreated: () => void;
}
type EditorTab = 'write' | 'preview';
export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: ForumCreatePostProps) {
export function ForumCreatePost({ categories, onBack, onCreated }: ForumCreatePostProps) {
const { t } = useLocale();
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [categoryId, setCategoryId] = useState<string | null>(null);
@@ -76,12 +77,12 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
}
} catch (err) {
console.error('[ForumCreatePost] Upload failed:', err);
setError(err instanceof Error ? err.message : (isEnglish ? 'Failed to upload image' : '图片上传失败'));
setError(err instanceof Error ? err.message : t('forum.failedToUploadImage'));
} finally {
setUploading(false);
setUploadProgress(0);
}
}, [body, forumService, isEnglish, uploading]);
}, [body, forumService, t, uploading]);
/**
* 处理拖拽事件
@@ -147,15 +148,15 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
// 验证 | Validation
if (!title.trim()) {
setError(isEnglish ? 'Please enter a title' : '请输入标题');
setError(t('forum.enterTitle'));
return;
}
if (!body.trim()) {
setError(isEnglish ? 'Please enter content' : '请输入内容');
setError(t('forum.enterContent'));
return;
}
if (!categoryId) {
setError(isEnglish ? 'Please select a category' : '请选择分类');
setError(t('forum.selectCategoryError'));
return;
}
@@ -170,11 +171,11 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
if (post) {
onCreated();
} else {
setError(isEnglish ? 'Failed to create discussion' : '创建讨论失败,请稍后重试');
setError(t('forum.failedToCreateDiscussion'));
}
} catch (err) {
console.error('[ForumCreatePost] Error:', err);
setError(err instanceof Error ? err.message : (isEnglish ? 'An error occurred' : '发生错误,请稍后重试'));
setError(err instanceof Error ? err.message : t('forum.anErrorOccurred'));
} finally {
setSubmitting(false);
}
@@ -201,13 +202,13 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
};
const toolbarButtons = [
{ icon: <Bold size={14} />, action: () => insertMarkdown('**', '**', 'bold'), title: isEnglish ? 'Bold' : '粗体' },
{ icon: <Italic size={14} />, action: () => insertMarkdown('*', '*', 'italic'), title: isEnglish ? 'Italic' : '斜体' },
{ icon: <Code size={14} />, action: () => insertMarkdown('`', '`', 'code'), title: isEnglish ? 'Inline code' : '行内代码' },
{ icon: <Link size={14} />, action: () => insertMarkdown('[', '](url)', 'link text'), title: isEnglish ? 'Link' : '链接' },
{ icon: <List size={14} />, action: () => insertMarkdown('\n- ', '', 'list item'), title: isEnglish ? 'List' : '列表' },
{ icon: <Quote size={14} />, action: () => insertMarkdown('\n> ', '', 'quote'), title: isEnglish ? 'Quote' : '引用' },
{ icon: <Upload size={14} />, action: () => fileInputRef.current?.click(), title: isEnglish ? 'Upload image' : '上传图片' },
{ icon: <Bold size={14} />, action: () => insertMarkdown('**', '**', 'bold'), title: t('forum.bold') },
{ icon: <Italic size={14} />, action: () => insertMarkdown('*', '*', 'italic'), title: t('forum.italic') },
{ icon: <Code size={14} />, action: () => insertMarkdown('`', '`', 'code'), title: t('forum.inlineCode') },
{ icon: <Link size={14} />, action: () => insertMarkdown('[', '](url)', 'link text'), title: t('forum.link') },
{ icon: <List size={14} />, action: () => insertMarkdown('\n- ', '', 'list item'), title: t('forum.list') },
{ icon: <Quote size={14} />, action: () => insertMarkdown('\n> ', '', 'quote'), title: t('forum.quote') },
{ icon: <Upload size={14} />, action: () => fileInputRef.current?.click(), title: t('forum.uploadImage') },
];
const selectedCategory = categories.find(c => c.id === categoryId);
@@ -217,14 +218,14 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
{/* 返回按钮 | Back button */}
<button className="forum-back-btn" onClick={onBack}>
<ArrowLeft size={18} />
<span>{isEnglish ? 'Back to list' : '返回列表'}</span>
<span>{t('forum.backToList')}</span>
</button>
<div className="forum-create-container">
{/* 左侧:编辑区 | Left: Editor */}
<div className="forum-create-main">
<div className="forum-create-header">
<h2>{isEnglish ? 'Start a Discussion' : '发起讨论'}</h2>
<h2>{t('forum.startDiscussion')}</h2>
{selectedCategory && (
<span className="forum-create-selected-category">
{parseEmoji(selectedCategory.emoji)} {selectedCategory.name}
@@ -235,7 +236,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
<form className="forum-create-form" onSubmit={handleSubmit}>
{/* 分类选择 | Category selection */}
<div className="forum-create-field">
<label>{isEnglish ? 'Select Category' : '选择分类'}</label>
<label>{t('forum.selectCategory')}</label>
<div className="forum-create-categories">
{categories.map(cat => (
<button
@@ -256,13 +257,13 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
{/* 标题 | Title */}
<div className="forum-create-field">
<label>{isEnglish ? 'Title' : '标题'}</label>
<label>{t('forum.title')}</label>
<div className="forum-create-title-input">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={isEnglish ? 'Enter a descriptive title...' : '输入一个描述性的标题...'}
placeholder={t('forum.enterDescriptiveTitle')}
maxLength={200}
/>
<span className="forum-create-count">{title.length}/200</span>
@@ -279,7 +280,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
onClick={() => setActiveTab('write')}
>
<Edit3 size={14} />
<span>{isEnglish ? 'Write' : '编辑'}</span>
<span>{t('forum.write')}</span>
</button>
<button
type="button"
@@ -287,7 +288,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
onClick={() => setActiveTab('preview')}
>
<Eye size={14} />
<span>{isEnglish ? 'Preview' : '预览'}</span>
<span>{t('forum.preview')}</span>
</button>
</div>
@@ -309,7 +310,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
target="_blank"
rel="noopener noreferrer"
className="forum-editor-help"
title={isEnglish ? 'Markdown Help' : 'Markdown 帮助'}
title={t('forum.markdownHelp')}
>
<HelpCircle size={14} />
</a>
@@ -336,7 +337,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
{uploading && (
<div className="forum-editor-upload-overlay">
<Loader2 size={24} className="spin" />
<span>{isEnglish ? 'Uploading...' : '上传中...'} {uploadProgress}%</span>
<span>{t('forum.uploading')} {uploadProgress}%</span>
</div>
)}
@@ -344,7 +345,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
{isDragging && !uploading && (
<div className="forum-editor-drag-overlay">
<Upload size={32} />
<span>{isEnglish ? 'Drop image here' : '拖放图片到这里'}</span>
<span>{t('forum.dropImageHere')}</span>
</div>
)}
@@ -355,9 +356,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
value={body}
onChange={(e) => setBody(e.target.value)}
onPaste={handlePaste}
placeholder={isEnglish
? 'Write your content here...\n\nYou can use Markdown:\n- **bold** and *italic*\n- `code` and ```code blocks```\n- [links](url) and ![images](url)\n- > quotes and - lists\n\nDrag & drop or paste images to upload'
: '在这里写下你的内容...\n\n支持 Markdown 语法:\n- **粗体** 和 *斜体*\n- `代码` 和 ```代码块```\n- [链接](url) 和 ![图片](url)\n- > 引用 和 - 列表\n\n拖拽或粘贴图片即可上传'}
placeholder={t('forum.editorPlaceholder')}
/>
) : (
<div className="forum-editor-preview">
@@ -367,7 +366,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
</ReactMarkdown>
) : (
<p className="forum-editor-preview-empty">
{isEnglish ? 'Nothing to preview' : '暂无内容可预览'}
{t('forum.nothingToPreview')}
</p>
)}
</div>
@@ -391,7 +390,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
onClick={onBack}
disabled={submitting}
>
{isEnglish ? 'Cancel' : '取消'}
{t('forum.cancel')}
</button>
<button
type="submit"
@@ -400,9 +399,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
>
<Send size={16} />
<span>
{submitting
? (isEnglish ? 'Creating...' : '创建中...')
: (isEnglish ? 'Create Discussion' : '创建讨论')}
{submitting ? t('forum.creating') : t('forum.createDiscussion')}
</span>
</button>
</div>
@@ -412,18 +409,18 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
{/* 右侧:提示 | Right: Tips */}
<div className="forum-create-sidebar">
<div className="forum-create-tips">
<h3>{isEnglish ? 'Tips' : '小贴士'}</h3>
<h3>{t('forum.tips')}</h3>
<ul>
<li>{isEnglish ? 'Use a clear, descriptive title' : '使用清晰、描述性的标题'}</li>
<li>{isEnglish ? 'Select the right category for your topic' : '为你的话题选择合适的分类'}</li>
<li>{isEnglish ? 'Provide enough context and details' : '提供足够的背景和细节'}</li>
<li>{isEnglish ? 'Use code blocks for code snippets' : '使用代码块展示代码'}</li>
<li>{isEnglish ? 'Be respectful and constructive' : '保持尊重和建设性'}</li>
<li>{t('forum.tip1')}</li>
<li>{t('forum.tip2')}</li>
<li>{t('forum.tip3')}</li>
<li>{t('forum.tip4')}</li>
<li>{t('forum.tip5')}</li>
</ul>
</div>
<div className="forum-create-markdown-guide">
<h3>{isEnglish ? 'Markdown Guide' : 'Markdown 指南'}</h3>
<h3>{t('forum.markdownGuide')}</h3>
<div className="forum-create-markdown-examples">
<div className="markdown-example">
<code>**bold**</code>

View File

@@ -3,8 +3,8 @@
* Forum panel main component - GitHub Discussions
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { MessageSquare, RefreshCw } from 'lucide-react';
import { useLocale } from '../../hooks/useLocale';
import { useForumAuth, useCategories, usePosts } from '../../hooks/useForum';
import { ForumAuth } from './ForumAuth';
import { ForumPostList } from './ForumPostList';
@@ -20,7 +20,8 @@ type ForumView = 'list' | 'detail' | 'create';
* 认证后的论坛内容组件 | Authenticated forum content component
* 只有在用户认证后才会渲染,确保 hooks 能正常工作
*/
function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean }) {
function ForumContent({ user }: { user: ForumUser }) {
const { t } = useLocale();
const { categories, refetch: refetchCategories } = useCategories();
const [view, setView] = useState<ForumView>('list');
const [selectedPostNumber, setSelectedPostNumber] = useState<number | null>(null);
@@ -80,14 +81,14 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
<div className="forum-header-left">
<MessageSquare size={18} />
<span className="forum-title">
{isEnglish ? 'Community' : '社区'}
{t('forum.community')}
</span>
</div>
<div className="forum-header-right">
<div
className="forum-user"
onClick={() => setShowProfile(!showProfile)}
title={isEnglish ? 'Click to view profile' : '点击查看资料'}
title={t('forum.clickToViewProfile')}
>
<img
src={user.avatarUrl}
@@ -118,7 +119,6 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
totalCount={totalCount}
hasNextPage={pageInfo.hasNextPage}
params={listParams}
isEnglish={isEnglish}
onViewPost={handleViewPost}
onCreatePost={handleCreatePost}
onCategoryChange={handleCategoryChange}
@@ -130,7 +130,6 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
{view === 'detail' && selectedPostNumber && (
<ForumPostDetail
postNumber={selectedPostNumber}
isEnglish={isEnglish}
currentUserId={user.id}
onBack={handleBack}
/>
@@ -138,7 +137,6 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
{view === 'create' && (
<ForumCreatePost
categories={categories}
isEnglish={isEnglish}
onBack={handleBack}
onCreated={handlePostCreated}
/>
@@ -149,18 +147,16 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
}
export function ForumPanel() {
const { i18n } = useTranslation();
const { t } = useLocale();
const { authState } = useForumAuth();
const isEnglish = i18n.language === 'en';
// 加载状态 | Loading state
if (authState.status === 'loading') {
return (
<div className="forum-panel">
<div className="forum-loading">
<RefreshCw className="spin" size={24} />
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
<span>{t('forum.loading')}</span>
</div>
</div>
);
@@ -178,7 +174,7 @@ export function ForumPanel() {
// 已登录状态 - 渲染内容组件 | Authenticated state - render content component
return (
<div className="forum-panel">
<ForumContent user={authState.user} isEnglish={isEnglish} />
<ForumContent user={authState.user} />
</div>
);
}

View File

@@ -8,6 +8,7 @@ import {
Send, RefreshCw, CornerDownRight, ExternalLink, CheckCircle
} from 'lucide-react';
import { open } from '@tauri-apps/plugin-shell';
import { useLocale } from '../../hooks/useLocale';
import { usePost, useReplies } from '../../hooks/useForum';
import { getForumService } from '../../services/forum';
import type { Reply } from '../../services/forum';
@@ -16,12 +17,12 @@ import './ForumPostDetail.css';
interface ForumPostDetailProps {
postNumber: number;
isEnglish: boolean;
currentUserId: string;
onBack: () => void;
}
export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }: ForumPostDetailProps) {
export function ForumPostDetail({ postNumber, currentUserId, onBack }: ForumPostDetailProps) {
const { t } = useLocale();
const { post, loading: postLoading, toggleUpvote, refetch: refetchPost } = usePost(postNumber);
const { replies, loading: repliesLoading, createReply, refetch: refetchReplies } = useReplies(postNumber);
const [replyContent, setReplyContent] = useState('');
@@ -71,7 +72,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
{reply.isAnswer && (
<span className="forum-reply-answer-badge">
<CheckCircle size={12} />
{isEnglish ? 'Answer' : '已采纳'}
{t('forum.answer')}
</span>
)}
</div>
@@ -99,7 +100,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
onClick={() => setReplyingTo(replyingTo === reply.id ? null : reply.id)}
>
<CornerDownRight size={14} />
<span>{isEnglish ? 'Reply' : '回复'}</span>
<span>{t('forum.reply')}</span>
</button>
</div>
@@ -108,9 +109,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
<textarea
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder={isEnglish
? `Reply to @${reply.author.login}...`
: `回复 @${reply.author.login}...`}
placeholder={t('forum.replyTo', { login: reply.author.login })}
rows={2}
/>
<div className="forum-reply-form-actions">
@@ -119,7 +118,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
className="forum-btn"
onClick={() => { setReplyingTo(null); setReplyContent(''); }}
>
{isEnglish ? 'Cancel' : '取消'}
{t('forum.cancel')}
</button>
<button
type="submit"
@@ -127,7 +126,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
disabled={!replyContent.trim() || submitting}
>
<Send size={14} />
<span>{isEnglish ? 'Reply' : '回复'}</span>
<span>{t('forum.reply')}</span>
</button>
</div>
</form>
@@ -144,7 +143,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
<div className="forum-post-detail">
<div className="forum-detail-loading">
<RefreshCw className="spin" size={24} />
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
<span>{t('forum.loading')}</span>
</div>
</div>
);
@@ -155,7 +154,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
{/* 返回按钮 | Back button */}
<button className="forum-back-btn" onClick={onBack}>
<ArrowLeft size={18} />
<span>{isEnglish ? 'Back to list' : '返回列表'}</span>
<span>{t('forum.backToList')}</span>
</button>
{/* 帖子内容 | Post content */}
@@ -168,13 +167,13 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
{post.answerChosenAt && (
<span className="forum-detail-answered">
<CheckCircle size={14} />
{isEnglish ? 'Answered' : '已解决'}
{t('forum.answered')}
</span>
)}
<button
className="forum-detail-external"
onClick={() => openInGitHub(post.url)}
title={isEnglish ? 'Open in GitHub' : '在 GitHub 中打开'}
title={t('forum.openInGitHub')}
>
<ExternalLink size={14} />
<span>GitHub</span>
@@ -221,7 +220,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
<h2 className="forum-replies-title">
<MessageCircle size={18} />
<span>
{isEnglish ? 'Comments' : '评论'}
{t('forum.comments')}
{post.comments.totalCount > 0 && ` (${post.comments.totalCount})`}
</span>
</h2>
@@ -232,7 +231,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
<textarea
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder={isEnglish ? 'Write a comment... (Markdown supported)' : '写下你的评论...(支持 Markdown'}
placeholder={t('forum.writeComment')}
rows={3}
/>
<div className="forum-reply-form-actions">
@@ -242,9 +241,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
disabled={!replyContent.trim() || submitting}
>
<Send size={14} />
<span>{submitting
? (isEnglish ? 'Posting...' : '发送中...')
: (isEnglish ? 'Post Comment' : '发表评论')}</span>
<span>{submitting ? t('forum.posting') : t('forum.postComment')}</span>
</button>
</div>
</form>
@@ -258,7 +255,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
</div>
) : replies.length === 0 ? (
<div className="forum-replies-empty">
<p>{isEnglish ? 'No comments yet. Be the first to comment!' : '暂无评论,来发表第一条评论吧!'}</p>
<p>{t('forum.noCommentsYet')}</p>
</div>
) : (
replies.map(reply => renderReply(reply))

View File

@@ -9,6 +9,7 @@ import {
Lightbulb, HelpCircle, Megaphone, BarChart3, Github
} from 'lucide-react';
import { open } from '@tauri-apps/plugin-shell';
import { useLocale } from '../../hooks/useLocale';
import type { Post, Category, PostListParams } from '../../services/forum';
import { parseEmoji } from './utils';
import './ForumPostList.css';
@@ -20,7 +21,6 @@ interface ForumPostListProps {
totalCount: number;
hasNextPage: boolean;
params: PostListParams;
isEnglish: boolean;
onViewPost: (postNumber: number) => void;
onCreatePost: () => void;
onCategoryChange: (categoryId: string | undefined) => void;
@@ -48,7 +48,6 @@ export function ForumPostList({
totalCount,
hasNextPage,
params,
isEnglish,
onViewPost,
onCreatePost,
onCategoryChange,
@@ -56,6 +55,7 @@ export function ForumPostList({
onRefresh,
onLoadMore
}: ForumPostListProps) {
const { t } = useLocale();
const [searchInput, setSearchInput] = useState(params.search || '');
const handleSearchSubmit = (e: React.FormEvent) => {
@@ -73,13 +73,13 @@ export function ForumPostList({
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours === 0) {
const mins = Math.floor(diff / (1000 * 60));
if (mins < 1) return isEnglish ? 'Just now' : '刚刚';
return isEnglish ? `${mins}m ago` : `${mins}分钟前`;
if (mins < 1) return t('forum.justNow');
return t('forum.minutesAgo', { count: mins });
}
return isEnglish ? `${hours}h ago` : `${hours}小时前`;
return t('forum.hoursAgo', { count: hours });
}
if (days === 1) return isEnglish ? 'Yesterday' : '昨天';
if (days < 7) return isEnglish ? `${days}d ago` : `${days}天前`;
if (days === 1) return t('forum.yesterday');
if (days < 7) return t('forum.daysAgo', { count: days });
return date.toLocaleDateString();
};
@@ -108,21 +108,17 @@ export function ForumPostList({
<div className="forum-welcome-banner">
<div className="forum-welcome-content">
<div className="forum-welcome-text">
<h2>{isEnglish ? 'ESEngine Community' : 'ESEngine 社区'}</h2>
<p>
{isEnglish
? 'Ask questions, share ideas, and connect with other developers'
: '提出问题、分享想法,与其他开发者交流'}
</p>
<h2>{t('forum.communityTitle')}</h2>
<p>{t('forum.askQuestionsShareIdeas')}</p>
</div>
<div className="forum-welcome-actions">
<button className="forum-btn forum-btn-primary" onClick={onCreatePost}>
<Plus size={14} />
<span>{isEnglish ? 'New Discussion' : '发起讨论'}</span>
<span>{t('forum.newDiscussion')}</span>
</button>
<button className="forum-btn forum-btn-github" onClick={openGitHubDiscussions}>
<Github size={14} />
<span>{isEnglish ? 'View on GitHub' : '在 GitHub 查看'}</span>
<span>{t('forum.viewOnGitHub')}</span>
</button>
</div>
</div>
@@ -156,7 +152,7 @@ export function ForumPostList({
value={params.categoryId || ''}
onChange={(e) => onCategoryChange(e.target.value || undefined)}
>
<option value="">{isEnglish ? 'All Categories' : '全部分类'}</option>
<option value="">{t('forum.allCategories')}</option>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>
{parseEmoji(cat.emoji)} {cat.name}
@@ -168,7 +164,7 @@ export function ForumPostList({
<Search size={14} />
<input
type="text"
placeholder={isEnglish ? 'Search discussions...' : '搜索讨论...'}
placeholder={t('forum.searchDiscussions')}
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
/>
@@ -180,7 +176,7 @@ export function ForumPostList({
className="forum-btn"
onClick={onRefresh}
disabled={loading}
title={isEnglish ? 'Refresh' : '刷新'}
title={t('forum.refresh')}
>
<RefreshCw size={14} className={loading ? 'spin' : ''} />
</button>
@@ -189,7 +185,7 @@ export function ForumPostList({
onClick={onCreatePost}
>
<Plus size={14} />
<span>{isEnglish ? 'New' : '发帖'}</span>
<span>{t('forum.new')}</span>
</button>
</div>
</div>
@@ -198,14 +194,14 @@ export function ForumPostList({
<div className="forum-stats">
<div className="forum-stats-left">
<TrendingUp size={14} />
<span>{totalCount} {isEnglish ? 'discussions' : '条讨论'}</span>
<span>{totalCount} {t('forum.discussions')}</span>
</div>
{params.categoryId && (
<button
className="forum-stats-clear"
onClick={() => onCategoryChange(undefined)}
>
{isEnglish ? 'Clear filter' : '清除筛选'}
{t('forum.clearFilter')}
</button>
)}
</div>
@@ -222,15 +218,15 @@ export function ForumPostList({
{loading && posts.length === 0 ? (
<div className="forum-posts-loading">
<RefreshCw size={16} className="spin" />
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
<span>{t('forum.loading')}</span>
</div>
) : posts.length === 0 ? (
<div className="forum-posts-empty">
<MessageCircle size={32} />
<p>{isEnglish ? 'No discussions yet' : '暂无讨论'}</p>
<p>{t('forum.noDiscussionsYet')}</p>
<button className="forum-btn forum-btn-primary" onClick={onCreatePost}>
<Plus size={14} />
<span>{isEnglish ? 'Start a discussion' : '发起讨论'}</span>
<span>{t('forum.startADiscussion')}</span>
</button>
</div>
) : (
@@ -258,13 +254,13 @@ export function ForumPostList({
{isRecentPost(post) && (
<span className="forum-post-badge new">
<Clock size={10} />
{isEnglish ? 'New' : '新'}
{t('forum.newBadge')}
</span>
)}
{isHotPost(post) && (
<span className="forum-post-badge hot">
<Flame size={10} />
{isEnglish ? 'Hot' : '热门'}
{t('forum.hotBadge')}
</span>
)}
</div>
@@ -272,7 +268,7 @@ export function ForumPostList({
<button
className="forum-post-external"
onClick={(e) => openInGitHub(post.url, e)}
title={isEnglish ? 'Open in GitHub' : '在 GitHub 中打开'}
title={t('forum.openInGitHub')}
>
<ExternalLink size={12} />
</button>
@@ -306,7 +302,7 @@ export function ForumPostList({
{post.answerChosenAt && (
<span className="forum-post-answered">
<CheckCircle size={12} />
{isEnglish ? 'Answered' : '已解决'}
{t('forum.answered')}
</span>
)}
</div>
@@ -325,10 +321,10 @@ export function ForumPostList({
{loading ? (
<>
<RefreshCw size={14} className="spin" />
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
<span>{t('forum.loading')}</span>
</>
) : (
<span>{isEnglish ? 'Load More' : '加载更多'}</span>
<span>{t('forum.loadMore')}</span>
)}
</button>
</div>

View File

@@ -2,9 +2,9 @@
* 用户资料组件 - GitHub
* User profile component - GitHub
*/
import { useTranslation } from 'react-i18next';
import { Github, LogOut, ExternalLink } from 'lucide-react';
import { open } from '@tauri-apps/plugin-shell';
import { useLocale } from '../../hooks/useLocale';
import { useForumAuth } from '../../hooks/useForum';
import './ForumProfile.css';
@@ -13,10 +13,9 @@ interface ForumProfileProps {
}
export function ForumProfile({ onClose }: ForumProfileProps) {
const { i18n } = useTranslation();
const { t } = useLocale();
const { authState, signOut } = useForumAuth();
const isEnglish = i18n.language === 'en';
const user = authState.status === 'authenticated' ? authState.user : null;
const handleSignOut = async () => {
@@ -47,7 +46,7 @@ export function ForumProfile({ onClose }: ForumProfileProps) {
onClick={openGitHubProfile}
>
<Github size={12} />
<span>{isEnglish ? 'View GitHub Profile' : '查看 GitHub 主页'}</span>
<span>{t('forum.viewGitHubProfile')}</span>
<ExternalLink size={10} />
</button>
</div>
@@ -58,7 +57,7 @@ export function ForumProfile({ onClose }: ForumProfileProps) {
<div className="forum-profile-actions">
<button className="forum-profile-btn logout" onClick={handleSignOut}>
<LogOut size={14} />
<span>{isEnglish ? 'Sign Out' : '退出登录'}</span>
<span>{t('forum.signOut')}</span>
</button>
</div>
</div>