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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 \n- > quotes and - lists\n\nDrag & drop or paste images to upload'
|
||||
: '在这里写下你的内容...\n\n支持 Markdown 语法:\n- **粗体** 和 *斜体*\n- `代码` 和 ```代码块```\n- [链接](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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user